feat: implement game modes system with letters and words support

- Add GameMode type: 'letters' | 'words' | 'sentences' | 'paragraphs'
- Add gameMode option to GameOptions (defaults to 'letters')
- Create wordLists.ts with curated word lists for each mode
- Update FallingLetter type to track:
  - fullText: complete word/sentence/paragraph
  - charIndex: current character progress in word
- Modify word spawning to pick from mode-specific word list
- Update handleTypedLetter to support multi-character typed sequences:
  - Track progress through word (charIndex)
  - Advance to next character when correct
  - Show complete word only when fully typed
- Enhanced canvas rendering with color-coded progress:
  - Green: completed characters
  - Yellow: current character being typed
  - White: remaining characters
- Add mode selector buttons to example app
- Default to 'words' mode in example for testing
- Full TypeScript support for all modes
This commit is contained in:
2025-11-03 23:10:00 +00:00
parent 7ee42a8dc1
commit 662557c933
7 changed files with 138 additions and 26 deletions
+33 -14
View File
@@ -1,5 +1,6 @@
import { useState, useRef, useCallback } from "react";
import type { GameStatus, FallingLetter, ShipState } from "../types";
import type { GameStatus, FallingLetter, ShipState, GameMode } from "../types";
import { getRandomWord } from "../wordLists";
const LETTER_SPEED = 100; // pixels per second
const SPAWN_INTERVAL = 1000; // ms between letter spawns
@@ -10,7 +11,7 @@ const CANVAS_WIDTH = 800;
const SPACESHIP_Y = 600 - 40; // Position of the ship on canvas
const COLLISION_THRESHOLD = 30; // Distance to trigger collision
export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: FallingLetter[]) => void) {
export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: FallingLetter[]) => void, gameMode: GameMode = 'letters') {
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
const [shipState, setShipState] = useState<ShipState>("normal");
const [shipExplosionTime, setShipExplosionTime] = useState(0);
@@ -24,21 +25,25 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall
const lastSpawnRef = useRef<number>(0);
const playerXRef = useRef<number>(CANVAS_WIDTH / 2);
const onGameLoopTickRef = useRef(onGameLoopTick);
const gameModeRef = useRef<GameMode>(gameMode);
// Game loop
const gameLoop = useCallback((currentTime: number) => {
const deltaTime = currentTime - lastFrameTimeRef.current;
lastFrameTimeRef.current = currentTime;
// Spawn new letter if enough time has passed and no letters active
// Spawn new letter/word if enough time has passed and no letters active
if (currentTime - lastSpawnRef.current > SPAWN_INTERVAL && lettersRef.current.length < MAX_ACTIVE_LETTERS) {
const randomLetter = String.fromCharCode(65 + Math.floor(Math.random() * 26)); // A-Z
const fullText = getRandomWord(gameModeRef.current);
const firstChar = fullText[0].toUpperCase();
const spawnWidth = CANVAS_WIDTH - MARGIN_LEFT - MARGIN_RIGHT;
const randomX = MARGIN_LEFT + Math.random() * spawnWidth;
const randomY = Math.random() * 150; // Between 0-150 (top half of screen)
const newLetter: FallingLetter = {
id: `${currentTime}-${randomX}`,
letter: randomLetter,
letter: firstChar,
fullText: fullText.toUpperCase(),
charIndex: 0,
x: randomX,
y: randomY,
state: 'normal',
@@ -113,17 +118,31 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall
if (lettersRef.current.length === 0) return;
const currentLetter = lettersRef.current[0];
if (typedLetter.toUpperCase() === currentLetter.letter) {
currentLetter.state = 'exploding';
currentLetter.stateStartTime = performance.now();
const expectedChar = currentLetter.letter;
const isCorrect = typedLetter.toUpperCase() === expectedChar;
if (isCorrect) {
setScore(prev => prev + 1);
setCorrect(prev => prev + 1);
setTimeout(() => {
lettersRef.current = [];
lastSpawnRef.current = 0;
}, 300);
const charIndex = (currentLetter.charIndex ?? 0) + 1;
const fullText = currentLetter.fullText ?? currentLetter.letter;
// Check if word is complete
if (charIndex >= fullText.length) {
// Word complete - explode and clear
currentLetter.state = 'exploding';
currentLetter.stateStartTime = performance.now();
setTimeout(() => {
lettersRef.current = [];
lastSpawnRef.current = 0;
}, 300);
} else {
// More characters to type - advance to next character
currentLetter.charIndex = charIndex;
currentLetter.letter = fullText[charIndex];
}
} else {
currentLetter.state = 'wrong';
currentLetter.stateStartTime = performance.now();