Files
vim-snake/src/useGameStatus/useGameStatus.ts
T
pastilhas 762bc86878 feat(phase1.2b): implement Nokia-style key buffering system
- Add nextDirectionRef to buffer player input
- Update changeDirection() to queue input instead of immediate execution
- Add applyBufferedDirection() to safely apply buffered input
- Buffer prevents 180° reversals for safe turns
- Call applyBufferedDirection() after each successful move in game loop
- Remove collision grace period - no longer needed with key buffering
- Remove collisionGracePeriodRef and related logic
- Clean up all console.log statements from useGameStatus
- Game now feels responsive at high speeds
- Allows pre-emptive turning on board edges (Nokia Snake style)
- Build and example app work correctly
2025-10-30 22:39:21 +00:00

153 lines
4.6 KiB
TypeScript
Executable File

import { useState, useEffect, useRef } from "react";
import type { GameStatus } from "../types";
import type { UseBoardType } from "../useBoard";
import type { UseCursorType } from "../useCursor";
import type { UseFoodType } from "../useGame/useFood";
const INITIAL_FPS = 5;
const FOODS_PER_LEVEL = 2;
export function useGameStatus(
boardManager: UseBoardType,
snakeManager: UseCursorType,
foodManager: UseFoodType
) {
const { containerRef } = boardManager;
const { initSnake, clearSnake, snakeBodyRef } = snakeManager;
const { spawnFood, clearFood } = foodManager;
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
const [score, setScore] = useState(0);
const [level, setLevel] = useState(1);
const [paused, setPaused] = useState(false);
const levelRef = useRef(1);
const foodsEatenRef = useRef(0);
const gameLoopRef = useRef<number | null>(null);
const lastUpdateRef = useRef<number>(0);
const snakeManagerRef = useRef(snakeManager);
const foodManagerRef = useRef(foodManager);
const currentFpsRef = useRef(INITIAL_FPS);
snakeManagerRef.current = snakeManager;
foodManagerRef.current = foodManager;
const startGame = () => {
initSnake();
spawnFood(snakeBodyRef.current, 3);
setScore(0);
setLevel(1);
levelRef.current = 1;
foodsEatenRef.current = 0;
currentFpsRef.current = INITIAL_FPS;
setGameStatus("started");
};
const stopGame = () => {
if (gameLoopRef.current) {
cancelAnimationFrame(gameLoopRef.current);
gameLoopRef.current = null;
}
clearSnake();
clearFood();
setGameStatus("waiting");
const container = containerRef.current;
container?.querySelectorAll(".snake-head, .snake-body, .snake-tail, .collision").forEach((el: Element) => {
(el as HTMLElement).classList.remove("snake-head", "snake-body", "snake-tail", "collision");
});
};
useEffect(() => {
if (gameStatus === "started") {
if (paused) return;
let shouldGrow = false;
lastUpdateRef.current = performance.now();
const gameLoop = (timestamp: number) => {
const msPerFrame = 1000 / currentFpsRef.current;
const elapsed = timestamp - lastUpdateRef.current;
if (elapsed >= msPerFrame) {
const result = snakeManagerRef.current.moveSnake(shouldGrow);
shouldGrow = false;
if (result === "wall-collision" || result === "self-collision") {
const container = containerRef.current;
container?.querySelectorAll(".snake-head, .snake-body, .snake-tail").forEach((el: Element) => {
(el as HTMLElement).classList.add("collision");
});
setGameStatus("game-over");
return;
}
// Apply any buffered direction input for next frame
snakeManagerRef.current.applyBufferedDirection();
const head = snakeManagerRef.current.snakeBodyRef.current[0];
if (head && foodManagerRef.current.checkFoodCollision(head)) {
foodManagerRef.current.removeFood(head);
shouldGrow = true;
foodsEatenRef.current += 1;
setScore((prev) => {
let newScore = prev + levelRef.current;
if (foodsEatenRef.current >= FOODS_PER_LEVEL) {
newScore--;
const newLevel = levelRef.current + 1;
levelRef.current = newLevel;
setLevel(newLevel);
currentFpsRef.current = INITIAL_FPS + (newLevel - 1);
foodsEatenRef.current = 0;
}
foodManagerRef.current.spawnFood(
snakeManagerRef.current.snakeBodyRef.current,
1
);
return newScore;
});
}
lastUpdateRef.current = timestamp;
}
gameLoopRef.current = requestAnimationFrame(gameLoop);
};
gameLoopRef.current = requestAnimationFrame(gameLoop);
return () => {
if (gameLoopRef.current) {
cancelAnimationFrame(gameLoopRef.current);
gameLoopRef.current = null;
}
};
}
}, [gameStatus, paused]);
useEffect(() => {
if (gameStatus === "game-over") {
if (gameLoopRef.current) {
cancelAnimationFrame(gameLoopRef.current);
gameLoopRef.current = null;
}
}
}, [gameStatus]);
return {
gameStatus,
setGameStatus,
startGame,
stopGame,
quitGame: stopGame,
paused,
togglePause: () => setPaused(curr => !curr),
score,
level,
};
}
export type UseGameStatusType = ReturnType<typeof useGameStatus>;
export type { GameStatus } from "../types";