From d02a8e96185b81a4e79984d52890402e1c4902de Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Thu, 16 Oct 2025 17:18:31 +0100 Subject: [PATCH] working on levels --- example/src/App.tsx | 5 +++- src/game.css | 27 ++++++++++++++++++ src/useGame/useGame.ts | 3 +- src/useGame/useGameStatus.ts | 53 ++++++++++++++++++++++++++++++------ src/useGame/useSnake.ts | 18 +++++++++++- 5 files changed, 95 insertions(+), 11 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 58feba4..cf4a8f6 100755 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -3,7 +3,7 @@ import "@vimazing/vim-snake/game.css"; import { useKeyBindings } from "./useKeyBindings"; function App() { - const { containerRef, gameStatus, scoreManager, keyLog, score } = useGame(30, 20, useKeyBindings); + const { containerRef, gameStatus, scoreManager, keyLog, score, level } = useGame(30, 20, useKeyBindings); const formatTime = (ms: number) => { const seconds = Math.floor(ms / 1000); @@ -20,6 +20,9 @@ function App() {
Time: {formatTime(scoreManager.timeValue)}
+
+ Level: {level} +
Score: {score}
diff --git a/src/game.css b/src/game.css index 5b99c1d..92a38c2 100755 --- a/src/game.css +++ b/src/game.css @@ -99,6 +99,33 @@ border-radius: 2px 6px 6px 2px; } +/* ========================================================= + Collision Effect + ========================================================= */ +.snake-cell.collision { + background-color: var(--destructive, #c53b53) !important; + box-shadow: + 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 90%, transparent), + 0 0 24px color-mix(in srgb, var(--destructive, #c53b53) 60%, transparent), + inset 0 0 8px color-mix(in srgb, var(--destructive, #c53b53) 70%, transparent) !important; + animation: collision-pulse 0.5s ease-in-out infinite alternate; +} + +@keyframes collision-pulse { + 0% { + box-shadow: + 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 90%, transparent), + 0 0 24px color-mix(in srgb, var(--destructive, #c53b53) 60%, transparent), + inset 0 0 8px color-mix(in srgb, var(--destructive, #c53b53) 70%, transparent); + } + 100% { + box-shadow: + 0 0 20px color-mix(in srgb, var(--destructive, #c53b53) 100%, transparent), + 0 0 40px color-mix(in srgb, var(--destructive, #c53b53) 80%, transparent), + inset 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 90%, transparent); + } +} + /* ========================================================= Food Rendering ========================================================= */ diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts index 883ae2c..87611b2 100755 --- a/src/useGame/useGame.ts +++ b/src/useGame/useGame.ts @@ -16,7 +16,7 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) { const foodManager = useFood(cols, rows, rendererManager); const gameManager = useGameStatus(rendererManager, snakeManager, foodManager); - const { gameStatus, startGame, stopGame, score } = gameManager; + const { gameStatus, startGame, stopGame, score, level } = gameManager; useEffect(() => { renderBoard(cols, rows); @@ -37,6 +37,7 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) { startGame, stopGame, score, + level, ...snakeManager, ...foodManager, ...keyBindings, diff --git a/src/useGame/useGameStatus.ts b/src/useGame/useGameStatus.ts index ee1ac51..e16d7cb 100755 --- a/src/useGame/useGameStatus.ts +++ b/src/useGame/useGameStatus.ts @@ -4,7 +4,8 @@ import type { UseRendererType } from "./useRenderer"; import type { UseSnakeType } from "./useSnake"; import type { UseFoodType } from "./useFood"; -const FRAMES_PER_SECOND = 5; +const INITIAL_FPS = 5; +const FOODS_PER_LEVEL = 2; export function useGameStatus( rendererManager: UseRendererType, @@ -16,10 +17,14 @@ export function useGameStatus( const { spawnFood, clearFood } = foodManager; const [gameStatus, setGameStatus] = useState("waiting"); const [score, setScore] = useState(0); + const [level, setLevel] = useState(1); + const foodsEatenRef = useRef(0); const gameLoopRef = useRef(null); const lastUpdateRef = useRef(0); const snakeManagerRef = useRef(snakeManager); const foodManagerRef = useRef(foodManager); + const currentFpsRef = useRef(INITIAL_FPS); + const collisionGracePeriodRef = useRef(false); snakeManagerRef.current = snakeManager; foodManagerRef.current = foodManager; @@ -28,6 +33,10 @@ export function useGameStatus( initSnake(); spawnFood(snakeBodyRef.current, 3); setScore(0); + setLevel(1); + foodsEatenRef.current = 0; + currentFpsRef.current = INITIAL_FPS; + collisionGracePeriodRef.current = false; setGameStatus("started"); }; @@ -40,8 +49,8 @@ export function useGameStatus( clearFood(); setGameStatus("waiting"); const container = containerRef.current; - container?.querySelectorAll(".snake-head, .snake-body, .snake-tail").forEach((el) => { - el.classList.remove("snake-head", "snake-body", "snake-tail"); + container?.querySelectorAll(".snake-head, .snake-body, .snake-tail, .collision").forEach((el) => { + el.classList.remove("snake-head", "snake-body", "snake-tail", "collision"); }); }; @@ -51,10 +60,10 @@ export function useGameStatus( console.log("Starting game loop"); let shouldGrow = false; - const msPerFrame = 1000 / FRAMES_PER_SECOND; lastUpdateRef.current = performance.now(); const gameLoop = (timestamp: number) => { + const msPerFrame = 1000 / currentFpsRef.current; const elapsed = timestamp - lastUpdateRef.current; if (elapsed >= msPerFrame) { @@ -64,17 +73,44 @@ export function useGameStatus( shouldGrow = false; - if (result === "wall-collision") { - setGameStatus("game-over"); - return; + if (result === "wall-collision" || result === "self-collision") { + if (collisionGracePeriodRef.current) { + const container = containerRef.current; + container?.querySelectorAll(".snake-head, .snake-body, .snake-tail").forEach((el) => { + el.classList.add("collision"); + }); + setGameStatus("game-over"); + return; + } else { + collisionGracePeriodRef.current = true; + lastUpdateRef.current = timestamp; + gameLoopRef.current = requestAnimationFrame(gameLoop); + return; + } } + + collisionGracePeriodRef.current = false; const head = snakeManagerRef.current.snakeBodyRef.current[0]; if (head && foodManagerRef.current.checkFoodCollision(head)) { console.log("Food eaten!"); foodManagerRef.current.removeFood(head); shouldGrow = true; - setScore((prev) => prev + 1); + + foodsEatenRef.current += 1; + + if (foodsEatenRef.current >= FOODS_PER_LEVEL) { + setLevel((lvl) => { + const newLevel = lvl + 1; + currentFpsRef.current = INITIAL_FPS + (newLevel - 1); + console.log(`Level up! Level ${newLevel}, FPS: ${currentFpsRef.current}`); + return newLevel; + }); + foodsEatenRef.current = 0; + } + + setScore((prev) => prev + level); + foodManagerRef.current.spawnFood( snakeManagerRef.current.snakeBodyRef.current, 1 @@ -114,6 +150,7 @@ export function useGameStatus( startGame, stopGame, score, + level, }; } diff --git a/src/useGame/useSnake.ts b/src/useGame/useSnake.ts index 6c30688..4264a4c 100755 --- a/src/useGame/useSnake.ts +++ b/src/useGame/useSnake.ts @@ -12,6 +12,7 @@ export function useSnake( const snakeBodyRef = useRef([]); const [direction, setDirection] = useState("up"); const directionRef = useRef("up"); + const lastMovedDirectionRef = useRef("up"); const initSnake = () => { const centerR = Math.floor(rows / 2); @@ -27,6 +28,7 @@ export function useSnake( snakeBodyRef.current = initialBody; setDirection("up"); directionRef.current = "up"; + lastMovedDirectionRef.current = "up"; renderSnake(initialBody); }; @@ -35,6 +37,7 @@ export function useSnake( snakeBodyRef.current = []; setDirection("up"); directionRef.current = "up"; + lastMovedDirectionRef.current = "up"; }; const renderSnake = (body: SnakeBody) => { @@ -100,7 +103,7 @@ export function useSnake( right: "left", }; - if (opposites[directionRef.current] !== newDirection) { + if (opposites[lastMovedDirectionRef.current] !== newDirection) { setDirection(newDirection); directionRef.current = newDirection; } @@ -136,10 +139,23 @@ export function useSnake( } const newHead = { r: newR, c: newC }; + + // Check self-collision (excluding tail if not growing, as tail will move) + const bodyToCheck = grow ? body : body.slice(0, -1); + const collision = bodyToCheck.some( + (segment) => segment.r === newHead.r && segment.c === newHead.c + ); + + if (collision) { + return "self-collision"; + } + const newBody = grow ? [newHead, ...body] : [newHead, ...body.slice(0, -1)]; + lastMovedDirectionRef.current = dir; + setSnakeBody(newBody); snakeBodyRef.current = newBody; renderSnake(newBody);