diff --git a/example/src/App.tsx b/example/src/App.tsx index ae345da..a9383e7 100755 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -4,8 +4,8 @@ import "@vimazing/typing-chud/game.css"; import { useKeyBindings } from "./useKeyBindings"; function App() { - const [gameMode, setGameMode] = useState("words"); - const gameManager = useGame({ gameMode }, useKeyBindings); + const [gameMode, setGameMode] = useState("letters"); + const gameManager = useGame({ initialGameMode: gameMode }, useKeyBindings); const { containerRef, gameStatus, scoreManager } = gameManager; const formatTime = (ms: number) => { @@ -21,20 +21,21 @@ function App() {

Typing Chud

{(['letters', 'words', 'sentences', 'paragraphs'] as const).map((mode) => ( - + ))}
diff --git a/example/src/useKeyBindings.ts b/example/src/useKeyBindings.ts index 986d64d..4654873 100755 --- a/example/src/useKeyBindings.ts +++ b/example/src/useKeyBindings.ts @@ -2,44 +2,44 @@ import { useEffect } from "react"; import type { GameManager } from "../../src"; export const useKeyBindings = (gameManager: GameManager) => { - const { gameStatus, renderBoard, startGame, quitGame, clearKeyLog, handleTypedLetter } = gameManager; - useEffect(() => { const handler = (ev: KeyboardEvent) => { // start game if (ev.code === "Space") { - if (["waiting", "game-over", "game-won"].includes(gameStatus)) { - clearKeyLog(); - renderBoard(); - if (gameStatus === "game-over" || gameStatus === "game-won") { - quitGame(); - setTimeout(() => startGame(), 0); + ev.preventDefault(); // Prevent button activation + if (["waiting", "game-over", "game-won"].includes(gameManager.gameStatus)) { + gameManager.clearKeyLog(); + gameManager.renderBoard(); + if (gameManager.gameStatus === "game-over" || gameManager.gameStatus === "game-won") { + gameManager.quitGame(); + setTimeout(() => gameManager.startGame(), 0); } else { - startGame(); + gameManager.startGame(); } return; } } // ignore other keys while waiting - if (gameStatus === "waiting") return; + if (gameManager.gameStatus === "waiting") return; // quit if (ev.key === "Escape") { - quitGame(); + ev.preventDefault(); + gameManager.quitGame(); return; } // handle letter typing during game - if (gameStatus === "started") { + if (gameManager.gameStatus === "started") { if (ev.key.length === 1 && /[a-zA-Z]/.test(ev.key)) { - handleTypedLetter(ev.key); + gameManager.handleTypedLetter(ev.key); } } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); - }, [gameStatus, handleTypedLetter]); + }, [gameManager.gameStatus]); }; diff --git a/src/types.ts b/src/types.ts index be14807..568bb2a 100755 --- a/src/types.ts +++ b/src/types.ts @@ -43,7 +43,7 @@ export type FallingLetter = { export type KeyLogEntry = { key: string; timestamp: number }; export type GameOptions = { - gameMode?: GameMode; + initialGameMode?: GameMode; }; // ============================================================================ @@ -153,6 +153,7 @@ export type GameManager = { // Game-specific handleTypedLetter: (letter: string) => void; + changeGameMode: (mode: GameMode) => void; }; // ============================================================================ diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts index 25cd9e9..7fc03fc 100755 --- a/src/useGame/useGame.ts +++ b/src/useGame/useGame.ts @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import type { GameManager, GameOptions, KeyLogEntry, FallingLetter } from "../types"; +import type { GameManager, GameOptions, KeyLogEntry, FallingLetter, GameMode } from "../types"; import { useBoard } from "../useBoard"; import { useCursor } from "../useCursor"; import { useGameStatus } from "../useGameStatus"; @@ -10,11 +10,11 @@ export function useGame( platformHook?: unknown ): GameManager { const [keyLog, setKeyLog] = useState([]); - const gameMode = options?.gameMode ?? 'letters'; + const [gameMode, setGameModeState] = useState(options?.initialGameMode ?? 'letters'); const boardManager = useBoard(); const cursorManager = useCursor(); - const gameStatusManager = useGameStatus(); + const gameStatusManager = useGameStatus(undefined, gameMode); // Wrap renderBoard to include player position const renderBoardWrapped = (letters: any = []) => { @@ -38,12 +38,20 @@ export function useGame( // Update gameMode when it changes (without recreating gameStatus state) useEffect(() => { gameStatusManager.setGameMode(gameMode); - }, [gameMode, gameStatusManager]); + }, [gameMode]); + + const changeGameMode = (mode: GameMode) => { + setGameModeState(mode); + // Only quit if the game is actively running + if (gameStatusManager.gameStatus === 'started') { + gameStatusManager.quitGame(); + } + }; // Set the game loop callback useEffect(() => { gameStatusManager.setGameLoopCallback(handleGameLoopTick); - }, [gameStatusManager, cursorManager, boardManager]); + }, []); // Initial render on mount useEffect(() => { @@ -65,6 +73,7 @@ export function useGame( clearKeyLog: () => setKeyLog([]), getKeyLog: () => [...keyLog], handleTypedLetter: gameStatusManager.handleTypedLetter, + changeGameMode, }; if (typeof platformHook === 'function') { diff --git a/src/useGameStatus/useGameStatus.ts b/src/useGameStatus/useGameStatus.ts index ce13522..45e7f73 100755 --- a/src/useGameStatus/useGameStatus.ts +++ b/src/useGameStatus/useGameStatus.ts @@ -1,4 +1,4 @@ -import { useState, useRef, useCallback } from "react"; +import { useState, useRef } from "react"; import type { GameStatus, FallingLetter, ShipState, GameMode } from "../types"; import { getRandomWord } from "../wordLists"; @@ -28,7 +28,7 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall const gameModeRef = useRef(gameMode); // Game loop - const gameLoop = useCallback((currentTime: number) => { + const gameLoop = (currentTime: number) => { const deltaTime = currentTime - lastFrameTimeRef.current; lastFrameTimeRef.current = currentTime; @@ -92,9 +92,9 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall // Continue loop gameLoopRef.current = requestAnimationFrame(gameLoop); - }, []); + }; - const startGame = useCallback(() => { + const startGame = () => { lastFrameTimeRef.current = performance.now(); lastSpawnRef.current = performance.now(); lettersRef.current = []; @@ -104,17 +104,17 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall setDeaths(0); setGameStatus("started"); gameLoopRef.current = requestAnimationFrame(gameLoop); - }, [gameLoop]); + }; - const quitGame = useCallback(() => { + const quitGame = () => { setGameStatus("waiting"); if (gameLoopRef.current !== null) { cancelAnimationFrame(gameLoopRef.current); gameLoopRef.current = null; } - }, []); + }; - const handleTypedLetter = useCallback((typedLetter: string) => { + const handleTypedLetter = (typedLetter: string) => { if (lettersRef.current.length === 0) return; const currentLetter = lettersRef.current[0]; @@ -155,33 +155,33 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall } }, 200); } - }, []); + }; - const updatePlayerX = useCallback((x: number) => { + const updatePlayerX = (x: number) => { playerXRef.current = x; - }, []); + }; - const setGameLoopCallback = useCallback((callback: (deltaTime: number, letters: FallingLetter[]) => void) => { + const setGameLoopCallback = (callback: (deltaTime: number, letters: FallingLetter[]) => void) => { onGameLoopTickRef.current = callback; - }, []); + }; - const setGameMode = useCallback((mode: GameMode) => { + const setGameMode = (mode: GameMode) => { gameModeRef.current = mode; - }, []); + }; - const addCorrect = useCallback(() => { + const addCorrect = () => { setScore(prev => prev + 1); setCorrect(prev => prev + 1); - }, []); + }; - const addMissed = useCallback(() => { + const addMissed = () => { setScore(prev => Math.max(0, prev - 1)); setMissed(prev => prev + 1); - }, []); + }; - const addDeath = useCallback(() => { + const addDeath = () => { setDeaths(prev => prev + 1); - }, []); + }; return { gameStatus, diff --git a/src/useScore/hooks/useTimer.ts b/src/useScore/hooks/useTimer.ts index 44b0e9e..a1a07a2 100644 --- a/src/useScore/hooks/useTimer.ts +++ b/src/useScore/hooks/useTimer.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; export type UseTimerReturn = { timeValue: number; @@ -22,16 +22,16 @@ export function useTimer(options: UseTimerOptions = {}): UseTimerReturn { const offsetRef = useRef(0); const startTimeRef = useRef(null); - const update = useCallback(() => { + const update = () => { if (startTimeRef.current === null) { return; } const now = performance.now(); setTimeValue(offsetRef.current + (now - startTimeRef.current)); - }, []); + }; - const startTimer = useCallback(() => { + const startTimer = () => { if (intervalIdRef.current !== null) { return; } @@ -42,9 +42,9 @@ export function useTimer(options: UseTimerOptions = {}): UseTimerReturn { update(); intervalIdRef.current = setInterval(update, Math.max(16, interval)); - }, [interval, update]); + }; - const stopTimer = useCallback(() => { + const stopTimer = () => { if (intervalIdRef.current !== null) { clearInterval(intervalIdRef.current); intervalIdRef.current = null; @@ -55,9 +55,9 @@ export function useTimer(options: UseTimerOptions = {}): UseTimerReturn { offsetRef.current += performance.now() - startTimeRef.current; startTimeRef.current = null; } - }, [update]); + }; - const resetTimer = useCallback(() => { + const resetTimer = () => { if (intervalIdRef.current !== null) { clearInterval(intervalIdRef.current); intervalIdRef.current = null; @@ -66,7 +66,7 @@ export function useTimer(options: UseTimerOptions = {}): UseTimerReturn { startTimeRef.current = null; offsetRef.current = 0; setTimeValue(0); - }, []); + }; useEffect(() => { return () => {