"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import Link from "next/link"; const FIELD_W = 600; const FIELD_H = 400; const BALL_R = 12; const PADDLE_W = 12; const PADDLE_H = 70; const CPU_SPEED = 3.2; const INITIAL_SPEED = 4.5; type Vec = { x: number; y: number }; type GameState = "idle" | "playing" | "paused" | "over"; function clamp(v: number, lo: number, hi: number) { return Math.max(lo, Math.min(hi, v)); } const HIGH_SCORE_KEY = "cyberlux-pong-hs"; function getHighScore() { if (typeof window === "undefined") return 0; return parseInt(localStorage.getItem(HIGH_SCORE_KEY) ?? "0", 10); } function setHighScore(n: number) { if (typeof window !== "undefined") localStorage.setItem(HIGH_SCORE_KEY, String(n)); } export default function PongPage() { const canvasRef = useRef(null); const stateRef = useRef("idle"); const ballRef = useRef({ x: FIELD_W / 2, y: FIELD_H / 2 }); const velRef = useRef({ x: INITIAL_SPEED, y: INITIAL_SPEED }); const playerYRef = useRef((FIELD_H - PADDLE_H) / 2); const cpuYRef = useRef((FIELD_H - PADDLE_H) / 2); const scoreRef = useRef({ player: 0, cpu: 0 }); const rafRef = useRef(0); const mouseYRef = useRef(FIELD_H / 2); const [displayScore, setDisplayScore] = useState({ player: 0, cpu: 0 }); const [gameState, setGameState] = useState("idle"); const [highScore, setHighScoreState] = useState(0); const [rally, setRally] = useState(0); const rallyRef = useRef(0); useEffect(() => { setHighScoreState(getHighScore()); }, []); const reset = useCallback(() => { ballRef.current = { x: FIELD_W / 2, y: FIELD_H / 2 }; const angle = (Math.random() * Math.PI) / 3 - Math.PI / 6; const dir = Math.random() > 0.5 ? 1 : -1; velRef.current = { x: INITIAL_SPEED * dir * Math.cos(angle), y: INITIAL_SPEED * Math.sin(angle), }; rallyRef.current = 0; setRally(0); }, []); const draw = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; // Background ctx.fillStyle = "#050510"; ctx.fillRect(0, 0, FIELD_W, FIELD_H); // Center line ctx.setLineDash([8, 8]); ctx.strokeStyle = "rgba(0,255,200,0.1)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(FIELD_W / 2, 0); ctx.lineTo(FIELD_W / 2, FIELD_H); ctx.stroke(); ctx.setLineDash([]); // Paddles const drawPaddle = (x: number, y: number, color: string) => { ctx.shadowColor = color; ctx.shadowBlur = 12; ctx.fillStyle = color; ctx.fillRect(x, y, PADDLE_W, PADDLE_H); ctx.shadowBlur = 0; }; drawPaddle(8, playerYRef.current, "#00ffcc"); drawPaddle(FIELD_W - 8 - PADDLE_W, cpuYRef.current, "#ff00ff"); // Ball ctx.shadowColor = "#00ffcc"; ctx.shadowBlur = 20; ctx.fillStyle = "#fff"; ctx.beginPath(); ctx.arc(ballRef.current.x, ballRef.current.y, BALL_R, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; // Score overlay ctx.fillStyle = "rgba(0,255,200,0.15)"; ctx.font = "bold 36px monospace"; ctx.textAlign = "center"; ctx.fillText(String(scoreRef.current.player), FIELD_W / 4, 50); ctx.fillStyle = "rgba(255,0,255,0.15)"; ctx.fillText(String(scoreRef.current.cpu), (FIELD_W * 3) / 4, 50); }, []); const tick = useCallback(() => { if (stateRef.current !== "playing") return; // Move ball const b = ballRef.current; const v = velRef.current; b.x += v.x; b.y += v.y; // Wall bounce top/bottom if (b.y - BALL_R <= 0) { b.y = BALL_R; v.y = Math.abs(v.y); } if (b.y + BALL_R >= FIELD_H) { b.y = FIELD_H - BALL_R; v.y = -Math.abs(v.y); } // CPU paddle AI const cpuCenter = cpuYRef.current + PADDLE_H / 2; if (cpuCenter < b.y - 4) cpuYRef.current = clamp(cpuYRef.current + CPU_SPEED, 0, FIELD_H - PADDLE_H); if (cpuCenter > b.y + 4) cpuYRef.current = clamp(cpuYRef.current - CPU_SPEED, 0, FIELD_H - PADDLE_H); // Player paddle follow mouse playerYRef.current = clamp(mouseYRef.current - PADDLE_H / 2, 0, FIELD_H - PADDLE_H); // Player paddle hit if (b.x - BALL_R <= 8 + PADDLE_W && b.y >= playerYRef.current && b.y <= playerYRef.current + PADDLE_H && v.x < 0) { b.x = 8 + PADDLE_W + BALL_R; const hitPos = (b.y - playerYRef.current) / PADDLE_H - 0.5; const speed = Math.min(Math.sqrt(v.x ** 2 + v.y ** 2) + 0.15, 10); v.x = Math.abs(speed * Math.cos(hitPos * Math.PI * 0.6)); v.y = speed * Math.sin(hitPos * Math.PI * 0.6); rallyRef.current++; setRally(rallyRef.current); } // CPU paddle hit if (b.x + BALL_R >= FIELD_W - 8 - PADDLE_W && b.y >= cpuYRef.current && b.y <= cpuYRef.current + PADDLE_H && v.x > 0) { b.x = FIELD_W - 8 - PADDLE_W - BALL_R; const hitPos = (b.y - cpuYRef.current) / PADDLE_H - 0.5; const speed = Math.sqrt(v.x ** 2 + v.y ** 2); v.x = -Math.abs(speed * Math.cos(hitPos * Math.PI * 0.6)); v.y = speed * Math.sin(hitPos * Math.PI * 0.6); rallyRef.current++; setRally(rallyRef.current); } // Scoring if (b.x < 0) { scoreRef.current.cpu++; setDisplayScore({ ...scoreRef.current }); if (scoreRef.current.cpu >= 7) { stateRef.current = "over"; setGameState("over"); const hs = getHighScore(); if (rallyRef.current > hs) { setHighScore(rallyRef.current); setHighScoreState(rallyRef.current); } } else reset(); } if (b.x > FIELD_W) { scoreRef.current.player++; setDisplayScore({ ...scoreRef.current }); if (scoreRef.current.player >= 7) { stateRef.current = "over"; setGameState("over"); const hs = getHighScore(); if (rallyRef.current > hs) { setHighScore(rallyRef.current); setHighScoreState(rallyRef.current); } } else reset(); } draw(); rafRef.current = requestAnimationFrame(tick); }, [draw, reset]); const startGame = useCallback(() => { scoreRef.current = { player: 0, cpu: 0 }; setDisplayScore({ player: 0, cpu: 0 }); stateRef.current = "playing"; setGameState("playing"); reset(); if (rafRef.current) cancelAnimationFrame(rafRef.current); rafRef.current = requestAnimationFrame(tick); }, [tick, reset]); useEffect(() => { draw(); return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }; }, [draw]); const handleMouseMove = useCallback((e: React.MouseEvent) => { const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; const scaleY = FIELD_H / rect.height; mouseYRef.current = (e.clientY - rect.top) * scaleY; }, []); const handleTouchMove = useCallback((e: React.TouchEvent) => { e.preventDefault(); const rect = canvasRef.current?.getBoundingClientRect(); if (!rect || !e.touches[0]) return; const scaleY = FIELD_H / rect.height; mouseYRef.current = (e.touches[0].clientY - rect.top) * scaleY; }, []); return (

VOID PONG

Move mouse over field to control paddle. First to 7.

Rally record: {highScore}
{rally > 0 && gameState === "playing" && (
Current rally: {rally}
)}
{gameState !== "idle" && (
YOU โ€” {displayScore.player} vs {displayScore.cpu} โ€” CPU
)} {gameState === "idle" && (

Move your mouse over the field to control the left paddle.

)} {gameState === "playing" && (
Rally: {rally} hits
)} {gameState === "over" && (
{displayScore.player >= 7 ? ( You Win ) : ( CPU Wins )}
Score: {displayScore.player} โ€” {displayScore.cpu} ยท Rally: {rallyRef.current} {rallyRef.current > 0 && rallyRef.current >= highScore && ( New record! )}
)}
Hub Arcade Shadow Lotto
); }