- 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
153 lines
4.6 KiB
TypeScript
Executable File
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";
|