Changes
This commit is contained in:
+335
-113
@@ -38,14 +38,27 @@ const Breakout = () => {
|
||||
const [gameStarted, setGameStarted] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [showGlitchCrash, setShowGlitchCrash] = useState(false);
|
||||
const [isTwoPlayer, setIsTwoPlayer] = useState(false);
|
||||
|
||||
// P2 state
|
||||
const [score2, setScore2] = useState(0);
|
||||
const [lives2, setLives2] = useState(3);
|
||||
const [level2, setLevel2] = useState(1);
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const canvas2Ref = useRef<HTMLCanvasElement>(null);
|
||||
const gameRef = useRef<HTMLDivElement>(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<Brick[]>([]);
|
||||
const bricks2Ref = useRef<Brick[]>([]);
|
||||
const keysRef = useRef<Set<string>>(new Set());
|
||||
const animationRef = useRef<number>();
|
||||
const animation2Ref = useRef<number>();
|
||||
const p1DeadRef = useRef(false);
|
||||
const p2DeadRef = useRef(false);
|
||||
|
||||
const { enterFullscreen, exitFullscreen } = useBrowserFullscreen();
|
||||
|
||||
@@ -54,7 +67,6 @@ const Breakout = () => {
|
||||
const isMobile = window.innerWidth < 768;
|
||||
if (isMobile) {
|
||||
const maxWidth = window.innerWidth - 32;
|
||||
// Reserve space for header + fixed controls dock on mobile
|
||||
const maxHeight = window.innerHeight - 380;
|
||||
const aspectRatio = 560 / 680;
|
||||
let width = maxWidth;
|
||||
@@ -62,8 +74,11 @@ const Breakout = () => {
|
||||
if (height > maxHeight) { height = maxHeight; width = height * aspectRatio; }
|
||||
return { width: Math.floor(width), height: Math.floor(height) };
|
||||
}
|
||||
if (isTwoPlayer) {
|
||||
return isFullscreen ? { width: 480, height: 580 } : { width: 400, height: 500 };
|
||||
}
|
||||
return isFullscreen ? { width: 700, height: 840 } : { width: 560, height: 680 };
|
||||
}, [isFullscreen]);
|
||||
}, [isFullscreen, isTwoPlayer]);
|
||||
|
||||
const [canvasSize, setCanvasSize] = useState(getCanvasSize);
|
||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
|
||||
@@ -74,10 +89,10 @@ const Breakout = () => {
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [getCanvasSize]);
|
||||
|
||||
useEffect(() => { setCanvasSize(getCanvasSize()); }, [isFullscreen, getCanvasSize]);
|
||||
useEffect(() => { setCanvasSize(getCanvasSize()); }, [isFullscreen, getCanvasSize, isTwoPlayer]);
|
||||
|
||||
const paddleWidth = isMobile ? 70 : (isFullscreen ? 100 : 90);
|
||||
const ballSize = isMobile ? 10 : (isFullscreen ? 14 : 12);
|
||||
const paddleWidth = isMobile ? 70 : (isTwoPlayer ? 70 : (isFullscreen ? 100 : 90));
|
||||
const ballSize = isMobile ? 10 : (isTwoPlayer ? 10 : (isFullscreen ? 14 : 12));
|
||||
|
||||
useEffect(() => {
|
||||
if (gameStarted && isMobile && !isFullscreen) { setIsFullscreen(true); enterFullscreen(); }
|
||||
@@ -100,7 +115,6 @@ const Breakout = () => {
|
||||
if (saved) setHighScore(Math.min(parseInt(saved, 10), MAX_SCORE));
|
||||
}, []);
|
||||
|
||||
// Check score achievements
|
||||
useEffect(() => {
|
||||
if (score > 0) {
|
||||
checkGameScoreAchievements('breakout', score);
|
||||
@@ -108,9 +122,9 @@ const Breakout = () => {
|
||||
}
|
||||
}, [score, checkGameScoreAchievements, unlockMaxScore]);
|
||||
|
||||
const initBricks = useCallback(() => {
|
||||
const initBricks = useCallback((width: number) => {
|
||||
const bricks: Brick[] = [];
|
||||
const brickWidth = (canvasSize.width - (BRICK_COLS + 1) * BRICK_GAP) / BRICK_COLS;
|
||||
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%)'];
|
||||
for (let row = 0; row < BRICK_ROWS; row++) {
|
||||
for (let col = 0; col < BRICK_COLS; col++) {
|
||||
@@ -118,34 +132,73 @@ const Breakout = () => {
|
||||
}
|
||||
}
|
||||
return bricks;
|
||||
}, [canvasSize.width]);
|
||||
}, []);
|
||||
|
||||
const resetBall = useCallback(() => {
|
||||
// Slower base speed (2.5 instead of 4), with gentler level scaling
|
||||
const speed = 2.5 + level * 0.3;
|
||||
ballRef.current = { x: canvasSize.width / 2, y: canvasSize.height - 60, dx: (Math.random() > 0.5 ? 1 : -1) * speed, dy: -speed };
|
||||
paddleXRef.current = canvasSize.width / 2 - paddleWidth / 2;
|
||||
}, [canvasSize.width, canvasSize.height, paddleWidth, level]);
|
||||
const resetBall = useCallback((ballRefToReset: React.MutableRefObject<{x: number, y: number, dx: number, dy: number}>, paddleRefToReset: React.MutableRefObject<number>, lvl: number) => {
|
||||
const speed = 2.5 + lvl * 0.3;
|
||||
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 = () => {
|
||||
const startGame = (twoPlayer: boolean) => {
|
||||
setIsTwoPlayer(twoPlayer);
|
||||
setScore(0); setLevel(1); setLives(3); setGameOver(false); setIsPaused(false); setGameStarted(true);
|
||||
bricksRef.current = initBricks(); resetBall(); playSound('success'); gameRef.current?.focus();
|
||||
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(() => {
|
||||
setLevel(prev => prev + 1); bricksRef.current = initBricks(); resetBall(); playSound('success');
|
||||
}, [initBricks, resetBall, playSound]);
|
||||
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 (['ArrowLeft', 'ArrowRight', 'a', 'd'].includes(e.key)) { e.preventDefault(); keysRef.current.add(e.key); }
|
||||
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]);
|
||||
}, [gameStarted, gameOver, playSound, isTwoPlayer]);
|
||||
|
||||
const handleTouchMove = useCallback((e: React.TouchEvent) => {
|
||||
if (!gameStarted || gameOver || isPaused) return;
|
||||
@@ -160,91 +213,191 @@ const Breakout = () => {
|
||||
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<number>,
|
||||
bricksRefParam: React.MutableRefObject<Brick[]>,
|
||||
setScoreParam: React.Dispatch<React.SetStateAction<number>>,
|
||||
setLivesParam: React.Dispatch<React.SetStateAction<number>>,
|
||||
getLives: () => number,
|
||||
playerNum: 1 | 2,
|
||||
primaryColor: string,
|
||||
leftKey: string,
|
||||
rightKey: string,
|
||||
deadRef: React.MutableRefObject<boolean>
|
||||
) => {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const paddleSpeed = isMobile ? 6 : (isFullscreen ? 10 : 8);
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const primaryHsl = computedStyle.getPropertyValue('--primary').trim();
|
||||
const primaryColor = `hsl(${primaryHsl})`;
|
||||
|
||||
const gameLoop = () => {
|
||||
const paddleSpeed = isMobile ? 6 : (isFullscreen ? 10 : 8);
|
||||
if (keysRef.current.has('ArrowLeft') || keysRef.current.has('a')) paddleXRef.current = Math.max(0, paddleXRef.current - paddleSpeed);
|
||||
if (keysRef.current.has('ArrowRight') || keysRef.current.has('d')) paddleXRef.current = Math.min(canvasSize.width - paddleWidth, paddleXRef.current + paddleSpeed);
|
||||
|
||||
const ball = ballRef.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 >= paddleXRef.current && ball.x <= paddleXRef.current + paddleWidth) {
|
||||
const hitPos = (ball.x - paddleXRef.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) {
|
||||
setLives(prev => {
|
||||
const newLives = prev - 1;
|
||||
if (newLives <= 0) { setGameOver(true); playSound('error'); }
|
||||
else { resetBall(); playSound('error'); }
|
||||
return newLives;
|
||||
});
|
||||
}
|
||||
|
||||
let allDestroyed = true;
|
||||
for (const brick of bricksRef.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;
|
||||
setScore(prev => {
|
||||
const ns = Math.min(prev + 10, MAX_SCORE);
|
||||
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
|
||||
if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); }
|
||||
return ns;
|
||||
});
|
||||
playSound('success'); break;
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
if (allDestroyed && bricksRef.current.length > 0) nextLevel();
|
||||
|
||||
const computedStyle = getComputedStyle(document.documentElement);
|
||||
const primaryHsl = computedStyle.getPropertyValue('--primary').trim();
|
||||
const primaryColor = `hsl(${primaryHsl})`;
|
||||
|
||||
ctx.fillStyle = '#0a0a0a'; ctx.fillRect(0, 0, canvasSize.width, canvasSize.height);
|
||||
|
||||
for (const brick of bricksRef.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;
|
||||
}
|
||||
|
||||
ctx.fillStyle = primaryColor; ctx.shadowColor = primaryColor; ctx.shadowBlur = 10;
|
||||
ctx.fillRect(paddleXRef.current, canvasSize.height - 30, paddleWidth, PADDLE_HEIGHT); ctx.shadowBlur = 0;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
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, canvasSize, paddleWidth, ballSize, highScore, isFullscreen, isMobile, playSound, resetBall, nextLevel]);
|
||||
}, [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(); resetBall(); }
|
||||
}, [canvasSize, gameStarted, gameOver, initBricks, resetBall]);
|
||||
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 <GlitchCrash onComplete={() => window.location.reload()} />;
|
||||
|
||||
@@ -255,6 +408,7 @@ const Breakout = () => {
|
||||
<div className="flex items-center gap-4">
|
||||
<Link to="/games" className="font-pixel text-sm text-foreground/50 hover:text-primary transition-colors">{'<'} Back</Link>
|
||||
<h1 className="font-minecraft text-2xl md:text-3xl text-primary text-glow-strong">Breakout</h1>
|
||||
{isTwoPlayer && <span className="font-pixel text-xs text-secondary">2P MODE</span>}
|
||||
</div>
|
||||
<button onClick={toggleFullscreen} className="p-2 border border-primary/50 hover:bg-primary/20 transition-colors" title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}>
|
||||
{isFullscreen ? <Minimize2 size={16} className="text-primary" /> : <Maximize2 size={16} className="text-primary" />}
|
||||
@@ -262,51 +416,96 @@ const Breakout = () => {
|
||||
</div>
|
||||
|
||||
<div className={`flex ${isMobile ? 'flex-col items-center' : 'flex-row gap-4'} flex-1 ${isFullscreen ? 'justify-center items-center' : ''}`}>
|
||||
<div className="border-2 border-primary box-glow bg-background/80">
|
||||
<canvas ref={canvasRef} width={canvasSize.width} height={canvasSize.height} onTouchMove={handleTouchMove} className="block" />
|
||||
{/* Game boards */}
|
||||
<div className={`flex ${isTwoPlayer ? 'gap-4' : ''}`}>
|
||||
{/* P1 Board */}
|
||||
<div className="border-2 border-primary box-glow bg-background/80">
|
||||
<canvas ref={canvasRef} width={canvasSize.width} height={canvasSize.height} onTouchMove={handleTouchMove} className="block" />
|
||||
</div>
|
||||
|
||||
{/* P2 Board */}
|
||||
{isTwoPlayer && !isMobile && (
|
||||
<div className="border-2 border-secondary bg-background/80" style={{ boxShadow: '0 0 20px hsl(var(--secondary) / 0.3)' }}>
|
||||
<canvas ref={canvas2Ref} width={canvasSize.width} height={canvasSize.height} className="block" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isMobile && (
|
||||
<div className="flex flex-col gap-3 min-w-[160px]">
|
||||
{/* Score Panel */}
|
||||
{/* P1 Score Panel */}
|
||||
<div className="border-2 border-primary/50 p-4 bg-background/60">
|
||||
<p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider">Score</p>
|
||||
<p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider">{isTwoPlayer ? 'P1 Score' : 'Score'}</p>
|
||||
<p className="font-minecraft text-2xl text-primary text-glow-strong">{score.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
{/* P2 Score Panel */}
|
||||
{isTwoPlayer && (
|
||||
<div className="border-2 border-secondary/50 p-4 bg-background/60">
|
||||
<p className="font-pixel text-[10px] text-foreground/50 uppercase tracking-wider">P2 Score</p>
|
||||
<p className="font-minecraft text-2xl text-secondary">{score2.toLocaleString()}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* High Score */}
|
||||
<div className="border border-primary/30 p-3 bg-background/40">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">High Score</p>
|
||||
<p className="font-minecraft text-lg text-primary/80">{highScore.toLocaleString()}</p>
|
||||
<p className="font-pixel text-[8px] text-foreground/30 mt-1">max: 4,294,967,296</p>
|
||||
</div>
|
||||
{!isTwoPlayer && (
|
||||
<div className="border border-primary/30 p-3 bg-background/40">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">High Score</p>
|
||||
<p className="font-minecraft text-lg text-primary/80">{highScore.toLocaleString()}</p>
|
||||
<p className="font-pixel text-[8px] text-foreground/30 mt-1">max: 4,294,967,296</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Level & Lives */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="border border-primary/30 p-3 bg-background/40 text-center">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">Level</p>
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">{isTwoPlayer ? 'P1 Lvl' : 'Level'}</p>
|
||||
<p className="font-minecraft text-xl text-primary">{level}</p>
|
||||
</div>
|
||||
<div className="border border-primary/30 p-3 bg-background/40 text-center">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">Lives</p>
|
||||
<p className="font-minecraft text-lg text-primary text-glow">{'♥'.repeat(lives)}</p>
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">{isTwoPlayer ? 'P1 ♥' : 'Lives'}</p>
|
||||
<p className="font-minecraft text-lg text-primary text-glow">{'♥'.repeat(Math.max(0, lives))}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isTwoPlayer && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="border border-secondary/30 p-3 bg-background/40 text-center">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">P2 Lvl</p>
|
||||
<p className="font-minecraft text-xl text-secondary">{level2}</p>
|
||||
</div>
|
||||
<div className="border border-secondary/30 p-3 bg-background/40 text-center">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider">P2 ♥</p>
|
||||
<p className="font-minecraft text-lg text-secondary">{'♥'.repeat(Math.max(0, lives2))}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Controls */}
|
||||
<div className="border border-primary/20 p-3 bg-background/30">
|
||||
<p className="font-pixel text-[10px] text-foreground/40 uppercase tracking-wider mb-2">Controls</p>
|
||||
<div className="space-y-1">
|
||||
<p className="font-pixel text-[10px] text-foreground/60">← → / A D</p>
|
||||
{isTwoPlayer ? (
|
||||
<>
|
||||
<p className="font-pixel text-[10px] text-primary">P1: A / D</p>
|
||||
<p className="font-pixel text-[10px] text-secondary">P2: ← →</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="font-pixel text-[10px] text-foreground/60">← → / A D</p>
|
||||
)}
|
||||
<p className="font-pixel text-[10px] text-foreground/60">P: Pause</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
{/* Action Buttons */}
|
||||
{!gameStarted || gameOver ? (
|
||||
<button onClick={startGame} className="font-minecraft text-sm py-3 px-4 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||
{gameOver ? 'RETRY' : 'START GAME'}
|
||||
</button>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button onClick={() => startGame(false)} className="font-minecraft text-sm py-3 px-4 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||
{gameOver ? 'RETRY 1P' : '1 PLAYER'}
|
||||
</button>
|
||||
<button onClick={() => startGame(true)} className="font-minecraft text-sm py-3 px-4 border-2 border-secondary bg-secondary/20 text-secondary hover:bg-secondary/40 transition-all">
|
||||
{gameOver ? 'RETRY 2P' : '2 PLAYERS'}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => setIsPaused(p => !p)} className="font-minecraft text-sm py-3 px-4 border-2 border-primary/60 bg-background/50 text-primary hover:bg-primary/20 transition-all">
|
||||
{isPaused ? 'RESUME' : 'PAUSE'}
|
||||
@@ -334,7 +533,10 @@ const Breakout = () => {
|
||||
<GameTouchButton onAction={moveRight} className="p-4 px-8 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg" interval={30}>→</GameTouchButton>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={startGame} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">{gameOver ? 'RETRY' : 'START'}</button>
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<button onClick={() => startGame(false)} className="font-minecraft text-sm py-3 px-8 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">{gameOver ? 'RETRY' : 'START'}</button>
|
||||
<p className="font-pixel text-[8px] text-foreground/40">MOBILE: 1 PLAYER ONLY</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</MobileControlsDock>
|
||||
@@ -346,8 +548,28 @@ const Breakout = () => {
|
||||
<div className="fixed inset-0 bg-background/80 flex items-center justify-center z-50">
|
||||
<div className="border-2 border-primary box-glow-strong p-6 bg-background text-center">
|
||||
<h2 className="font-minecraft text-2xl text-primary text-glow-strong mb-3">{gameOver ? 'GAME OVER' : 'PAUSED'}</h2>
|
||||
{gameOver && (<><p className="font-pixel text-sm text-foreground/80 mb-1">Final Score: {score.toLocaleString()}</p><p className="font-pixel text-xs text-foreground/60 mb-3">Level: {level}</p></>)}
|
||||
<button onClick={gameOver ? startGame : () => setIsPaused(false)} className="font-minecraft text-sm py-2 px-6 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">{gameOver ? 'PLAY AGAIN' : 'RESUME'}</button>
|
||||
{gameOver && isTwoPlayer && (
|
||||
<p className="font-minecraft text-xl mb-2" style={{ color: getWinner() === 'P1' ? 'hsl(var(--primary))' : getWinner() === 'P2' ? 'hsl(var(--secondary))' : 'hsl(var(--foreground))' }}>
|
||||
{getWinner() === 'TIE' ? "IT'S A TIE!" : `${getWinner()} WINS!`}
|
||||
</p>
|
||||
)}
|
||||
{gameOver && (
|
||||
<div className="mb-4">
|
||||
<p className="font-pixel text-sm text-foreground/60">{isTwoPlayer ? 'P1: ' : 'Score: '}{score.toLocaleString()}</p>
|
||||
{isTwoPlayer && <p className="font-pixel text-sm text-foreground/60">P2: {score2.toLocaleString()}</p>}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Link to="/games" className="font-minecraft text-sm py-2 px-4 border border-foreground/50 text-foreground/70 hover:bg-foreground/10 transition-all">Back</Link>
|
||||
{gameOver ? (
|
||||
<>
|
||||
<button onClick={() => startGame(false)} className="font-minecraft text-sm py-2 px-4 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">1P</button>
|
||||
<button onClick={() => startGame(true)} className="font-minecraft text-sm py-2 px-4 border-2 border-secondary bg-secondary/20 text-secondary hover:bg-secondary/40 transition-all">2P</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={() => setIsPaused(false)} className="font-minecraft text-sm py-2 px-4 border-2 border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">Resume</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user