feat: implement startingLevel option and remove INITIAL_FPS constant
- Remove INITIAL_FPS constant entirely - no longer exists - Add startingLevel to GameOptions (defaults to 1) - FPS now equals currentLevel (1:1 ratio) - Points per food always equals currentLevel - Game can start at any level with appropriate speed/scoring
This commit is contained in:
@@ -32,6 +32,7 @@ export type GameOptions = {
|
|||||||
cols: number;
|
cols: number;
|
||||||
rows: number;
|
rows: number;
|
||||||
timeLimit?: number;
|
timeLimit?: number;
|
||||||
|
startingLevel?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useScore } from "../useScore";
|
|||||||
export function useGame(optionsOrCols: GameOptions | number, rows?: number, platformHook?: unknown): GameManager {
|
export function useGame(optionsOrCols: GameOptions | number, rows?: number, platformHook?: unknown): GameManager {
|
||||||
// Support both old signature (cols, rows) and new signature (options)
|
// Support both old signature (cols, rows) and new signature (options)
|
||||||
let cols: number;
|
let cols: number;
|
||||||
|
let startingLevel: number | undefined;
|
||||||
let actualPlatformHook = platformHook;
|
let actualPlatformHook = platformHook;
|
||||||
|
|
||||||
if (typeof optionsOrCols === 'object') {
|
if (typeof optionsOrCols === 'object') {
|
||||||
@@ -17,6 +18,7 @@ export function useGame(optionsOrCols: GameOptions | number, rows?: number, plat
|
|||||||
const options = optionsOrCols as GameOptions;
|
const options = optionsOrCols as GameOptions;
|
||||||
cols = options.cols;
|
cols = options.cols;
|
||||||
rows = options.rows;
|
rows = options.rows;
|
||||||
|
startingLevel = options.startingLevel;
|
||||||
actualPlatformHook = rows as unknown as typeof platformHook;
|
actualPlatformHook = rows as unknown as typeof platformHook;
|
||||||
} else {
|
} else {
|
||||||
// Old signature: useGame(cols, rows, platformHook)
|
// Old signature: useGame(cols, rows, platformHook)
|
||||||
@@ -32,7 +34,7 @@ export function useGame(optionsOrCols: GameOptions | number, rows?: number, plat
|
|||||||
|
|
||||||
const foodManager = useFood(cols, rows, boardManager);
|
const foodManager = useFood(cols, rows, boardManager);
|
||||||
|
|
||||||
const gameManager = useGameStatus(boardManager, snakeManager, foodManager);
|
const gameManager = useGameStatus(boardManager, snakeManager, foodManager, { startingLevel });
|
||||||
const { gameStatus, setGameStatus, startGame, quitGame, togglePause, score, level } = gameManager;
|
const { gameStatus, setGameStatus, startGame, quitGame, togglePause, score, level } = gameManager;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -4,28 +4,32 @@ import type { UseBoardType } from "../useBoard";
|
|||||||
import type { UseCursorType } from "../useCursor";
|
import type { UseCursorType } from "../useCursor";
|
||||||
import type { UseFoodType } from "../useGame/useFood";
|
import type { UseFoodType } from "../useGame/useFood";
|
||||||
|
|
||||||
const INITIAL_FPS = 3;
|
|
||||||
const FOODS_PER_LEVEL = 2;
|
const FOODS_PER_LEVEL = 2;
|
||||||
|
|
||||||
|
export type UseGameStatusParams = {
|
||||||
|
startingLevel?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export function useGameStatus(
|
export function useGameStatus(
|
||||||
boardManager: UseBoardType,
|
boardManager: UseBoardType,
|
||||||
snakeManager: UseCursorType,
|
snakeManager: UseCursorType,
|
||||||
foodManager: UseFoodType
|
foodManager: UseFoodType,
|
||||||
|
params?: UseGameStatusParams
|
||||||
) {
|
) {
|
||||||
|
const startingLevel = params?.startingLevel ?? 1;
|
||||||
const { containerRef } = boardManager;
|
const { containerRef } = boardManager;
|
||||||
const { initSnake, clearSnake, snakeBodyRef } = snakeManager;
|
const { initSnake, clearSnake, snakeBodyRef } = snakeManager;
|
||||||
const { spawnFood, clearFood } = foodManager;
|
const { spawnFood, clearFood } = foodManager;
|
||||||
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
|
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
|
||||||
const [score, setScore] = useState(0);
|
const [score, setScore] = useState(0);
|
||||||
const [level, setLevel] = useState(1);
|
const [level, setLevel] = useState(startingLevel);
|
||||||
const [paused, setPaused] = useState(false);
|
const [paused, setPaused] = useState(false);
|
||||||
const levelRef = useRef(1);
|
const levelRef = useRef(startingLevel);
|
||||||
const foodsEatenRef = useRef(0);
|
const foodsEatenRef = useRef(0);
|
||||||
const gameLoopRef = useRef<number | null>(null);
|
const gameLoopRef = useRef<number | null>(null);
|
||||||
const lastUpdateRef = useRef<number>(0);
|
const lastUpdateRef = useRef<number>(0);
|
||||||
const snakeManagerRef = useRef(snakeManager);
|
const snakeManagerRef = useRef(snakeManager);
|
||||||
const foodManagerRef = useRef(foodManager);
|
const foodManagerRef = useRef(foodManager);
|
||||||
const currentFpsRef = useRef(INITIAL_FPS);
|
|
||||||
|
|
||||||
snakeManagerRef.current = snakeManager;
|
snakeManagerRef.current = snakeManager;
|
||||||
foodManagerRef.current = foodManager;
|
foodManagerRef.current = foodManager;
|
||||||
@@ -34,10 +38,9 @@ export function useGameStatus(
|
|||||||
initSnake();
|
initSnake();
|
||||||
spawnFood(snakeBodyRef.current, 3);
|
spawnFood(snakeBodyRef.current, 3);
|
||||||
setScore(0);
|
setScore(0);
|
||||||
setLevel(1);
|
setLevel(startingLevel);
|
||||||
levelRef.current = 1;
|
levelRef.current = startingLevel;
|
||||||
foodsEatenRef.current = 0;
|
foodsEatenRef.current = 0;
|
||||||
currentFpsRef.current = INITIAL_FPS;
|
|
||||||
setGameStatus("started");
|
setGameStatus("started");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,67 +64,66 @@ export function useGameStatus(
|
|||||||
let shouldGrow = false;
|
let shouldGrow = false;
|
||||||
lastUpdateRef.current = performance.now();
|
lastUpdateRef.current = performance.now();
|
||||||
|
|
||||||
const gameLoop = (timestamp: number) => {
|
const gameLoop = (timestamp: number) => {
|
||||||
const msPerFrame = 1000 / currentFpsRef.current;
|
const msPerFrame = 1000 / levelRef.current;
|
||||||
const elapsed = timestamp - lastUpdateRef.current;
|
const elapsed = timestamp - lastUpdateRef.current;
|
||||||
|
|
||||||
if (elapsed >= msPerFrame) {
|
if (elapsed >= msPerFrame) {
|
||||||
// Apply any buffered direction input BEFORE moving
|
// Apply any buffered direction input BEFORE moving
|
||||||
snakeManagerRef.current.applyBufferedDirection();
|
snakeManagerRef.current.applyBufferedDirection();
|
||||||
|
|
||||||
const result = snakeManagerRef.current.moveSnake(shouldGrow);
|
const result = snakeManagerRef.current.moveSnake(shouldGrow);
|
||||||
|
|
||||||
shouldGrow = false;
|
shouldGrow = false;
|
||||||
|
|
||||||
if (result === "wall-collision" || result === "self-collision") {
|
if (result === "wall-collision" || result === "self-collision") {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
container?.querySelectorAll(".snake-head, .snake-body, .snake-tail").forEach((el: Element) => {
|
container?.querySelectorAll(".snake-head, .snake-body, .snake-tail").forEach((el: Element) => {
|
||||||
(el as HTMLElement).classList.add("collision");
|
(el as HTMLElement).classList.add("collision");
|
||||||
});
|
|
||||||
setGameStatus("game-over");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const head = snakeManagerRef.current.snakeBodyRef.current[0];
|
|
||||||
if (head && foodManagerRef.current.checkFoodCollision(head)) {
|
|
||||||
foodManagerRef.current.removeFood(head);
|
|
||||||
shouldGrow = true;
|
|
||||||
|
|
||||||
// Check BEFORE incrementing if we're about to level up
|
|
||||||
const willLevelUp = (foodsEatenRef.current + 1) >= FOODS_PER_LEVEL;
|
|
||||||
|
|
||||||
foodsEatenRef.current += 1;
|
|
||||||
|
|
||||||
// Save the current level BEFORE any changes
|
|
||||||
const pointsToAdd = levelRef.current;
|
|
||||||
|
|
||||||
// Update level OUTSIDE of setScore to avoid multiple triggers
|
|
||||||
if (willLevelUp) {
|
|
||||||
const newLevel = levelRef.current + 1;
|
|
||||||
levelRef.current = newLevel;
|
|
||||||
setLevel(newLevel);
|
|
||||||
currentFpsRef.current = INITIAL_FPS + (newLevel - 1);
|
|
||||||
foodsEatenRef.current = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
setScore((prev) => {
|
|
||||||
// Add points at CURRENT level
|
|
||||||
let newScore = prev + pointsToAdd;
|
|
||||||
|
|
||||||
foodManagerRef.current.spawnFood(
|
|
||||||
snakeManagerRef.current.snakeBodyRef.current,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
return newScore;
|
|
||||||
});
|
});
|
||||||
|
setGameStatus("game-over");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
const head = snakeManagerRef.current.snakeBodyRef.current[0];
|
||||||
|
if (head && foodManagerRef.current.checkFoodCollision(head)) {
|
||||||
|
foodManagerRef.current.removeFood(head);
|
||||||
|
shouldGrow = true;
|
||||||
|
|
||||||
lastUpdateRef.current = timestamp;
|
// Check BEFORE incrementing if we're about to level up
|
||||||
}
|
const willLevelUp = (foodsEatenRef.current + 1) >= FOODS_PER_LEVEL;
|
||||||
|
|
||||||
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
foodsEatenRef.current += 1;
|
||||||
};
|
|
||||||
|
// Save the current level BEFORE any changes
|
||||||
|
const pointsToAdd = levelRef.current;
|
||||||
|
|
||||||
|
// Update level OUTSIDE of setScore to avoid multiple triggers
|
||||||
|
if (willLevelUp) {
|
||||||
|
const newLevel = levelRef.current + 1;
|
||||||
|
levelRef.current = newLevel;
|
||||||
|
setLevel(newLevel);
|
||||||
|
foodsEatenRef.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setScore((prev) => {
|
||||||
|
// Add points at CURRENT level
|
||||||
|
let newScore = prev + pointsToAdd;
|
||||||
|
|
||||||
|
foodManagerRef.current.spawnFood(
|
||||||
|
snakeManagerRef.current.snakeBodyRef.current,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
return newScore;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUpdateRef.current = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
};
|
||||||
|
|
||||||
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user