Pacman add invulnerability

Add 3-second invulnerability for respawns in 2P Pac-Man, introduce per-player invulnerability state, timers, and visual blinking cues; fix 2P continue logic to handle post-loss respawns correctly. Also wire invulnerability into collision checks and render to reflect blinking.

X-Lovable-Edit-ID: edt-67c3618b-84a3-4349-8052-99157bfd2d9d
This commit is contained in:
gpt-engineer-app[bot]
2026-01-23 00:00:46 +00:00
+78 -33
View File
@@ -68,6 +68,10 @@ const Pacman = () => {
const [player1Eliminated, setPlayer1Eliminated] = useState(false); const [player1Eliminated, setPlayer1Eliminated] = useState(false);
const [player2Eliminated, setPlayer2Eliminated] = useState(false); const [player2Eliminated, setPlayer2Eliminated] = useState(false);
const [showEliminationPrompt, setShowEliminationPrompt] = useState(false); const [showEliminationPrompt, setShowEliminationPrompt] = useState(false);
const [p1Invulnerable, setP1Invulnerable] = useState(false);
const [p2Invulnerable, setP2Invulnerable] = useState(false);
const invulnerableTimerRef = useRef<NodeJS.Timeout | null>(null);
const invulnerable2TimerRef = useRef<NodeJS.Timeout | null>(null);
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 },
@@ -211,10 +215,38 @@ const Pacman = () => {
setGameMode('1p'); setGameMode('1p');
setShowEliminationPrompt(false); setShowEliminationPrompt(false);
setIsPaused(false); setIsPaused(false);
// If P1 was eliminated, remaining player continues as P1
if (player1Eliminated && !player2Eliminated) {
// P2 becomes P1
setPacman(pacman2);
setDirection(direction2);
setNextDirection(nextDirection2);
setScore(score2);
setLives(lives2);
setPlayer1Eliminated(false);
setPacman2({ x: -1, y: -1 });
}
// If P2 was eliminated, P1 just continues normally
playSound('success'); playSound('success');
}; };
useEffect(() => { return () => { if (powerTimerRef.current) clearTimeout(powerTimerRef.current); }; }, []); const makeP1Invulnerable = () => {
if (invulnerableTimerRef.current) clearTimeout(invulnerableTimerRef.current);
setP1Invulnerable(true);
invulnerableTimerRef.current = setTimeout(() => setP1Invulnerable(false), 3000);
};
const makeP2Invulnerable = () => {
if (invulnerable2TimerRef.current) clearTimeout(invulnerable2TimerRef.current);
setP2Invulnerable(true);
invulnerable2TimerRef.current = setTimeout(() => setP2Invulnerable(false), 3000);
};
useEffect(() => { return () => {
if (powerTimerRef.current) clearTimeout(powerTimerRef.current);
if (invulnerableTimerRef.current) clearTimeout(invulnerableTimerRef.current);
if (invulnerable2TimerRef.current) clearTimeout(invulnerable2TimerRef.current);
}; }, []);
useEffect(() => { useEffect(() => {
if (!gameStarted || gameOver || gameComplete || isPaused) return; if (!gameStarted || gameOver || gameComplete || isPaused) return;
@@ -260,35 +292,45 @@ const Pacman = () => {
}); });
playSound('success'); playSound('success');
} else if (!ghostAtTarget.eaten) { } else if (!ghostAtTarget.eaten) {
// Hit by ghost - lose life without moving // Check invulnerability
const isPlayer1 = player === pacman; const isPlayer1 = setPlayer === setPacman;
const currentLives = isPlayer1 ? lives : lives2; const isInvulnerable = isPlayer1 ? p1Invulnerable : p2Invulnerable;
const setLivesFunc = isPlayer1 ? setLives : setLives2;
const newLives = currentLives - 1; if (!isInvulnerable) {
setLivesFunc(newLives); // Hit by ghost - lose life without moving
const currentLives = isPlayer1 ? lives : lives2;
const setLivesFunc = isPlayer1 ? setLives : setLives2;
if (newLives > 0) { const newLives = currentLives - 1;
// Respawn at start setLivesFunc(newLives);
setPlayer({ x: 10, y: 15 });
playSound('error'); if (newLives > 0) {
// Respawn at start with invulnerability
setPlayer({ x: 10, y: 15 });
if (isPlayer1) makeP1Invulnerable();
else makeP2Invulnerable();
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 { } else {
// Player eliminated // Invulnerable - just move through
if (isPlayer1) { setPlayer(newPos);
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 { } else {
@@ -337,12 +379,13 @@ const Pacman = () => {
return ns; return ns;
}); });
playSound('success'); playSound('success');
} else if (!ghost.eaten) { } else if (!ghost.eaten && !p1Invulnerable) {
// Player 1 dies // Player 1 dies
const newLives = lives - 1; const newLives = lives - 1;
setLives(newLives); setLives(newLives);
if (newLives > 0) { if (newLives > 0) {
setPacman({ x: 10, y: 15 }); setPacman({ x: 10, y: 15 });
makeP1Invulnerable();
playSound('error'); playSound('error');
} else { } else {
setPlayer1Eliminated(true); setPlayer1Eliminated(true);
@@ -402,12 +445,13 @@ const Pacman = () => {
}); });
playSound('success'); playSound('success');
return currentGhosts.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g); return currentGhosts.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g);
} else if (!ghost.eaten) { } else if (!ghost.eaten && !p1Invulnerable) {
// Ghost kills player // Ghost kills player
const newLives = lives - 1; const newLives = lives - 1;
setLives(newLives); setLives(newLives);
if (newLives > 0) { if (newLives > 0) {
setPacman({ x: 10, y: 15 }); setPacman({ x: 10, y: 15 });
makeP1Invulnerable();
playSound('error'); playSound('error');
} else { } else {
setPlayer1Eliminated(true); setPlayer1Eliminated(true);
@@ -443,12 +487,13 @@ const Pacman = () => {
}); });
playSound('success'); playSound('success');
return currentGhosts.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g); return currentGhosts.map((g, idx) => idx === i ? { ...g, eaten: true, pos: { x: 10, y: 9 } } : g);
} else if (!ghost.eaten) { } else if (!ghost.eaten && !p2Invulnerable) {
// Ghost kills player // Ghost kills player
const newLives = lives2 - 1; const newLives = lives2 - 1;
setLives2(newLives); setLives2(newLives);
if (newLives > 0) { if (newLives > 0) {
setPacman2({ x: 10, y: 15 }); setPacman2({ x: 10, y: 15 });
makeP2Invulnerable();
playSound('error'); playSound('error');
} else { } else {
setPlayer2Eliminated(true); setPlayer2Eliminated(true);
@@ -469,7 +514,7 @@ const Pacman = () => {
} }
}, TICK_SPEED); }, TICK_SPEED);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [gameStarted, gameOver, gameComplete, isPaused, pacman, direction, nextDirection, pacman2, direction2, nextDirection2, dots, powerPellets, highScore, isPowered, playSound, gameMode, player1Eliminated, player2Eliminated]); }, [gameStarted, gameOver, gameComplete, isPaused, pacman, direction, nextDirection, pacman2, direction2, nextDirection2, dots, powerPellets, highScore, isPowered, playSound, gameMode, player1Eliminated, player2Eliminated, p1Invulnerable, p2Invulnerable, lives, lives2]);
@@ -604,8 +649,8 @@ const Pacman = () => {
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-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 }}> <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" style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out', filter: 'drop-shadow(0 0 4px hsl(var(--primary)))' }}>{renderPacman()}</div>} {isPacmanHere && <div className={`drop-shadow-lg ${p1Invulnerable ? 'animate-pulse' : ''}`} style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out', filter: 'drop-shadow(0 0 4px hsl(var(--primary)))', opacity: p1Invulnerable ? 0.5 : 1 }}>{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>} {isPacman2Here && <div className={`drop-shadow-lg ${p2Invulnerable ? 'animate-pulse' : ''}`} style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out', filter: 'drop-shadow(0 0 4px hsl(120 70% 50%))', opacity: p2Invulnerable ? 0.5 : 1 }}>{renderPacman2()}</div>}
{ghostIndex !== -1 && !isPacmanHere && !isPacman2Here && <div style={{ width: spriteSize, height: spriteSize, transition: 'all 0.2s ease-out' }}>{renderGhost(ghostIndex, ghosts[ghostIndex].eaten)}</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' }} />} {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" />} {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" />}