working on levels
This commit is contained in:
+4
-1
@@ -3,7 +3,7 @@ import "@vimazing/vim-snake/game.css";
|
|||||||
import { useKeyBindings } from "./useKeyBindings";
|
import { useKeyBindings } from "./useKeyBindings";
|
||||||
|
|
||||||
function App() {
|
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 formatTime = (ms: number) => {
|
||||||
const seconds = Math.floor(ms / 1000);
|
const seconds = Math.floor(ms / 1000);
|
||||||
@@ -20,6 +20,9 @@ function App() {
|
|||||||
<div className="px-3 py-1 bg-muted rounded">
|
<div className="px-3 py-1 bg-muted rounded">
|
||||||
Time: {formatTime(scoreManager.timeValue)}
|
Time: {formatTime(scoreManager.timeValue)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="px-3 py-1 bg-muted rounded">
|
||||||
|
Level: {level}
|
||||||
|
</div>
|
||||||
<div className="px-3 py-1 bg-muted rounded">
|
<div className="px-3 py-1 bg-muted rounded">
|
||||||
Score: {score}
|
Score: {score}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -99,6 +99,33 @@
|
|||||||
border-radius: 2px 6px 6px 2px;
|
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
|
Food Rendering
|
||||||
========================================================= */
|
========================================================= */
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) {
|
|||||||
const foodManager = useFood(cols, rows, rendererManager);
|
const foodManager = useFood(cols, rows, rendererManager);
|
||||||
|
|
||||||
const gameManager = useGameStatus(rendererManager, snakeManager, foodManager);
|
const gameManager = useGameStatus(rendererManager, snakeManager, foodManager);
|
||||||
const { gameStatus, startGame, stopGame, score } = gameManager;
|
const { gameStatus, startGame, stopGame, score, level } = gameManager;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderBoard(cols, rows);
|
renderBoard(cols, rows);
|
||||||
@@ -37,6 +37,7 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) {
|
|||||||
startGame,
|
startGame,
|
||||||
stopGame,
|
stopGame,
|
||||||
score,
|
score,
|
||||||
|
level,
|
||||||
...snakeManager,
|
...snakeManager,
|
||||||
...foodManager,
|
...foodManager,
|
||||||
...keyBindings,
|
...keyBindings,
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import type { UseRendererType } from "./useRenderer";
|
|||||||
import type { UseSnakeType } from "./useSnake";
|
import type { UseSnakeType } from "./useSnake";
|
||||||
import type { UseFoodType } from "./useFood";
|
import type { UseFoodType } from "./useFood";
|
||||||
|
|
||||||
const FRAMES_PER_SECOND = 5;
|
const INITIAL_FPS = 5;
|
||||||
|
const FOODS_PER_LEVEL = 2;
|
||||||
|
|
||||||
export function useGameStatus(
|
export function useGameStatus(
|
||||||
rendererManager: UseRendererType,
|
rendererManager: UseRendererType,
|
||||||
@@ -16,10 +17,14 @@ export function useGameStatus(
|
|||||||
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 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);
|
||||||
|
const collisionGracePeriodRef = useRef(false);
|
||||||
|
|
||||||
snakeManagerRef.current = snakeManager;
|
snakeManagerRef.current = snakeManager;
|
||||||
foodManagerRef.current = foodManager;
|
foodManagerRef.current = foodManager;
|
||||||
@@ -28,6 +33,10 @@ export function useGameStatus(
|
|||||||
initSnake();
|
initSnake();
|
||||||
spawnFood(snakeBodyRef.current, 3);
|
spawnFood(snakeBodyRef.current, 3);
|
||||||
setScore(0);
|
setScore(0);
|
||||||
|
setLevel(1);
|
||||||
|
foodsEatenRef.current = 0;
|
||||||
|
currentFpsRef.current = INITIAL_FPS;
|
||||||
|
collisionGracePeriodRef.current = false;
|
||||||
setGameStatus("started");
|
setGameStatus("started");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,8 +49,8 @@ export function useGameStatus(
|
|||||||
clearFood();
|
clearFood();
|
||||||
setGameStatus("waiting");
|
setGameStatus("waiting");
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
container?.querySelectorAll(".snake-head, .snake-body, .snake-tail").forEach((el) => {
|
container?.querySelectorAll(".snake-head, .snake-body, .snake-tail, .collision").forEach((el) => {
|
||||||
el.classList.remove("snake-head", "snake-body", "snake-tail");
|
el.classList.remove("snake-head", "snake-body", "snake-tail", "collision");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,10 +60,10 @@ export function useGameStatus(
|
|||||||
console.log("Starting game loop");
|
console.log("Starting game loop");
|
||||||
|
|
||||||
let shouldGrow = false;
|
let shouldGrow = false;
|
||||||
const msPerFrame = 1000 / FRAMES_PER_SECOND;
|
|
||||||
lastUpdateRef.current = performance.now();
|
lastUpdateRef.current = performance.now();
|
||||||
|
|
||||||
const gameLoop = (timestamp: number) => {
|
const gameLoop = (timestamp: number) => {
|
||||||
|
const msPerFrame = 1000 / currentFpsRef.current;
|
||||||
const elapsed = timestamp - lastUpdateRef.current;
|
const elapsed = timestamp - lastUpdateRef.current;
|
||||||
|
|
||||||
if (elapsed >= msPerFrame) {
|
if (elapsed >= msPerFrame) {
|
||||||
@@ -64,17 +73,44 @@ export function useGameStatus(
|
|||||||
|
|
||||||
shouldGrow = false;
|
shouldGrow = false;
|
||||||
|
|
||||||
if (result === "wall-collision") {
|
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");
|
setGameStatus("game-over");
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
collisionGracePeriodRef.current = true;
|
||||||
|
lastUpdateRef.current = timestamp;
|
||||||
|
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collisionGracePeriodRef.current = false;
|
||||||
|
|
||||||
const head = snakeManagerRef.current.snakeBodyRef.current[0];
|
const head = snakeManagerRef.current.snakeBodyRef.current[0];
|
||||||
if (head && foodManagerRef.current.checkFoodCollision(head)) {
|
if (head && foodManagerRef.current.checkFoodCollision(head)) {
|
||||||
console.log("Food eaten!");
|
console.log("Food eaten!");
|
||||||
foodManagerRef.current.removeFood(head);
|
foodManagerRef.current.removeFood(head);
|
||||||
shouldGrow = true;
|
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(
|
foodManagerRef.current.spawnFood(
|
||||||
snakeManagerRef.current.snakeBodyRef.current,
|
snakeManagerRef.current.snakeBodyRef.current,
|
||||||
1
|
1
|
||||||
@@ -114,6 +150,7 @@ export function useGameStatus(
|
|||||||
startGame,
|
startGame,
|
||||||
stopGame,
|
stopGame,
|
||||||
score,
|
score,
|
||||||
|
level,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -12,6 +12,7 @@ export function useSnake(
|
|||||||
const snakeBodyRef = useRef<SnakeBody>([]);
|
const snakeBodyRef = useRef<SnakeBody>([]);
|
||||||
const [direction, setDirection] = useState<Direction>("up");
|
const [direction, setDirection] = useState<Direction>("up");
|
||||||
const directionRef = useRef<Direction>("up");
|
const directionRef = useRef<Direction>("up");
|
||||||
|
const lastMovedDirectionRef = useRef<Direction>("up");
|
||||||
|
|
||||||
const initSnake = () => {
|
const initSnake = () => {
|
||||||
const centerR = Math.floor(rows / 2);
|
const centerR = Math.floor(rows / 2);
|
||||||
@@ -27,6 +28,7 @@ export function useSnake(
|
|||||||
snakeBodyRef.current = initialBody;
|
snakeBodyRef.current = initialBody;
|
||||||
setDirection("up");
|
setDirection("up");
|
||||||
directionRef.current = "up";
|
directionRef.current = "up";
|
||||||
|
lastMovedDirectionRef.current = "up";
|
||||||
renderSnake(initialBody);
|
renderSnake(initialBody);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,6 +37,7 @@ export function useSnake(
|
|||||||
snakeBodyRef.current = [];
|
snakeBodyRef.current = [];
|
||||||
setDirection("up");
|
setDirection("up");
|
||||||
directionRef.current = "up";
|
directionRef.current = "up";
|
||||||
|
lastMovedDirectionRef.current = "up";
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSnake = (body: SnakeBody) => {
|
const renderSnake = (body: SnakeBody) => {
|
||||||
@@ -100,7 +103,7 @@ export function useSnake(
|
|||||||
right: "left",
|
right: "left",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opposites[directionRef.current] !== newDirection) {
|
if (opposites[lastMovedDirectionRef.current] !== newDirection) {
|
||||||
setDirection(newDirection);
|
setDirection(newDirection);
|
||||||
directionRef.current = newDirection;
|
directionRef.current = newDirection;
|
||||||
}
|
}
|
||||||
@@ -136,10 +139,23 @@ export function useSnake(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newHead = { r: newR, c: newC };
|
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
|
const newBody = grow
|
||||||
? [newHead, ...body]
|
? [newHead, ...body]
|
||||||
: [newHead, ...body.slice(0, -1)];
|
: [newHead, ...body.slice(0, -1)];
|
||||||
|
|
||||||
|
lastMovedDirectionRef.current = dir;
|
||||||
|
|
||||||
setSnakeBody(newBody);
|
setSnakeBody(newBody);
|
||||||
snakeBodyRef.current = newBody;
|
snakeBodyRef.current = newBody;
|
||||||
renderSnake(newBody);
|
renderSnake(newBody);
|
||||||
|
|||||||
Reference in New Issue
Block a user