Reverted to commit a06d538170

This commit is contained in:
gpt-engineer-app[bot]
2026-01-06 23:56:25 +00:00
parent 93e454a42e
commit fdf6b08c07
9 changed files with 1605 additions and 160 deletions
+2 -7
View File
@@ -62,12 +62,7 @@ const Breakout = () => {
if (height > maxHeight) { height = maxHeight; width = height * aspectRatio; }
return { width: Math.floor(width), height: Math.floor(height) };
}
// Desktop: maximize based on available height
const availableHeight = window.innerHeight - 100;
const aspectRatio = 480 / 580;
const height = Math.min(availableHeight, 700);
const width = Math.floor(height * aspectRatio);
return { width, height };
return isFullscreen ? { width: 600, height: 720 } : { width: 480, height: 580 };
}, [isFullscreen]);
const [canvasSize, setCanvasSize] = useState(getCanvasSize);
@@ -266,7 +261,7 @@ const Breakout = () => {
</button>
</div>
<div className={`flex ${isMobile ? 'flex-col items-center' : 'flex-row gap-4 items-start justify-center'} flex-1`}>
<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" />
</div>
+121 -82
View File
@@ -67,8 +67,6 @@ const Pacman = () => {
const [player1Eliminated, setPlayer1Eliminated] = useState(false);
const [player2Eliminated, setPlayer2Eliminated] = useState(false);
const [showEliminationPrompt, setShowEliminationPrompt] = useState(false);
const [player1Invincible, setPlayer1Invincible] = useState(false);
const [player2Invincible, setPlayer2Invincible] = useState(false);
const [ghosts, setGhosts] = useState<{ pos: Position; dir: Direction; eaten: boolean }[]>([
{ pos: { x: 9, y: 9 }, dir: 'left', eaten: false },
{ pos: { x: 10, y: 9 }, dir: 'up', eaten: false },
@@ -101,11 +99,7 @@ const Pacman = () => {
const maxHeight = window.innerHeight - 420;
return Math.min(Math.floor(maxWidth / GRID_WIDTH), Math.floor(maxHeight / GRID_HEIGHT), 16);
}
// Desktop: calculate based on available height (subtract header ~60px, margins ~40px)
const availableHeight = window.innerHeight - 100;
const maxCellFromHeight = Math.floor(availableHeight / GRID_HEIGHT);
// Clamp to reasonable max
return Math.min(maxCellFromHeight, 28);
return isFullscreen ? 26 : 20;
}, [isFullscreen]);
const [cellSize, setCellSize] = useState(getCellSize);
@@ -194,44 +188,10 @@ const Pacman = () => {
setDots(newDots); setPowerPellets(newPowerPellets); setIsPowered(false); setScore(0); setScore2(0); setLevel(1);
setGameOver(false); setGameComplete(false); setIsPaused(false); setGameStarted(true); setShowModeSelection(false);
setPlayer1Eliminated(false); setPlayer2Eliminated(false); setShowEliminationPrompt(false);
setPlayer1Invincible(false); setPlayer2Invincible(false);
playSound('success'); gameRef.current?.focus();
};
const continueIn1PMode = () => {
// Transfer the surviving player's state to Player 1
if (player1Eliminated && !player2Eliminated) {
// Player 1 died, Player 2 survives - transfer P2 to P1
setPacman(pacman2);
setDirection(direction2);
setNextDirection(nextDirection2);
setMouthOpen(mouthOpen2);
setLives(lives2);
setScore(score2);
setPlayer1Invincible(player2Invincible);
// Reset Player 2
setPacman2({ x: -1, y: -1 });
setDirection2('right');
setNextDirection2('right');
setMouthOpen2(true);
setLives2(3);
setScore2(0);
setPlayer2Invincible(false);
} else if (player2Eliminated && !player1Eliminated) {
// Player 2 died, Player 1 survives - P1 already active, just reset P2
setPacman2({ x: -1, y: -1 });
setDirection2('right');
setNextDirection2('right');
setMouthOpen2(true);
setLives2(3);
setScore2(0);
setPlayer2Invincible(false);
}
// Reset eliminated states and continue
setPlayer1Eliminated(false);
setPlayer2Eliminated(false);
setGameMode('1p');
setShowEliminationPrompt(false);
setIsPaused(false);
@@ -361,16 +321,14 @@ const Pacman = () => {
return ns;
});
playSound('success');
} else if (!ghost.eaten && !player1Invincible) {
// Player 1 dies (only if not invincible)
const newLives = lives - 1;
setLives(newLives);
if (newLives > 0) {
setPacman({ x: 10, y: 15 });
setPlayer1Invincible(true);
setTimeout(() => setPlayer1Invincible(false), 3000); // 3 seconds of invincibility
playSound('error');
} else {
} else if (!ghost.eaten) {
// Player 1 dies
const newLives = lives - 1;
setLives(newLives);
if (newLives > 0) {
setPacman({ x: 10, y: 15 });
playSound('error');
} else {
setPlayer1Eliminated(true);
setPacman({ x: -1, y: -1 });
playSound('error');
@@ -411,54 +369,135 @@ const Pacman = () => {
return { ...ghost, pos: moveEntity(ghost.pos, dir), dir };
}));
// Now check for collisions after all movement is complete
// Check Player 1 collisions (from both player movement and ghost movement)
if (!player1Eliminated) {
setGhosts(currentGhosts => {
for (let i = 0; i < currentGhosts.length; i++) {
const ghost = currentGhosts[i];
if (ghost.pos.x === pacman.x && ghost.pos.y === pacman.y) {
if (isPowered && !ghost.eaten) {
// Player eats ghost
setScore(prev => {
const ns = Math.min(prev + 200, MAX_SCORE);
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); }
return ns;
});
playSound('success');
return currentGhosts.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g);
} else if (!ghost.eaten) {
// Ghost kills player
const newLives = lives - 1;
setLives(newLives);
if (newLives > 0) {
setPacman({ x: 10, y: 15 });
playSound('error');
} else {
setPlayer1Eliminated(true);
setPacman({ x: -1, y: -1 });
playSound('error');
if (gameMode === '2p' && !player2Eliminated) {
setShowEliminationPrompt(true);
setIsPaused(true);
} else {
setGameOver(true);
}
}
}
}
}
return currentGhosts;
});
}
// Check Player 2 collisions
if (gameMode === '2p' && !player2Eliminated) {
setGhosts(currentGhosts => {
for (let i = 0; i < currentGhosts.length; i++) {
const ghost = currentGhosts[i];
if (ghost.pos.x === pacman2.x && ghost.pos.y === pacman2.y) {
if (isPowered && !ghost.eaten) {
// Player eats ghost
setScore2(prev => {
const ns = Math.min(prev + 200, MAX_SCORE);
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); }
return ns;
});
playSound('success');
return currentGhosts.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g);
} else if (!ghost.eaten) {
// Ghost kills player
const newLives = lives2 - 1;
setLives2(newLives);
if (newLives > 0) {
setPacman2({ x: 10, y: 15 });
playSound('error');
} else {
setPlayer2Eliminated(true);
setPacman2({ x: -1, y: -1 });
playSound('error');
if (!player1Eliminated) {
setShowEliminationPrompt(true);
setIsPaused(true);
} else {
setGameOver(true);
}
}
}
}
}
return currentGhosts;
});
}
}, TICK_SPEED);
return () => clearInterval(interval);
}, [gameStarted, gameOver, gameComplete, isPaused, pacman, direction, nextDirection, pacman2, direction2, nextDirection2, dots, powerPellets, highScore, isPowered, playSound, gameMode, player1Eliminated, player2Eliminated]);
// More responsive input - update direction immediately when pressed
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!gameStarted || gameOver || gameComplete || isPaused) return;
// Player 1 controls - WASD always, arrows only in 1P mode
if (e.key === 'w' || e.key === 'W' || e.key === 's' || e.key === 'S' || e.key === 'a' || e.key === 'A' || e.key === 'd' || e.key === 'D' ||
(gameMode === '1p' && (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'ArrowLeft' || e.key === 'ArrowRight'))) {
switch (e.key) {
case 'ArrowUp': case 'w': case 'W':
e.preventDefault();
if (!player1Eliminated) setNextDirection('up');
break;
case 'ArrowDown': case 's': case 'S':
e.preventDefault();
if (!player1Eliminated) setNextDirection('down');
break;
case 'ArrowLeft': case 'a': case 'A':
e.preventDefault();
if (!player1Eliminated) setNextDirection('left');
break;
case 'ArrowRight': case 'd': case 'D':
e.preventDefault();
if (!player1Eliminated) setNextDirection('right');
break;
}
// Player 1 controls (WASD and Arrow keys) - immediate response
switch (e.key) {
case 'ArrowUp': case 'w': case 'W':
e.preventDefault();
if (!player1Eliminated) setNextDirection('up');
break;
case 'ArrowDown': case 's': case 'S':
e.preventDefault();
if (!player1Eliminated) setNextDirection('down');
break;
case 'ArrowLeft': case 'a': case 'A':
e.preventDefault();
if (!player1Eliminated) setNextDirection('left');
break;
case 'ArrowRight': case 'd': case 'D':
e.preventDefault();
if (!player1Eliminated) setNextDirection('right');
break;
}
// Player 2 controls (Arrow keys) - only in 2P mode
// Player 2 controls (IJKL keys) - only in 2P mode
if (gameMode === '2p') {
switch (e.key) {
case 'ArrowUp':
case 'i': case 'I':
e.preventDefault();
if (!player2Eliminated) setNextDirection2('up');
break;
case 'ArrowDown':
case 'k': case 'K':
e.preventDefault();
if (!player2Eliminated) setNextDirection2('down');
break;
case 'ArrowLeft':
case 'j': case 'J':
e.preventDefault();
if (!player2Eliminated) setNextDirection2('left');
break;
case 'ArrowRight':
case 'l': case 'L':
e.preventDefault();
if (!player2Eliminated) setNextDirection2('right');
break;
@@ -531,8 +570,8 @@ const Pacman = () => {
const isPowerPellet = powerPellets.has(`${x},${y}`);
cells.push(
<div key={`${x}-${y}`} className={`flex items-center justify-center transition-all duration-200 ease-out ${isWall ? 'bg-primary/20 border border-primary/40' : isTunnel ? 'bg-background/30' : 'bg-background/50 border border-primary/5'}`} style={{ width: cellSize, height: cellSize }}>
{isPacmanHere && <div className={`drop-shadow-lg ${player1Invincible ? 'animate-pulse' : ''}`} style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out', filter: player1Invincible ? 'drop-shadow(0 0 8px hsl(var(--primary))) brightness(1.5)' : 'drop-shadow(0 0 4px hsl(var(--primary)))', opacity: player1Invincible ? 0.8 : 1 }}>{renderPacman()}</div>}
{isPacman2Here && <div className={`drop-shadow-lg ${player2Invincible ? 'animate-pulse' : ''}`} style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out', filter: player2Invincible ? 'drop-shadow(0 0 8px hsl(120 70% 50%)) brightness(1.5)' : 'drop-shadow(0 0 4px hsl(120 70% 50%))', opacity: player2Invincible ? 0.8 : 1 }}>{renderPacman2()}</div>}
{isPacmanHere && <div className="drop-shadow-lg" style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out', filter: 'drop-shadow(0 0 4px hsl(var(--primary)))' }}>{renderPacman()}</div>}
{isPacman2Here && <div className="drop-shadow-lg" style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out', filter: 'drop-shadow(0 0 4px hsl(120 70% 50%))' }}>{renderPacman2()}</div>}
{ghostIndex !== -1 && !isPacmanHere && !isPacman2Here && <div style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out' }}>{renderGhost(ghostIndex, ghosts[ghostIndex].eaten)}</div>}
{isDot && !isPacmanHere && !isPacman2Here && ghostIndex === -1 && <div className="w-1.5 h-1.5 bg-primary/80 rounded-full transition-all duration-200 ease-out animate-pulse" style={{ animationDuration: '2s' }} />}
{isPowerPellet && !isPacmanHere && !isPacman2Here && ghostIndex === -1 && <div className="w-3 h-3 bg-primary rounded-full animate-pulse box-glow transition-all duration-200 ease-out" />}
@@ -558,7 +597,7 @@ const Pacman = () => {
</button>
</div>
<div className={`flex ${isMobile ? 'flex-col items-center' : 'flex-row gap-4 items-start justify-center'} flex-1`}>
<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 p-1 bg-background/80 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-primary/5 animate-pulse" style={{ animationDuration: '4s' }}></div>
<div className="grid relative z-10" style={{ gridTemplateColumns: `repeat(${GRID_WIDTH}, ${cellSize}px)` }}>{renderGrid()}</div>
@@ -595,8 +634,8 @@ const Pacman = () => {
<p className="font-pixel text-[10px] text-foreground/60 mb-1">CONTROLS</p>
{gameMode === '2p' ? (
<>
<p className="font-pixel text-[8px] text-foreground/80">P1: WASD</p>
<p className="font-pixel text-[8px] text-foreground/80">P2: </p>
<p className="font-pixel text-[8px] text-foreground/80">P1: WASD / </p>
<p className="font-pixel text-[8px] text-foreground/80">P2: I J K L</p>
<p className="font-pixel text-[8px] text-foreground/80">P: Pause</p>
</>
) : (
+5 -6
View File
@@ -75,11 +75,10 @@ const Snake = () => {
const maxHeight = window.innerHeight - 420;
return Math.min(Math.floor(maxWidth / GRID_SIZE), Math.floor(maxHeight / GRID_SIZE), 18);
}
// Desktop: calculate based on available height (subtract header ~60px, margins ~40px)
const availableHeight = window.innerHeight - 100;
const maxCellFromHeight = Math.floor(availableHeight / GRID_SIZE);
// Clamp to reasonable max
return Math.min(maxCellFromHeight, 30);
if (gameMode === '2p') {
return isFullscreen ? 20 : 16;
}
return isFullscreen ? 28 : 24;
}, [isFullscreen, gameMode]);
const [cellSize, setCellSize] = useState(getCellSize);
@@ -539,7 +538,7 @@ const Snake = () => {
</button>
</div>
<div className={`flex ${isMobile ? 'flex-col items-center' : 'flex-row gap-4 items-start justify-center'} flex-1`}>
<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 p-1 bg-background/80">
<div className="grid" style={{ gridTemplateColumns: `repeat(${GRID_SIZE}, ${cellSize}px)` }}>
{gameMode === '1p' ? renderGrid1P() : renderGrid2P()}
+55 -65
View File
@@ -148,17 +148,10 @@ const Tetris = () => {
const maxHeight = window.innerHeight - 440;
return Math.min(Math.floor(maxWidth / BOARD_WIDTH), Math.floor(maxHeight / BOARD_HEIGHT), 22);
}
// Desktop: calculate based on available height (subtract header ~60px, margins ~40px)
const availableHeight = window.innerHeight - 100;
const maxCellFromHeight = Math.floor(availableHeight / BOARD_HEIGHT);
if (gameMode === '2p') {
// 2P needs two boards side by side plus controls (~500px for both boards + gaps)
const availableWidth = window.innerWidth - 300; // space for controls
const maxCellFromWidth = Math.floor(availableWidth / (BOARD_WIDTH * 2 + 4));
return Math.min(maxCellFromHeight, maxCellFromWidth, 28);
return isFullscreen ? 22 : 18;
}
// 1P mode - maximize based on height
return Math.min(maxCellFromHeight, 32);
return isFullscreen ? 30 : 24;
}, [isFullscreen, gameMode]);
const [cellSize, setCellSize] = useState(getCellSize);
@@ -605,7 +598,7 @@ const Tetris = () => {
</button>
</div>
<div className={`flex ${isMobile ? 'flex-col items-center' : 'flex-row gap-3 items-start justify-center'} flex-1`}>
<div className={`flex ${isMobile ? 'flex-col items-center' : 'flex-row gap-4'} flex-1 ${isFullscreen ? 'justify-center items-center' : ''}`}>
{gameMode === '1p' ? (
<>
<div className="border-2 border-primary box-glow p-1 bg-background/80">{renderBoard1P()}</div>
@@ -636,44 +629,42 @@ const Tetris = () => {
)}
</>
) : (
/* 2P Layout: P1 Board with Next | Controls | P2 Board with Next */
<>
{/* P1 Section */}
<div className="flex gap-2 items-start">
<div className="flex flex-col items-center gap-1">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-sm bg-primary" />
<span className="font-pixel text-xs text-primary">P1 (WASD/Q)</span>
</div>
<div className="border-2 border-primary box-glow p-1 bg-background/80">{renderBoard2P(player1, 'border-primary/20')}</div>
<div className="flex gap-2 text-center">
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{player1.score}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{player1.lines}</p></div>
</div>
{player1.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
{/* P1 Board */}
<div className="flex flex-col items-center gap-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-sm bg-primary" />
<span className="font-pixel text-xs text-primary">P1 (WASD/Q)</span>
</div>
{/* P1 Next Piece */}
<div className="border border-primary/50 p-2 bg-background/50 flex flex-col items-center justify-center">
<p className="font-pixel text-[8px] text-foreground/60 mb-1">NEXT</p>
<div className="flex flex-col gap-0.5">
{player1.nextPiece.shape.map((row, y) => (
<div key={y} className="flex gap-0.5">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-2 h-2 ${val ? 'bg-primary box-glow' : 'bg-transparent'}`} />
))}
</div>
))}
</div>
<div className="border-2 border-primary box-glow p-1 bg-background/80">{renderBoard2P(player1, 'border-primary/20')}</div>
<div className="flex gap-2 text-center">
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{player1.score}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-primary">{player1.lines}</p></div>
</div>
{player1.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
</div>
{/* P1 Next Piece */}
<div className="border-2 border-primary/50 p-2 bg-background/50 flex flex-col items-center justify-center min-h-[60px]">
<p className="font-pixel text-[10px] text-foreground/60">P1 NEXT</p>
<div className="flex flex-col gap-1">
{player1.nextPiece.shape.map((row, y) => (
<div key={y} className="flex gap-1">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-2 h-2 ${val ? 'bg-primary box-glow' : 'bg-transparent'}`} />
))}
</div>
))}
</div>
</div>
{/* Center controls */}
<div className="flex flex-col gap-2 min-w-[90px] items-center justify-center">
<div className="flex flex-col gap-2 min-w-[100px] items-center">
<div className="border border-primary/50 p-2 bg-background/50 text-center">
<p className="font-pixel text-[8px] text-foreground/60 mb-1">CONTROLS</p>
<p className="font-pixel text-[7px] text-primary">P1: WASD Q</p>
<p className="font-pixel text-[7px] text-purple-400">P2: /</p>
<p className="font-pixel text-[7px] text-foreground/60 mt-1">P: Pause</p>
<p className="font-pixel text-[8px] text-primary">P1: WASD Q</p>
<p className="font-pixel text-[8px] text-purple-400">P2: /</p>
<p className="font-pixel text-[8px] text-foreground/60 mt-1">P: Pause</p>
</div>
{!gameStarted || gameOver ? (
<button onClick={() => startGame('2p')} className="font-minecraft text-xs py-2 px-3 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
@@ -686,32 +677,31 @@ const Tetris = () => {
)}
</div>
{/* P2 Section */}
<div className="flex gap-2 items-start">
{/* P2 Next Piece */}
<div className="border border-purple-500/50 p-2 bg-background/50 flex flex-col items-center justify-center">
<p className="font-pixel text-[8px] text-purple-400 mb-1">NEXT</p>
<div className="flex flex-col gap-0.5">
{player2.nextPiece.shape.map((row, y) => (
<div key={y} className="flex gap-0.5">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-2 h-2 ${val ? 'bg-purple-400 box-glow' : 'bg-transparent'}`} />
))}
</div>
))}
</div>
{/* P2 Board */}
<div className="flex flex-col items-center gap-2">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: 'hsl(280 70% 50%)' }} />
<span className="font-pixel text-xs text-purple-400">P2 (/)</span>
</div>
<div className="flex flex-col items-center gap-1">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-sm" style={{ backgroundColor: 'hsl(280 70% 50%)' }} />
<span className="font-pixel text-xs text-purple-400">P2 (/)</span>
</div>
<div className="border-2 border-purple-500 p-1 bg-background/80" style={{ boxShadow: '0 0 10px hsl(280 70% 50% / 0.5)' }}>{renderBoard2P(player2, 'border-purple-500/20')}</div>
<div className="flex gap-2 text-center">
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-purple-400">{player2.score}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-purple-400">{player2.lines}</p></div>
</div>
{player2.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
<div className="border-2 border-purple-500 p-1 bg-background/80" style={{ boxShadow: '0 0 10px hsl(280 70% 50% / 0.5)' }}>{renderBoard2P(player2, 'border-purple-500/20')}</div>
<div className="flex gap-2 text-center">
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-purple-400">{player2.score}</p></div>
<div><p className="font-pixel text-[8px] text-foreground/60">LINES</p><p className="font-minecraft text-sm text-purple-400">{player2.lines}</p></div>
</div>
{player2.gameOver && <span className="font-pixel text-xs text-destructive">GAME OVER</span>}
</div>
{/* P2 Next Piece */}
<div className="border-2 border-purple-500/50 p-2 bg-background/50 flex flex-col items-center justify-center min-h-[60px]">
<p className="font-pixel text-[10px] text-purple-400">P2 NEXT</p>
<div className="flex flex-col gap-1">
{player2.nextPiece.shape.map((row, y) => (
<div key={y} className="flex gap-1">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-2 h-2 ${val ? 'bg-purple-400 box-glow' : 'bg-transparent'}`} />
))}
</div>
))}
</div>
</div>
</>
+15
View File
@@ -0,0 +1,15 @@
{/* P2 Next Piece */}
<div className="border-2 border-purple-500/50 p-3 bg-background/50 flex flex-col items-center justify-center min-h-[60px]">
<p className="font-pixel text-[10px] text-purple-400">P2 NEXT</p>
<div className="flex flex-col gap-1">
{player2.nextPiece.shape.map((row, y) => (
<div key={y} className="flex gap-1">
{row.map((val, x) => (
<div key={`${y}-${x}`} className={`w-2 h-2 ${val ? 'bg-purple-400 box-glow' : 'bg-transparent'}`} />
))}
</div>
))}
</div>
</div>
{/* Center controls */}