Implement 2P Pacman mode with proper collision detection and ghost AI
- Add 2P mode selection and controls (P1: WASD/Arrows, P2: IJKL) - Implement individual scoring and lives for both players - Fix collision detection for both moving into ghosts and ghosts moving into players - Add player elimination system with continue/restart options - Revert ghost AI to original working logic - Add visual smoothing with CSS transitions and effects - Fix standing still collision bug
This commit is contained in:
+476
-110
@@ -46,20 +46,38 @@ const MAZE_TEMPLATE = [
|
|||||||
const Pacman = () => {
|
const Pacman = () => {
|
||||||
const { playSound } = useSettings();
|
const { playSound } = useSettings();
|
||||||
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
const { checkGameScoreAchievements, unlockMaxScore } = useAchievements();
|
||||||
|
|
||||||
|
// Game mode selection
|
||||||
|
const [gameMode, setGameMode] = useState<'1p' | '2p'>('1p');
|
||||||
|
const [showModeSelection, setShowModeSelection] = useState(true);
|
||||||
|
|
||||||
|
// Player 1 (always active)
|
||||||
const [pacman, setPacman] = useState<Position>({ x: 10, y: 15 });
|
const [pacman, setPacman] = useState<Position>({ x: 10, y: 15 });
|
||||||
const [direction, setDirection] = useState<Direction>('right');
|
const [direction, setDirection] = useState<Direction>('right');
|
||||||
const [nextDirection, setNextDirection] = useState<Direction>('right');
|
const [nextDirection, setNextDirection] = useState<Direction>('right');
|
||||||
const [mouthOpen, setMouthOpen] = useState(true);
|
const [mouthOpen, setMouthOpen] = useState(true);
|
||||||
|
const [lives, setLives] = useState(3);
|
||||||
|
|
||||||
|
// Player 2 (for 2P mode)
|
||||||
|
const [pacman2, setPacman2] = useState<Position>({ x: 10, y: 15 });
|
||||||
|
const [direction2, setDirection2] = useState<Direction>('right');
|
||||||
|
const [nextDirection2, setNextDirection2] = useState<Direction>('right');
|
||||||
|
const [mouthOpen2, setMouthOpen2] = useState(true);
|
||||||
|
const [lives2, setLives2] = useState(3);
|
||||||
|
const [player1Eliminated, setPlayer1Eliminated] = useState(false);
|
||||||
|
const [player2Eliminated, setPlayer2Eliminated] = useState(false);
|
||||||
|
const [showEliminationPrompt, setShowEliminationPrompt] = useState(false);
|
||||||
const [ghosts, setGhosts] = useState<{ pos: Position; dir: Direction; eaten: boolean }[]>([
|
const [ghosts, setGhosts] = useState<{ pos: Position; dir: Direction; eaten: boolean }[]>([
|
||||||
{ pos: { x: 9, y: 9 }, dir: 'left', eaten: false },
|
{ pos: { x: 9, y: 9 }, dir: 'left', eaten: false },
|
||||||
{ pos: { x: 10, y: 9 }, dir: 'up', eaten: false },
|
{ pos: { x: 10, y: 9 }, dir: 'up', eaten: false },
|
||||||
{ pos: { x: 11, y: 9 }, dir: 'right', eaten: false },
|
{ pos: { x: 11, y: 9 }, dir: 'right', eaten: false },
|
||||||
]);
|
]);
|
||||||
const [dots, setDots] = useState<Set<string>>(new Set());
|
const [dots, setDots] = useState<Set<string>>(new Set());
|
||||||
const [powerPellets, setPowerPellets] = useState<Set<string>>(new Set());
|
const [powerPellets, setPowerPellets] = useState<Set<string>>(new Set());
|
||||||
const [isPowered, setIsPowered] = useState(false);
|
const [isPowered, setIsPowered] = useState(false);
|
||||||
const [score, setScore] = useState(0);
|
const [score, setScore] = useState(0);
|
||||||
const [highScore, setHighScore] = useState(0);
|
const [score2, setScore2] = useState(0);
|
||||||
|
const [highScore, setHighScore] = useState(0);
|
||||||
const [gameOver, setGameOver] = useState(false);
|
const [gameOver, setGameOver] = useState(false);
|
||||||
const [gameComplete, setGameComplete] = useState(false);
|
const [gameComplete, setGameComplete] = useState(false);
|
||||||
const [gameStarted, setGameStarted] = useState(false);
|
const [gameStarted, setGameStarted] = useState(false);
|
||||||
@@ -163,46 +181,174 @@ const Pacman = () => {
|
|||||||
const startGame = () => {
|
const startGame = () => {
|
||||||
if (powerTimerRef.current) clearTimeout(powerTimerRef.current);
|
if (powerTimerRef.current) clearTimeout(powerTimerRef.current);
|
||||||
setPacman({ x: 10, y: 15 }); setDirection('right'); setNextDirection('right'); setMouthOpen(true);
|
setPacman({ x: 10, y: 15 }); setDirection('right'); setNextDirection('right'); setMouthOpen(true);
|
||||||
|
setPacman2({ x: 10, y: 15 }); setDirection2('right'); setNextDirection2('right'); setMouthOpen2(true);
|
||||||
|
setLives(3); setLives2(3);
|
||||||
setGhosts([{ pos: { x: 9, y: 9 }, dir: 'left', eaten: false }, { pos: { x: 10, y: 9 }, dir: 'up', eaten: false }, { pos: { x: 11, y: 9 }, dir: 'right', eaten: false }]);
|
setGhosts([{ pos: { x: 9, y: 9 }, dir: 'left', eaten: false }, { pos: { x: 10, y: 9 }, dir: 'up', eaten: false }, { pos: { x: 11, y: 9 }, dir: 'right', eaten: false }]);
|
||||||
const { dots: newDots, powerPellets: newPowerPellets } = initDots();
|
const { dots: newDots, powerPellets: newPowerPellets } = initDots();
|
||||||
setDots(newDots); setPowerPellets(newPowerPellets); setIsPowered(false); setScore(0); setLevel(1);
|
setDots(newDots); setPowerPellets(newPowerPellets); setIsPowered(false); setScore(0); setScore2(0); setLevel(1);
|
||||||
setGameOver(false); setGameComplete(false); setIsPaused(false); setGameStarted(true);
|
setGameOver(false); setGameComplete(false); setIsPaused(false); setGameStarted(true); setShowModeSelection(false);
|
||||||
|
setPlayer1Eliminated(false); setPlayer2Eliminated(false); setShowEliminationPrompt(false);
|
||||||
playSound('success'); gameRef.current?.focus();
|
playSound('success'); gameRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const continueIn1PMode = () => {
|
||||||
|
setGameMode('1p');
|
||||||
|
setShowEliminationPrompt(false);
|
||||||
|
setIsPaused(false);
|
||||||
|
playSound('success');
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => { return () => { if (powerTimerRef.current) clearTimeout(powerTimerRef.current); }; }, []);
|
useEffect(() => { return () => { if (powerTimerRef.current) clearTimeout(powerTimerRef.current); }; }, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gameStarted || gameOver || gameComplete || isPaused) return;
|
if (!gameStarted || gameOver || gameComplete || isPaused) return;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setMouthOpen(prev => !prev);
|
// Handle both players
|
||||||
if (canMove(pacman, nextDirection)) setDirection(nextDirection);
|
const handlePlayerMovement = (
|
||||||
const actualDir = canMove(pacman, nextDirection) ? nextDirection : direction;
|
player: Position, playerDir: Direction, playerNextDir: Direction,
|
||||||
if (canMove(pacman, actualDir)) {
|
setPlayerDir: (dir: Direction) => void, setPlayer: (pos: Position) => void,
|
||||||
const newPos = moveEntity(pacman, actualDir);
|
setPlayerScore: (fn: (prev: number) => number) => void,
|
||||||
setPacman(newPos);
|
setPlayerMouth?: (fn: (prev: boolean) => boolean) => void
|
||||||
const posKey = `${newPos.x},${newPos.y}`;
|
) => {
|
||||||
if (powerPellets.has(posKey)) {
|
// Animate mouth every tick for smoother appearance
|
||||||
setPowerPellets(prev => { const np = new Set(prev); np.delete(posKey); return np; });
|
if (setPlayerMouth) setPlayerMouth(prev => !prev);
|
||||||
activatePowerMode();
|
|
||||||
setScore(prev => {
|
if (canMove(player, playerNextDir)) setPlayerDir(playerNextDir);
|
||||||
const ns = Math.min(prev + 50, MAX_SCORE);
|
const actualDir = canMove(player, playerNextDir) ? playerNextDir : playerDir;
|
||||||
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
|
|
||||||
if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); }
|
if (canMove(player, actualDir)) {
|
||||||
return ns;
|
const newPos = moveEntity(player, actualDir);
|
||||||
});
|
|
||||||
playSound('success');
|
// Check for ghosts at target position BEFORE moving
|
||||||
} else if (dots.has(posKey)) {
|
let ghostAtTarget = null;
|
||||||
setDots(prev => { const nd = new Set(prev); nd.delete(posKey); return nd; });
|
let ghostIndex = -1;
|
||||||
setScore(prev => {
|
for (let i = 0; i < ghosts.length; i++) {
|
||||||
const ns = Math.min(prev + 10, MAX_SCORE);
|
if (ghosts[i].pos.x === newPos.x && ghosts[i].pos.y === newPos.y) {
|
||||||
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
|
ghostAtTarget = ghosts[i];
|
||||||
if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); }
|
ghostIndex = i;
|
||||||
return ns;
|
break;
|
||||||
});
|
}
|
||||||
playSound('hover');
|
}
|
||||||
|
|
||||||
|
if (ghostAtTarget) {
|
||||||
|
// There's a ghost at the target position
|
||||||
|
if (isPowered && !ghostAtTarget.eaten) {
|
||||||
|
// Eat the ghost - move to position and score
|
||||||
|
setPlayer(newPos);
|
||||||
|
setGhosts(prev => prev.map((g, idx) => idx === ghostIndex ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g));
|
||||||
|
setPlayerScore(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');
|
||||||
|
} else if (!ghostAtTarget.eaten) {
|
||||||
|
// Hit by ghost - lose life without moving
|
||||||
|
const isPlayer1 = player === pacman;
|
||||||
|
const currentLives = isPlayer1 ? lives : lives2;
|
||||||
|
const setLivesFunc = isPlayer1 ? setLives : setLives2;
|
||||||
|
|
||||||
|
const newLives = currentLives - 1;
|
||||||
|
setLivesFunc(newLives);
|
||||||
|
|
||||||
|
if (newLives > 0) {
|
||||||
|
// Respawn at start
|
||||||
|
setPlayer({ x: 10, y: 15 });
|
||||||
|
playSound('error');
|
||||||
|
} else {
|
||||||
|
// Player eliminated
|
||||||
|
if (isPlayer1) {
|
||||||
|
setPlayer1Eliminated(true);
|
||||||
|
setPacman({ x: -1, y: -1 });
|
||||||
|
} else {
|
||||||
|
setPlayer2Eliminated(true);
|
||||||
|
setPacman2({ x: -1, y: -1 });
|
||||||
|
}
|
||||||
|
playSound('error');
|
||||||
|
|
||||||
|
if (gameMode === '2p' && ((isPlayer1 && !player2Eliminated) || (!isPlayer1 && !player1Eliminated))) {
|
||||||
|
setShowEliminationPrompt(true);
|
||||||
|
setIsPaused(true);
|
||||||
|
} else {
|
||||||
|
setGameOver(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No ghost at target, safe to move
|
||||||
|
setPlayer(newPos);
|
||||||
|
|
||||||
|
const posKey = `${newPos.x},${newPos.y}`;
|
||||||
|
if (powerPellets.has(posKey)) {
|
||||||
|
setPowerPellets(prev => { const np = new Set(prev); np.delete(posKey); return np; });
|
||||||
|
activatePowerMode();
|
||||||
|
setPlayerScore(prev => {
|
||||||
|
const ns = Math.min(prev + 50, MAX_SCORE);
|
||||||
|
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
|
||||||
|
if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); }
|
||||||
|
return ns;
|
||||||
|
});
|
||||||
|
playSound('success');
|
||||||
|
} else if (dots.has(posKey)) {
|
||||||
|
setDots(prev => { const nd = new Set(prev); nd.delete(posKey); return nd; });
|
||||||
|
setPlayerScore(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('hover');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Player 1 (only if not eliminated)
|
||||||
|
if (!player1Eliminated) {
|
||||||
|
handlePlayerMovement(pacman, direction, nextDirection, setDirection, setPacman, setScore, setMouthOpen);
|
||||||
|
|
||||||
|
// Check collisions immediately after movement
|
||||||
|
for (let i = 0; i < ghosts.length; i++) {
|
||||||
|
const ghost = ghosts[i];
|
||||||
|
if (ghost.pos.x === pacman.x && ghost.pos.y === pacman.y) {
|
||||||
|
if (isPowered && !ghost.eaten) {
|
||||||
|
setGhosts(prev => prev.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g));
|
||||||
|
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');
|
||||||
|
} 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');
|
||||||
|
if (gameMode === '2p' && !player2Eliminated) {
|
||||||
|
setShowEliminationPrompt(true);
|
||||||
|
setIsPaused(true);
|
||||||
|
} else {
|
||||||
|
setGameOver(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Player 2 (only in 2P mode and not eliminated)
|
||||||
|
if (gameMode === '2p' && !player2Eliminated) {
|
||||||
|
handlePlayerMovement(pacman2, direction2, nextDirection2, setDirection2, setPacman2, setScore2, setMouthOpen2);
|
||||||
|
}
|
||||||
|
|
||||||
setGhosts(prev => prev.map(ghost => {
|
setGhosts(prev => prev.map(ghost => {
|
||||||
if (ghost.eaten) return ghost;
|
if (ghost.eaten) return ghost;
|
||||||
const directions: Direction[] = ['up', 'down', 'left', 'right'];
|
const directions: Direction[] = ['up', 'down', 'left', 'right'];
|
||||||
@@ -222,63 +368,178 @@ const Pacman = () => {
|
|||||||
const dir = validDirs[Math.floor(Math.random() * validDirs.length)];
|
const dir = validDirs[Math.floor(Math.random() * validDirs.length)];
|
||||||
return { ...ghost, pos: moveEntity(ghost.pos, dir), dir };
|
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);
|
}, TICK_SPEED);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [gameStarted, gameOver, gameComplete, isPaused, pacman, direction, nextDirection, dots, powerPellets, highScore, isPowered, playSound]);
|
}, [gameStarted, gameOver, gameComplete, isPaused, pacman, direction, nextDirection, pacman2, direction2, nextDirection2, dots, powerPellets, highScore, isPowered, playSound, gameMode, player1Eliminated, player2Eliminated]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!gameStarted || gameOver || gameComplete) return;
|
|
||||||
for (let i = 0; i < ghosts.length; i++) {
|
|
||||||
const ghost = ghosts[i];
|
|
||||||
if (ghost.pos.x === pacman.x && ghost.pos.y === pacman.y) {
|
|
||||||
if (isPowered && !ghost.eaten) {
|
|
||||||
setGhosts(prev => prev.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g));
|
|
||||||
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');
|
|
||||||
} else if (!ghost.eaten) { setGameOver(true); playSound('error'); return; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dots.size === 0 && powerPellets.size === 0) {
|
|
||||||
setScore(prev => {
|
|
||||||
const ns = Math.min(prev + 500, MAX_SCORE);
|
|
||||||
if (ns >= MAX_SCORE) setShowGlitchCrash(true);
|
|
||||||
if (ns > highScore) { setHighScore(ns); localStorage.setItem(HIGHSCORE_KEY, ns.toString()); }
|
|
||||||
return ns;
|
|
||||||
});
|
|
||||||
setLevel(prev => prev + 1);
|
|
||||||
const { dots: newDots, powerPellets: newPowerPellets } = initDots();
|
|
||||||
setDots(newDots); setPowerPellets(newPowerPellets);
|
|
||||||
playSound('success');
|
|
||||||
}
|
|
||||||
}, [pacman, ghosts, dots, powerPellets, gameStarted, gameOver, gameComplete, highScore, isPowered, playSound, initDots]);
|
|
||||||
|
|
||||||
|
|
||||||
|
// More responsive input - update direction immediately when pressed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (!gameStarted) return;
|
if (!gameStarted || gameOver || gameComplete || isPaused) return;
|
||||||
|
|
||||||
|
// Player 1 controls (WASD and Arrow keys) - immediate response
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowUp': case 'w': e.preventDefault(); setNextDirection('up'); break;
|
case 'ArrowUp': case 'w': case 'W':
|
||||||
case 'ArrowDown': case 's': e.preventDefault(); setNextDirection('down'); break;
|
e.preventDefault();
|
||||||
case 'ArrowLeft': case 'a': e.preventDefault(); setNextDirection('left'); break;
|
if (!player1Eliminated) setNextDirection('up');
|
||||||
case 'ArrowRight': case 'd': e.preventDefault(); setNextDirection('right'); break;
|
break;
|
||||||
case 'p': e.preventDefault(); if (!gameOver && !gameComplete) setIsPaused(prev => !prev); 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 (IJKL keys) - only in 2P mode
|
||||||
|
if (gameMode === '2p') {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'i': case 'I':
|
||||||
|
e.preventDefault();
|
||||||
|
if (!player2Eliminated) setNextDirection2('up');
|
||||||
|
break;
|
||||||
|
case 'k': case 'K':
|
||||||
|
e.preventDefault();
|
||||||
|
if (!player2Eliminated) setNextDirection2('down');
|
||||||
|
break;
|
||||||
|
case 'j': case 'J':
|
||||||
|
e.preventDefault();
|
||||||
|
if (!player2Eliminated) setNextDirection2('left');
|
||||||
|
break;
|
||||||
|
case 'l': case 'L':
|
||||||
|
e.preventDefault();
|
||||||
|
if (!player2Eliminated) setNextDirection2('right');
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (!gameStarted || gameOver || gameComplete || isPaused) return;
|
||||||
|
|
||||||
|
// Pause (works for both modes)
|
||||||
|
if (e.key === 'p' || e.key === 'P') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!gameOver && !gameComplete) setIsPaused(prev => !prev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
window.addEventListener('keyup', handleKeyUp);
|
||||||
}, [gameStarted, gameOver, gameComplete]);
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
|
};
|
||||||
|
}, [gameStarted, gameOver, gameComplete, isPaused, gameMode, player1Eliminated, player2Eliminated]);
|
||||||
|
|
||||||
const getPacmanRotation = () => { switch (direction) { case 'right': return 0; case 'down': return 90; case 'left': return 180; case 'up': return 270; } };
|
const getPacmanRotation = () => { switch (direction) { case 'right': return 0; case 'down': return 90; case 'left': return 180; case 'up': return 270; } };
|
||||||
|
|
||||||
|
const getPacmanRotation2 = () => { switch (direction2) { case 'right': return 0; case 'down': return 90; case 'left': return 180; case 'up': return 270; } };
|
||||||
|
|
||||||
const renderPacman = () => (
|
const renderPacman = () => (
|
||||||
<svg viewBox="0 0 100 100" className="w-full h-full" style={{ transform: `rotate(${getPacmanRotation()}deg)` }}>
|
<svg viewBox="0 0 100 100" className="w-full h-full transition-transform duration-100 ease-out" style={{ transform: `rotate(${getPacmanRotation()}deg)` }}>
|
||||||
<circle cx="50" cy="50" r="45" fill="hsl(var(--primary))" />
|
<circle cx="50" cy="50" r="45" fill="hsl(var(--primary))" className="transition-all duration-100 ease-out" />
|
||||||
{mouthOpen && <path d="M 50 50 L 95 25 L 95 75 Z" fill="hsl(var(--background))" />}
|
{mouthOpen && <path d="M 50 50 L 95 25 L 95 75 Z" fill="hsl(var(--background))" className="transition-all duration-100 ease-out" />}
|
||||||
<circle cx="50" cy="25" r="6" fill="hsl(var(--background))" />
|
<circle cx="50" cy="25" r="6" fill="hsl(var(--background))" className="transition-all duration-100 ease-out" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPacman2 = () => (
|
||||||
|
<svg viewBox="0 0 100 100" className="w-full h-full transition-transform duration-100 ease-out" style={{ transform: `rotate(${getPacmanRotation2()}deg)` }}>
|
||||||
|
<circle cx="50" cy="50" r="45" fill="hsl(120 70% 50%)" className="transition-all duration-100 ease-out" />
|
||||||
|
{mouthOpen2 && <path d="M 50 50 L 95 25 L 95 75 Z" fill="hsl(var(--background))" className="transition-all duration-100 ease-out" />}
|
||||||
|
<circle cx="50" cy="25" r="6" fill="hsl(var(--background))" className="transition-all duration-100 ease-out" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -303,15 +564,17 @@ const Pacman = () => {
|
|||||||
const isWall = MAZE_TEMPLATE[y][x] === 1;
|
const isWall = MAZE_TEMPLATE[y][x] === 1;
|
||||||
const isTunnel = MAZE_TEMPLATE[y][x] === 2;
|
const isTunnel = MAZE_TEMPLATE[y][x] === 2;
|
||||||
const isPacmanHere = pacman.x === x && pacman.y === y;
|
const isPacmanHere = pacman.x === x && pacman.y === y;
|
||||||
|
const isPacman2Here = gameMode === '2p' && pacman2.x === x && pacman2.y === y;
|
||||||
const ghostIndex = ghosts.findIndex(g => g.pos.x === x && g.pos.y === y && !g.eaten);
|
const ghostIndex = ghosts.findIndex(g => g.pos.x === x && g.pos.y === y && !g.eaten);
|
||||||
const isDot = dots.has(`${x},${y}`);
|
const isDot = dots.has(`${x},${y}`);
|
||||||
const isPowerPellet = powerPellets.has(`${x},${y}`);
|
const isPowerPellet = powerPellets.has(`${x},${y}`);
|
||||||
cells.push(
|
cells.push(
|
||||||
<div key={`${x}-${y}`} className={`flex items-center justify-center transition-colors duration-100 ${isWall ? 'bg-primary/20 border border-primary/40' : isTunnel ? 'bg-background/30' : 'bg-background/50 border border-primary/5'}`} style={{ width: cellSize, height: cellSize }}>
|
<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 style={{ width: spriteSize, height: spriteSize }}>{renderPacman()}</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>}
|
||||||
{ghostIndex !== -1 && !isPacmanHere && <div style={{ width: spriteSize, height: spriteSize }}>{renderGhost(ghostIndex, ghosts[ghostIndex].eaten)}</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>}
|
||||||
{isDot && !isPacmanHere && ghostIndex === -1 && <div className="w-1.5 h-1.5 bg-primary/80 rounded-full" />}
|
{ghostIndex !== -1 && !isPacmanHere && !isPacman2Here && <div style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out' }}>{renderGhost(ghostIndex, ghosts[ghostIndex].eaten)}</div>}
|
||||||
{isPowerPellet && !isPacmanHere && ghostIndex === -1 && <div className="w-3 h-3 bg-primary rounded-full animate-pulse box-glow" />}
|
{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" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -335,17 +598,60 @@ const Pacman = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`flex ${isMobile ? 'flex-col items-center' : 'flex-row gap-4'} flex-1 ${isFullscreen ? 'justify-center items-center' : ''}`}>
|
<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="border-2 border-primary box-glow p-1 bg-background/80 relative overflow-hidden">
|
||||||
<div className="grid" style={{ gridTemplateColumns: `repeat(${GRID_WIDTH}, ${cellSize}px)` }}>{renderGrid()}</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div className="flex flex-col gap-2 min-w-[140px]">
|
<div className="flex flex-col gap-2 min-w-[140px]">
|
||||||
<div className="border border-primary/50 p-3 bg-background/50"><p className="font-pixel text-[10px] text-foreground/60">SCORE</p><p className="font-minecraft text-xl text-primary text-glow">{score.toLocaleString()}</p></div>
|
{gameMode === '2p' ? (
|
||||||
|
<div className="border border-primary/50 p-3 bg-background/50">
|
||||||
|
<p className="font-pixel text-[10px] text-foreground/60 mb-1">SCORES</p>
|
||||||
|
<div className="flex gap-2 justify-center">
|
||||||
|
<div className="text-center"><p className="font-pixel text-[8px] text-foreground/80">P1</p><p className="font-minecraft text-lg text-primary text-glow">{score.toLocaleString()}</p></div>
|
||||||
|
<div className="text-center"><p className="font-pixel text-[8px] text-foreground/80">P2</p><p className="font-minecraft text-lg text-primary text-glow">{score2.toLocaleString()}</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="border border-primary/50 p-3 bg-background/50"><p className="font-pixel text-[10px] text-foreground/60">SCORE</p><p className="font-minecraft text-xl text-primary text-glow">{score.toLocaleString()}</p></div>
|
||||||
|
)}
|
||||||
<div className="border border-primary/50 p-3 bg-background/50"><p className="font-pixel text-[10px] text-foreground/60">HIGH SCORE</p><p className="font-minecraft text-lg text-primary text-glow">{highScore.toLocaleString()}</p><p className="font-pixel text-[8px] text-foreground/30">max: 4,294,967,296</p></div>
|
<div className="border border-primary/50 p-3 bg-background/50"><p className="font-pixel text-[10px] text-foreground/60">HIGH SCORE</p><p className="font-minecraft text-lg text-primary text-glow">{highScore.toLocaleString()}</p><p className="font-pixel text-[8px] text-foreground/30">max: 4,294,967,296</p></div>
|
||||||
<div className="border border-primary/50 p-3 bg-background/50"><p className="font-pixel text-[10px] text-foreground/60">LEVEL</p><p className="font-minecraft text-lg text-primary text-glow">{level}</p></div>
|
<div className="border border-primary/50 p-3 bg-background/50"><p className="font-pixel text-[10px] text-foreground/60">LEVEL</p><p className="font-minecraft text-lg text-primary text-glow">{level}</p></div>
|
||||||
<div className="border border-primary/50 p-3 bg-background/50"><p className="font-pixel text-[10px] text-foreground/60 mb-1">CONTROLS</p><p className="font-pixel text-[10px] text-foreground/80">← → ↑ ↓ / WASD</p><p className="font-pixel text-[10px] text-foreground/80">P: Pause</p></div>
|
{gameMode === '2p' && (
|
||||||
{!gameStarted || gameOver || gameComplete ? (
|
<div className="border border-primary/50 p-3 bg-background/50">
|
||||||
|
<p className="font-pixel text-[10px] text-foreground/60 mb-1">LIVES</p>
|
||||||
|
<div className="flex gap-2 justify-center">
|
||||||
|
<div className="text-center"><p className="font-pixel text-[8px] text-foreground/80">P1</p><p className="font-minecraft text-sm text-primary">{lives}</p></div>
|
||||||
|
<div className="text-center"><p className="font-pixel text-[8px] text-foreground/80">P2</p><p className="font-minecraft text-sm text-primary">{lives2}</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{gameMode === '1p' && (
|
||||||
|
<div className="border border-primary/50 p-3 bg-background/50"><p className="font-pixel text-[10px] text-foreground/60">LIVES</p><p className="font-minecraft text-lg text-primary text-glow">{lives}</p></div>
|
||||||
|
)}
|
||||||
|
<div className="border border-primary/50 p-3 bg-background/50">
|
||||||
|
<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: I J K L</p>
|
||||||
|
<p className="font-pixel text-[8px] text-foreground/80">P: Pause</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="font-pixel text-[10px] text-foreground/80">WASD / ←→↑↓</p>
|
||||||
|
<p className="font-pixel text-[10px] text-foreground/80">P: Pause</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showModeSelection ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="font-pixel text-[10px] text-foreground/60 text-center">SELECT MODE</p>
|
||||||
|
<button onClick={() => { setGameMode('1p'); setShowModeSelection(false); }} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">1 PLAYER</button>
|
||||||
|
<button onClick={() => { setGameMode('2p'); setShowModeSelection(false); }} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">2 PLAYERS</button>
|
||||||
|
</div>
|
||||||
|
) : !gameStarted || gameOver || gameComplete ? (
|
||||||
<button onClick={startGame} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}</button>
|
<button onClick={startGame} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}</button>
|
||||||
) : (
|
) : (
|
||||||
<button onClick={() => setIsPaused(p => !p)} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all">{isPaused ? 'RESUME' : 'PAUSE'}</button>
|
<button onClick={() => setIsPaused(p => !p)} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all">{isPaused ? 'RESUME' : 'PAUSE'}</button>
|
||||||
@@ -358,40 +664,100 @@ const Pacman = () => {
|
|||||||
<MobileControlsSpacer />
|
<MobileControlsSpacer />
|
||||||
<MobileControlsDock>
|
<MobileControlsDock>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<div className="flex gap-4 text-center">
|
<div className="flex gap-4 text-center">
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
{gameMode === '2p' ? (
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
<>
|
||||||
<div><p className="font-pixel text-[8px] text-foreground/60">LVL</p><p className="font-minecraft text-sm text-primary">{level}</p></div>
|
<div><p className="font-pixel text-[8px] text-foreground/60">P1</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
||||||
</div>
|
<div><p className="font-pixel text-[8px] text-foreground/60">P2</p><p className="font-minecraft text-sm text-primary">{score2.toLocaleString()}</p></div>
|
||||||
|
<div><p className="font-pixel text-[8px] text-foreground/60">LVL</p><p className="font-minecraft text-sm text-primary">{level}</p></div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div><p className="font-pixel text-[8px] text-foreground/60">SCORE</p><p className="font-minecraft text-sm text-primary">{score.toLocaleString()}</p></div>
|
||||||
|
<div><p className="font-pixel text-[8px] text-foreground/60">HIGH</p><p className="font-minecraft text-sm text-primary">{highScore.toLocaleString()}</p></div>
|
||||||
|
<div><p className="font-pixel text-[8px] text-foreground/60">LVL</p><p className="font-minecraft text-sm text-primary">{level}</p></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{gameMode === '2p' && (
|
||||||
|
<>
|
||||||
|
<div><p className="font-pixel text-[8px] text-foreground/60">P1</p><p className="font-minecraft text-sm text-primary">{lives}</p></div>
|
||||||
|
<div><p className="font-pixel text-[8px] text-foreground/60">P2</p><p className="font-minecraft text-sm text-primary">{lives2}</p></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{gameMode === '1p' && (
|
||||||
|
<div><p className="font-pixel text-[8px] text-foreground/60">LIVES</p><p className="font-minecraft text-sm text-primary">{lives}</p></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{gameStarted && !gameOver && !gameComplete ? (
|
{showModeSelection ? (
|
||||||
<div className="grid grid-cols-3 gap-1 mt-2">
|
<div className="flex flex-col gap-2 mt-2">
|
||||||
<div />
|
<p className="font-pixel text-[10px] text-foreground/60 text-center">SELECT MODE</p>
|
||||||
<GameTouchButton onAction={() => setNextDirection('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑</GameTouchButton>
|
<button onClick={() => { setGameMode('1p'); setShowModeSelection(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">1 PLAYER</button>
|
||||||
<div />
|
<button onClick={() => { setGameMode('2p'); setShowModeSelection(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">2 PLAYERS</button>
|
||||||
<GameTouchButton onAction={() => setNextDirection('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
</div>
|
||||||
<button onClick={() => setIsPaused(p => !p)} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
) : gameStarted && !gameOver && !gameComplete ? (
|
||||||
<GameTouchButton onAction={() => setNextDirection('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→</GameTouchButton>
|
<div className="grid grid-cols-3 gap-1 mt-2">
|
||||||
<div />
|
<div />
|
||||||
<GameTouchButton onAction={() => setNextDirection('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓</GameTouchButton>
|
<GameTouchButton onAction={() => setNextDirection('up')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↑</GameTouchButton>
|
||||||
<div />
|
<div />
|
||||||
</div>
|
<GameTouchButton onAction={() => setNextDirection('left')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">←</GameTouchButton>
|
||||||
) : (
|
<button onClick={() => setIsPaused(p => !p)} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-[10px] select-none">{isPaused ? '▶' : '❚❚'}</button>
|
||||||
<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">
|
<GameTouchButton onAction={() => setNextDirection('right')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">→</GameTouchButton>
|
||||||
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}
|
<div />
|
||||||
</button>
|
<GameTouchButton onAction={() => setNextDirection('down')} className="p-4 border border-primary/50 active:bg-primary/40 text-primary font-pixel text-lg">↓</GameTouchButton>
|
||||||
)}
|
<div />
|
||||||
|
</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">
|
||||||
|
{gameComplete ? 'PERFECT!' : gameOver ? 'RETRY' : 'START'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</MobileControlsDock>
|
</MobileControlsDock>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && (
|
{!isMobile && showEliminationPrompt && gameStarted && (
|
||||||
|
<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">{player1Eliminated ? 'PLAYER 2 WON!' : 'PLAYER 1 WON!'}</h2>
|
||||||
|
<p className="font-pixel text-sm text-foreground/80 mb-3">
|
||||||
|
{player1Eliminated ? 'Player 1' : 'Player 2'} has run out of lives.<br/>
|
||||||
|
Continue with the remaining player?
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-4 justify-center">
|
||||||
|
<button onClick={continueIn1PMode} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||||
|
CONTINUE (1P)
|
||||||
|
</button>
|
||||||
|
<button onClick={startGame} className="font-minecraft text-sm py-2 px-4 border border-primary bg-primary/20 text-primary hover:bg-primary/40 transition-all box-glow">
|
||||||
|
RESTART
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isMobile && (gameOver || isPaused || gameComplete) && gameStarted && !showEliminationPrompt && (
|
||||||
<div className="fixed inset-0 bg-background/80 flex items-center justify-center z-50">
|
<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">
|
<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">{gameComplete ? 'GAME COMPLETE!' : gameOver ? 'GAME OVER' : 'PAUSED'}</h2>
|
<h2 className="font-minecraft text-2xl text-primary text-glow-strong mb-3">{gameComplete ? 'GAME COMPLETE!' : gameOver ? 'GAME OVER' : 'PAUSED'}</h2>
|
||||||
{(gameOver || gameComplete) && (<><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></>)}
|
{(gameOver || gameComplete) && (
|
||||||
|
<>
|
||||||
|
{gameMode === '2p' ? (
|
||||||
|
<>
|
||||||
|
<p className="font-pixel text-sm text-foreground/80 mb-1">Final Scores</p>
|
||||||
|
<div className="flex gap-4 justify-center mb-1">
|
||||||
|
<div className="text-center"><p className="font-pixel text-xs text-foreground/80">P1</p><p className="font-minecraft text-lg text-primary">{score.toLocaleString()}</p></div>
|
||||||
|
<div className="text-center"><p className="font-pixel text-xs text-foreground/80">P2</p><p className="font-minecraft text-lg text-primary">{score2.toLocaleString()}</p></div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<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 || gameComplete) ? 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 || gameComplete) ? 'PLAY AGAIN' : 'RESUME'}</button>
|
<button onClick={(gameOver || gameComplete) ? 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 || gameComplete) ? 'PLAY AGAIN' : 'RESUME'}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user