feat: implement falling letters spawning from top
- Letters spawn at random horizontal positions every 1 second - Letters fall at 100px/second from top to bottom - Random A-Z letters rendered in white monospace font - Fix game loop to start properly when startGame called - Remove letters that exit bottom of screen
This commit is contained in:
+4
-4
@@ -19,11 +19,11 @@ export const getGamePhase = (status: GameStatus): GamePhase =>
|
||||
// ============================================================================
|
||||
|
||||
export type PlayerPosition = { x: number }; // 0-9 for 10 columns
|
||||
export type Word = {
|
||||
export type FallingLetter = {
|
||||
id: string;
|
||||
text: string;
|
||||
x: number; // 0-9 column position
|
||||
y: number; // vertical position (0 = top, increases downward)
|
||||
letter: string;
|
||||
x: number; // 0-800 pixel position
|
||||
y: number; // 0-600 pixel position
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -25,7 +25,7 @@ export function useBoard() {
|
||||
canvasRef.current = canvas;
|
||||
}, []);
|
||||
|
||||
const renderBoard = (playerPosition: PlayerPosition) => {
|
||||
const renderBoard = (playerPosition: PlayerPosition, letters: any = []) => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
@@ -72,6 +72,15 @@ export function useBoard() {
|
||||
ctx.lineTo(spaceshipX + 8, spaceshipY + 15);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Draw falling letters
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.font = "24px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "top";
|
||||
letters.forEach((letter: any) => {
|
||||
ctx.fillText(letter.letter, letter.x, letter.y);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
+13
-7
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { GameManager, GameOptions, KeyLogEntry } from "../types";
|
||||
import { useBoard } from "../useBoard";
|
||||
import { useCursor } from "../useCursor";
|
||||
@@ -13,18 +13,24 @@ export function useGame(
|
||||
|
||||
const boardManager = useBoard();
|
||||
const cursorManager = useCursor();
|
||||
const gameStatusManager = useGameStatus();
|
||||
const scoreManager = useScore(gameStatusManager);
|
||||
|
||||
// Wrap renderBoard to include player position
|
||||
const renderBoardWrapped = () => {
|
||||
boardManager.renderBoard(cursorManager.position());
|
||||
};
|
||||
const renderBoardWrapped = useCallback((letters: any = []) => {
|
||||
boardManager.renderBoard(cursorManager.position(), letters);
|
||||
}, [boardManager, cursorManager]);
|
||||
|
||||
// Game loop tick handler
|
||||
const handleGameLoopTick = useCallback((_deltaTime: number, letters: any) => {
|
||||
renderBoardWrapped(letters);
|
||||
}, [renderBoardWrapped]);
|
||||
|
||||
const gameStatusManager = useGameStatus(handleGameLoopTick);
|
||||
const scoreManager = useScore(gameStatusManager);
|
||||
|
||||
// Initial render on mount
|
||||
useEffect(() => {
|
||||
renderBoardWrapped();
|
||||
}, []);
|
||||
}, [renderBoardWrapped]);
|
||||
|
||||
const gameManager: GameManager = {
|
||||
containerRef: boardManager.containerRef,
|
||||
|
||||
@@ -1,16 +1,67 @@
|
||||
import { useState } from "react";
|
||||
import type { GameStatus } from "../types";
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import type { GameStatus, FallingLetter } from "../types";
|
||||
|
||||
export function useGameStatus() {
|
||||
const LETTER_SPEED = 100; // pixels per second
|
||||
const SPAWN_INTERVAL = 1000; // ms between letter spawns
|
||||
|
||||
export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: FallingLetter[]) => void) {
|
||||
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
|
||||
const gameLoopRef = useRef<number | null>(null);
|
||||
const lastFrameTimeRef = useRef<number>(0);
|
||||
const lettersRef = useRef<FallingLetter[]>([]);
|
||||
const lastSpawnRef = useRef<number>(0);
|
||||
|
||||
const startGame = () => {
|
||||
// Game loop
|
||||
const gameLoop = useCallback((currentTime: number) => {
|
||||
const deltaTime = currentTime - lastFrameTimeRef.current;
|
||||
lastFrameTimeRef.current = currentTime;
|
||||
|
||||
// Spawn new letter if enough time has passed
|
||||
if (currentTime - lastSpawnRef.current > SPAWN_INTERVAL) {
|
||||
const randomLetter = String.fromCharCode(65 + Math.floor(Math.random() * 26)); // A-Z
|
||||
const randomX = Math.random() * 800;
|
||||
const newLetter: FallingLetter = {
|
||||
id: `${currentTime}-${randomX}`,
|
||||
letter: randomLetter,
|
||||
x: randomX,
|
||||
y: 0,
|
||||
};
|
||||
lettersRef.current.push(newLetter);
|
||||
lastSpawnRef.current = currentTime;
|
||||
}
|
||||
|
||||
// Update letter positions
|
||||
lettersRef.current.forEach(letter => {
|
||||
letter.y += (LETTER_SPEED * deltaTime) / 1000;
|
||||
});
|
||||
|
||||
// Remove letters that went off screen
|
||||
lettersRef.current = lettersRef.current.filter(letter => letter.y < 600);
|
||||
|
||||
// Call tick callback if provided
|
||||
if (onGameLoopTick) {
|
||||
onGameLoopTick(deltaTime, lettersRef.current);
|
||||
}
|
||||
|
||||
// Continue loop
|
||||
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||
}, [onGameLoopTick]);
|
||||
|
||||
const startGame = useCallback(() => {
|
||||
lastFrameTimeRef.current = performance.now();
|
||||
lastSpawnRef.current = performance.now();
|
||||
lettersRef.current = [];
|
||||
setGameStatus("started");
|
||||
};
|
||||
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||
}, [gameLoop]);
|
||||
|
||||
const quitGame = () => {
|
||||
const quitGame = useCallback(() => {
|
||||
setGameStatus("waiting");
|
||||
};
|
||||
if (gameLoopRef.current !== null) {
|
||||
cancelAnimationFrame(gameLoopRef.current);
|
||||
gameLoopRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
gameStatus,
|
||||
|
||||
Reference in New Issue
Block a user