import { useState, useEffect, useCallback, useRef } from 'react'; import { createPortal } from 'react-dom'; import { motion } from 'framer-motion'; import { useSettings } from '@/contexts/SettingsContext'; import { useAchievements } from '@/contexts/AchievementsContext'; import { Link } from 'react-router-dom'; import GlitchCrash from '@/components/GlitchCrash'; import { Maximize2, Minimize2 } from 'lucide-react'; import { useBrowserFullscreen } from '@/hooks/useGameDimensions'; import GameTouchButton from '@/components/GameTouchButton'; import { MobileControlsDock, MobileControlsSpacer } from '@/components/MobileControlsDock'; import MatrixCursor from '@/components/MatrixCursor'; const PADDLE_HEIGHT = 14; const BRICK_ROWS = 6; const BRICK_COLS = 10; const BRICK_HEIGHT = 20; const BRICK_GAP = 4; const MAX_SCORE = 4294967296; const HIGHSCORE_KEY = 'breakout-highscore'; interface Brick { x: number; y: number; width: number; height: number; alive: boolean; color: string; hasPowerUp?: PowerUpType; } type PowerUpType = 'wide' | 'multi' | 'slow' | 'life'; interface PowerUp { x: number; y: number; type: PowerUpType; active: boolean; } const POWER_UP_COLORS: Record = { wide: 'hsl(200 80% 60%)', multi: 'hsl(280 80% 60%)', slow: 'hsl(45 80% 60%)', life: 'hsl(0 80% 60%)', }; const POWER_UP_LABELS: Record = { wide: 'W', multi: 'M', slow: 'S', life: '+', }; const Breakout = () => { const { playSound } = useSettings(); const { checkGameScoreAchievements, unlockMaxScore } = useAchievements(); const [isFullscreen, setIsFullscreen] = useState(false); const [score, setScore] = useState(0); const [highScore, setHighScore] = useState(0); const [level, setLevel] = useState(1); const [lives, setLives] = useState(3); const [gameOver, setGameOver] = useState(false); const [gameStarted, setGameStarted] = useState(false); const [isPaused, setIsPaused] = useState(false); const [showGlitchCrash, setShowGlitchCrash] = useState(false); const [isTwoPlayer, setIsTwoPlayer] = useState(false); // Power-up state const [activePowerUps, setActivePowerUps] = useState<{ type: PowerUpType; endTime: number }[]>([]); const powerUpsRef = useRef([]); const extraBallsRef = useRef<{ x: number; y: number; dx: number; dy: number }[]>([]); const basePaddleWidth = useRef(90); // P2 state const [score2, setScore2] = useState(0); const [lives2, setLives2] = useState(3); const [level2, setLevel2] = useState(1); const canvasRef = useRef(null); const canvas2Ref = useRef(null); const gameRef = useRef(null); const paddleXRef = useRef(160); const paddle2XRef = useRef(160); const ballRef = useRef({ x: 200, y: 450, dx: 4, dy: -4 }); const ball2Ref = useRef({ x: 200, y: 450, dx: 4, dy: -4 }); const bricksRef = useRef([]); const bricks2Ref = useRef([]); const keysRef = useRef>(new Set()); const animationRef = useRef(); const animation2Ref = useRef(); const p1DeadRef = useRef(false); const p2DeadRef = useRef(false); const { enterFullscreen, exitFullscreen } = useBrowserFullscreen(); const getCanvasSize = useCallback(() => { if (typeof window === 'undefined') return { width: 560, height: 680 }; const isMobile = window.innerWidth < 768; if (isMobile) { const maxWidth = window.innerWidth - 32; const maxHeight = window.innerHeight - 380; const aspectRatio = 560 / 680; let width = maxWidth; let height = width / aspectRatio; if (height > maxHeight) { height = maxHeight; width = height * aspectRatio; } return { width: Math.floor(width), height: Math.floor(height) }; } if (isFullscreen) { const aspectRatio = 560 / 680; if (isTwoPlayer) { // Two canvases side by side const maxWidth = (window.innerWidth - 100) / 2; const maxHeight = window.innerHeight - 120; let width = maxWidth; let height = width / aspectRatio; if (height > maxHeight) { height = maxHeight; width = height * aspectRatio; } return { width: Math.floor(width), height: Math.floor(height) }; } // Single player - fill screen const maxWidth = window.innerWidth - 280; const maxHeight = window.innerHeight - 100; let width = maxWidth; let height = width / aspectRatio; if (height > maxHeight) { height = maxHeight; width = height * aspectRatio; } return { width: Math.floor(width), height: Math.floor(height) }; } return isTwoPlayer ? { width: 400, height: 500 } : { width: 560, height: 680 }; }, [isFullscreen, isTwoPlayer]); const [canvasSize, setCanvasSize] = useState(getCanvasSize); const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; useEffect(() => { const handleResize = () => setCanvasSize(getCanvasSize()); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [getCanvasSize]); useEffect(() => { setCanvasSize(getCanvasSize()); }, [isFullscreen, getCanvasSize, isTwoPlayer]); // Calculate paddle width based on active power-ups const hasWidePaddle = activePowerUps.some(p => p.type === 'wide' && p.endTime > Date.now()); const hasSlowBall = activePowerUps.some(p => p.type === 'slow' && p.endTime > Date.now()); basePaddleWidth.current = isMobile ? 70 : (isTwoPlayer ? 70 : (isFullscreen ? 100 : 90)); const paddleWidth = hasWidePaddle ? basePaddleWidth.current * 1.5 : basePaddleWidth.current; const ballSize = isMobile ? 10 : (isTwoPlayer ? 10 : (isFullscreen ? 14 : 12)); useEffect(() => { if (gameStarted && isMobile && !isFullscreen) { setIsFullscreen(true); enterFullscreen(); } }, [gameStarted, isMobile, isFullscreen, enterFullscreen]); // Prevent page scroll while in fullscreen mode (portal overlays the viewport) useEffect(() => { if (typeof document === 'undefined') return; if (!isFullscreen) return; const prevOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prevOverflow; }; }, [isFullscreen]); const toggleFullscreen = async () => { if (!isFullscreen) { setIsFullscreen(true); await enterFullscreen(); } else { setIsFullscreen(false); await exitFullscreen(); } playSound('click'); }; useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape' && isFullscreen) { setIsFullscreen(false); exitFullscreen(); } }; window.addEventListener('keydown', handleEscape); return () => window.removeEventListener('keydown', handleEscape); }, [isFullscreen, exitFullscreen]); useEffect(() => { const saved = localStorage.getItem(HIGHSCORE_KEY); if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE)); }, []); useEffect(() => { if (score > 0) { checkGameScoreAchievements('breakout', score); if (score >= MAX_SCORE) unlockMaxScore(); } }, [score, checkGameScoreAchievements, unlockMaxScore]); const initBricks = useCallback((width: number) => { const bricks: Brick[] = []; const brickWidth = (width - (BRICK_COLS + 1) * BRICK_GAP) / BRICK_COLS; const colors = ['hsl(0 70% 50%)', 'hsl(30 70% 50%)', 'hsl(60 70% 50%)', 'hsl(120 70% 50%)', 'hsl(200 70% 50%)', 'hsl(280 70% 50%)']; const powerUpTypes: PowerUpType[] = ['wide', 'multi', 'slow', 'life']; for (let row = 0; row < BRICK_ROWS; row++) { for (let col = 0; col < BRICK_COLS; col++) { // ~15% chance of having a power-up const hasPowerUp = Math.random() < 0.15; const powerUpType = hasPowerUp ? powerUpTypes[Math.floor(Math.random() * powerUpTypes.length)] : undefined; bricks.push({ x: BRICK_GAP + col * (brickWidth + BRICK_GAP), y: 60 + row * (BRICK_HEIGHT + BRICK_GAP), width: brickWidth, height: BRICK_HEIGHT, alive: true, color: colors[row % colors.length], hasPowerUp: powerUpType }); } } return bricks; }, []); const resetBall = useCallback((ballRefToReset: React.MutableRefObject<{x: number, y: number, dx: number, dy: number}>, paddleRefToReset: React.MutableRefObject, lvl: number) => { // Slower base speed for classic Breakout feel (was 2.5) const speed = 1.8 + lvl * 0.2; ballRefToReset.current = { x: canvasSize.width / 2, y: canvasSize.height - 60, dx: (Math.random() > 0.5 ? 1 : -1) * speed, dy: -speed }; paddleRefToReset.current = canvasSize.width / 2 - paddleWidth / 2; }, [canvasSize.width, canvasSize.height, paddleWidth]); const startGame = (twoPlayer: boolean) => { setIsTwoPlayer(twoPlayer); setScore(0); setLevel(1); setLives(3); setGameOver(false); setIsPaused(false); setGameStarted(true); setActivePowerUps([]); powerUpsRef.current = []; extraBallsRef.current = []; p1DeadRef.current = false; p2DeadRef.current = false; if (twoPlayer) { setScore2(0); setLevel2(1); setLives2(3); } setTimeout(() => { const size = getCanvasSize(); bricksRef.current = initBricks(size.width); resetBall(ballRef, paddleXRef, 1); if (twoPlayer) { bricks2Ref.current = initBricks(size.width); resetBall(ball2Ref, paddle2XRef, 1); } }, 50); playSound('success'); gameRef.current?.focus(); }; const nextLevel = useCallback((player: 1 | 2) => { if (player === 1) { setLevel(prev => { const newLevel = prev + 1; bricksRef.current = initBricks(canvasSize.width); resetBall(ballRef, paddleXRef, newLevel); return newLevel; }); } else { setLevel2(prev => { const newLevel = prev + 1; bricks2Ref.current = initBricks(canvasSize.width); resetBall(ball2Ref, paddle2XRef, newLevel); return newLevel; }); } playSound('success'); }, [initBricks, resetBall, playSound, canvasSize.width]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (isTwoPlayer) { // P1: A/D, P2: Arrow keys if (['a', 'd', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { e.preventDefault(); keysRef.current.add(e.key); } } else { if (['ArrowLeft', 'ArrowRight', 'a', 'd'].includes(e.key)) { e.preventDefault(); keysRef.current.add(e.key); } } if (e.key === 'p' && gameStarted && !gameOver) { setIsPaused(prev => !prev); playSound('click'); } }; const handleKeyUp = (e: KeyboardEvent) => { keysRef.current.delete(e.key); }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); }; }, [gameStarted, gameOver, playSound, isTwoPlayer]); const handleTouchMove = useCallback((e: React.TouchEvent) => { if (!gameStarted || gameOver || isPaused) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const touch = e.touches[0]; const x = touch.clientX - rect.left; paddleXRef.current = Math.max(0, Math.min(canvasSize.width - paddleWidth, x - paddleWidth / 2)); }, [gameStarted, gameOver, isPaused, canvasSize.width, paddleWidth]); // Mouse move handler for desktop const handleMouseMove = useCallback((e: React.MouseEvent) => { if (!gameStarted || gameOver || isPaused || isMobile || isTwoPlayer) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; paddleXRef.current = Math.max(0, Math.min(canvasSize.width - paddleWidth, x - paddleWidth / 2)); }, [gameStarted, gameOver, isPaused, canvasSize.width, paddleWidth, isMobile, isTwoPlayer]); const moveLeft = useCallback(() => { paddleXRef.current = Math.max(0, paddleXRef.current - 20); }, []); const moveRight = useCallback(() => { paddleXRef.current = Math.min(canvasSize.width - paddleWidth, paddleXRef.current + 20); }, [canvasSize.width, paddleWidth]); // Game loop for a player const runGameLoop = useCallback(( canvas: HTMLCanvasElement, ballRefParam: React.MutableRefObject<{x: number, y: number, dx: number, dy: number}>, paddleRefParam: React.MutableRefObject, bricksRefParam: React.MutableRefObject, setScoreParam: React.Dispatch>, setLivesParam: React.Dispatch>, getLives: () => number, playerNum: 1 | 2, primaryColor: string, leftKey: string, rightKey: string, deadRef: React.MutableRefObject ) => { const ctx = canvas.getContext('2d'); if (!ctx) return; const paddleSpeed = isMobile ? 4 : (isFullscreen ? 6 : 5); if (keysRef.current.has(leftKey)) paddleRefParam.current = Math.max(0, paddleRefParam.current - paddleSpeed); if (keysRef.current.has(rightKey)) paddleRefParam.current = Math.min(canvasSize.width - paddleWidth, paddleRefParam.current + paddleSpeed); const ball = ballRefParam.current; ball.x += ball.dx; ball.y += ball.dy; if (ball.x <= ballSize / 2 || ball.x >= canvasSize.width - ballSize / 2) { ball.dx = -ball.dx; playSound('hover'); } if (ball.y <= ballSize / 2) { ball.dy = -ball.dy; playSound('hover'); } const paddleY = canvasSize.height - 30; if (ball.y + ballSize / 2 >= paddleY && ball.y - ballSize / 2 <= paddleY + PADDLE_HEIGHT && ball.x >= paddleRefParam.current && ball.x <= paddleRefParam.current + paddleWidth) { const hitPos = (ball.x - paddleRefParam.current) / paddleWidth; const angle = (hitPos - 0.5) * Math.PI * 0.7; const speed = Math.sqrt(ball.dx * ball.dx + ball.dy * ball.dy); ball.dx = Math.sin(angle) * speed; ball.dy = -Math.abs(Math.cos(angle) * speed); ball.y = paddleY - ballSize / 2; playSound('click'); } if (ball.y >= canvasSize.height && !deadRef.current) { const currentLives = getLives(); const newLives = currentLives - 1; setLivesParam(newLives); if (newLives <= 0) { deadRef.current = true; playSound('error'); // Check if game over (both players dead in 2P, or single player dead) if (!isTwoPlayer) { setGameOver(true); } else { // Check if both dead if ((playerNum === 1 && p2DeadRef.current) || (playerNum === 2 && p1DeadRef.current)) { setGameOver(true); } } } else { resetBall(ballRefParam, paddleRefParam, playerNum === 1 ? level : level2); playSound('error'); } } let allDestroyed = true; for (const brick of bricksRefParam.current) { if (!brick.alive) continue; allDestroyed = false; if (ball.x + ballSize / 2 >= brick.x && ball.x - ballSize / 2 <= brick.x + brick.width && ball.y + ballSize / 2 >= brick.y && ball.y - ballSize / 2 <= brick.y + brick.height) { brick.alive = false; ball.dy = -ball.dy; // Spawn power-up if brick had one (P1 only for simplicity) if (brick.hasPowerUp && playerNum === 1) { powerUpsRef.current.push({ x: brick.x + brick.width / 2, y: brick.y + brick.height, type: brick.hasPowerUp, active: true }); } setScoreParam(prev => { const ns = Math.min(prev + 10, MAX_SCORE); if (ns >= MAX_SCORE && playerNum === 1) setShowGlitchCrash(true); if (ns > highScore && playerNum === 1) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); } return ns; }); playSound('success'); break; } } // Update power-ups (P1 only) if (playerNum === 1) { const paddleY = canvasSize.height - 30; for (const pu of powerUpsRef.current) { if (!pu.active) continue; pu.y += 2; // Fall speed // Check paddle collision if (pu.y >= paddleY && pu.y <= paddleY + PADDLE_HEIGHT && pu.x >= paddleRefParam.current && pu.x <= paddleRefParam.current + paddleWidth) { pu.active = false; playSound('success'); if (pu.type === 'life') { setLivesParam(prev => Math.min(prev + 1, 5)); } else if (pu.type === 'multi') { // Add extra ball const newBall = { x: ball.x, y: ball.y, dx: -ball.dx, dy: ball.dy }; extraBallsRef.current.push(newBall); } else { // Wide or Slow - timed power-up (8 seconds) setActivePowerUps(prev => [...prev.filter(p => p.type !== pu.type), { type: pu.type, endTime: Date.now() + 8000 }]); } } // Remove if off screen if (pu.y > canvasSize.height) pu.active = false; } powerUpsRef.current = powerUpsRef.current.filter(pu => pu.active); // Update extra balls for (const eb of extraBallsRef.current) { const speedMod = hasSlowBall ? 0.6 : 1; eb.x += eb.dx * speedMod; eb.y += eb.dy * speedMod; if (eb.x <= ballSize / 2 || eb.x >= canvasSize.width - ballSize / 2) eb.dx = -eb.dx; if (eb.y <= ballSize / 2) eb.dy = -eb.dy; if (eb.y + ballSize / 2 >= paddleY && eb.y - ballSize / 2 <= paddleY + PADDLE_HEIGHT && eb.x >= paddleRefParam.current && eb.x <= paddleRefParam.current + paddleWidth) { const hitPos = (eb.x - paddleRefParam.current) / paddleWidth; const angle = (hitPos - 0.5) * Math.PI * 0.7; const speed = Math.sqrt(eb.dx * eb.dx + eb.dy * eb.dy); eb.dx = Math.sin(angle) * speed; eb.dy = -Math.abs(Math.cos(angle) * speed); eb.y = paddleY - ballSize / 2; } } extraBallsRef.current = extraBallsRef.current.filter(eb => eb.y < canvasSize.height); } if (allDestroyed && bricksRefParam.current.length > 0 && !deadRef.current) nextLevel(playerNum); ctx.fillStyle = '#0a0a0a'; ctx.fillRect(0, 0, canvasSize.width, canvasSize.height); for (const brick of bricksRefParam.current) { if (!brick.alive) continue; ctx.fillStyle = brick.color; ctx.fillRect(brick.x, brick.y, brick.width, brick.height); ctx.strokeStyle = primaryColor; ctx.globalAlpha = 0.5; ctx.strokeRect(brick.x, brick.y, brick.width, brick.height); ctx.globalAlpha = 1; // Draw power-up indicator if (brick.hasPowerUp) { ctx.fillStyle = POWER_UP_COLORS[brick.hasPowerUp]; ctx.font = '10px monospace'; ctx.textAlign = 'center'; ctx.fillText(POWER_UP_LABELS[brick.hasPowerUp], brick.x + brick.width / 2, brick.y + brick.height / 2 + 3); } } // Draw falling power-ups if (playerNum === 1) { for (const pu of powerUpsRef.current) { if (!pu.active) continue; ctx.fillStyle = POWER_UP_COLORS[pu.type]; ctx.shadowColor = POWER_UP_COLORS[pu.type]; ctx.shadowBlur = 8; ctx.beginPath(); ctx.arc(pu.x, pu.y, 8, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000'; ctx.font = 'bold 10px monospace'; ctx.textAlign = 'center'; ctx.fillText(POWER_UP_LABELS[pu.type], pu.x, pu.y + 4); ctx.shadowBlur = 0; } } ctx.fillStyle = primaryColor; ctx.shadowColor = primaryColor; ctx.shadowBlur = 10; ctx.fillRect(paddleRefParam.current, canvasSize.height - 30, paddleWidth, PADDLE_HEIGHT); ctx.shadowBlur = 0; if (!deadRef.current) { ctx.beginPath(); ctx.arc(ball.x, ball.y, ballSize / 2, 0, Math.PI * 2); ctx.fillStyle = primaryColor; ctx.shadowColor = primaryColor; ctx.shadowBlur = 15; ctx.fill(); ctx.shadowBlur = 0; } ctx.strokeStyle = primaryColor; ctx.globalAlpha = 0.5; ctx.lineWidth = 2; ctx.strokeRect(0, 0, canvasSize.width, canvasSize.height); ctx.globalAlpha = 1; // Draw player label in 2P mode if (isTwoPlayer) { ctx.font = '14px monospace'; ctx.fillStyle = primaryColor; ctx.textAlign = 'center'; ctx.fillText(`P${playerNum}`, canvasSize.width / 2, 20); if (deadRef.current) { ctx.font = '20px monospace'; ctx.fillStyle = 'hsl(0 70% 50%)'; ctx.fillText('ELIMINATED', canvasSize.width / 2, canvasSize.height / 2); } } }, [canvasSize, paddleWidth, ballSize, highScore, isFullscreen, isMobile, playSound, resetBall, nextLevel, isTwoPlayer, level, level2]); // P1 game loop useEffect(() => { if (!gameStarted || gameOver || isPaused) return; const canvas = canvasRef.current; if (!canvas) return; const computedStyle = getComputedStyle(document.documentElement); const primaryHsl = computedStyle.getPropertyValue('--primary').trim(); const primaryColor = `hsl(${primaryHsl})`; const gameLoop = () => { const leftKey = isTwoPlayer ? 'a' : 'ArrowLeft'; const rightKey = isTwoPlayer ? 'd' : 'ArrowRight'; const altLeftKey = isTwoPlayer ? '' : 'a'; const altRightKey = isTwoPlayer ? '' : 'd'; // Check both keys in single player const checkLeft = leftKey; const checkRight = rightKey; if (!isTwoPlayer) { // In single player, check both arrow and A/D if (keysRef.current.has('ArrowLeft') || keysRef.current.has('a')) { keysRef.current.add('ArrowLeft'); } if (keysRef.current.has('ArrowRight') || keysRef.current.has('d')) { keysRef.current.add('ArrowRight'); } } runGameLoop(canvas, ballRef, paddleXRef, bricksRef, setScore, setLives, () => lives, 1, primaryColor, checkLeft, checkRight, p1DeadRef); animationRef.current = requestAnimationFrame(gameLoop); }; animationRef.current = requestAnimationFrame(gameLoop); return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current); }; }, [gameStarted, gameOver, isPaused, runGameLoop, isTwoPlayer, lives]); // P2 game loop useEffect(() => { if (!gameStarted || gameOver || isPaused || !isTwoPlayer) return; const canvas = canvas2Ref.current; if (!canvas) return; const computedStyle = getComputedStyle(document.documentElement); const secondaryHsl = computedStyle.getPropertyValue('--secondary').trim(); const secondaryColor = `hsl(${secondaryHsl})`; const gameLoop = () => { runGameLoop(canvas, ball2Ref, paddle2XRef, bricks2Ref, setScore2, setLives2, () => lives2, 2, secondaryColor, 'ArrowLeft', 'ArrowRight', p2DeadRef); animation2Ref.current = requestAnimationFrame(gameLoop); }; animation2Ref.current = requestAnimationFrame(gameLoop); return () => { if (animation2Ref.current) cancelAnimationFrame(animation2Ref.current); }; }, [gameStarted, gameOver, isPaused, isTwoPlayer, runGameLoop, lives2]); useEffect(() => { if (gameStarted && !gameOver) { bricksRef.current = initBricks(canvasSize.width); resetBall(ballRef, paddleXRef, level); if (isTwoPlayer) { bricks2Ref.current = initBricks(canvasSize.width); resetBall(ball2Ref, paddle2XRef, level2); } } }, [canvasSize, gameStarted, gameOver, initBricks, resetBall, isTwoPlayer, level, level2]); const getWinner = () => { if (!isTwoPlayer) return null; if (score > score2) return 'P1'; if (score2 > score) return 'P2'; return 'TIE'; }; if (showGlitchCrash) return window.location.reload()} />; const gameUi = ( {/* Render cursor inside portal for fullscreen */} {isFullscreen && }
{'<'} Back

Breakout

{isTwoPlayer && 2P MODE}
{/* Game boards */}
{/* P1 Board */}
{/* P2 Board */} {isTwoPlayer && !isMobile && (
)}
{!isMobile && (
{/* P1 Score Panel */}

{isTwoPlayer ? 'P1 Score' : 'Score'}

{score.toLocaleString()}

{/* P2 Score Panel */} {isTwoPlayer && (

P2 Score

{score2.toLocaleString()}

)} {!isTwoPlayer && (

High Score

{highScore.toLocaleString()}

max: 4,294,967,296

)} {/* Level & Lives */}

{isTwoPlayer ? 'P1 Lvl' : 'Level'}

{level}

{isTwoPlayer ? 'P1 ♥' : 'Lives'}

{'♥'.repeat(Math.max(0, lives))}

{isTwoPlayer && (

P2 Lvl

{level2}

P2 ♥

{'♥'.repeat(Math.max(0, lives2))}

)} {/* Controls */}

Controls

{isTwoPlayer ? ( <>

P1: A / D

P2: ← →

) : (

← → / A D

)}

P: Pause

{/* Action Buttons */} {!gameStarted || gameOver ? (
) : ( )}
)} {isMobile && ( <>

SCORE

{score.toLocaleString()}

HIGH

{highScore.toLocaleString()}

LVL

{level}

{lives}

{gameStarted && !gameOver ? (
) : (

MOBILE: 1 PLAYER ONLY

)}
)}
{!isMobile && (gameOver || isPaused) && gameStarted && (

{gameOver ? 'GAME OVER' : 'PAUSED'}

{gameOver && isTwoPlayer && (

{getWinner() === 'TIE' ? "IT'S A TIE!" : `${getWinner()} WINS!`}

)} {gameOver && (

{isTwoPlayer ? 'P1: ' : 'Score: '}{score.toLocaleString()}

{isTwoPlayer &&

P2: {score2.toLocaleString()}

}
)}
Back {gameOver ? ( <> ) : ( )}
)}
); // Portal to body for true fullscreen, escaping MainLayout transforms return isFullscreen && typeof document !== 'undefined' ? createPortal(gameUi, document.body) : gameUi; }; export default Breakout;