diff --git a/UNIFIED_API.md b/UNIFIED_API.md index b7a1173..1d17ae6 100644 --- a/UNIFIED_API.md +++ b/UNIFIED_API.md @@ -132,6 +132,7 @@ type GameManager = { getKeyLog: () => KeyLogEntry[]; // Optional: Game-specific additions + changeGameMode?: (options: GameOptions) => void; // Runtime option changes // [additional fields as needed] }; ``` @@ -201,6 +202,29 @@ type ScoreManager = { }; ``` +**typing-chud Content Tracking:** + +All content types are tracked independently throughout the game: + +```typescript +type ScoreManager = { + // ... core fields + + lettersCompleted: number; // Total letters typed + wordsCompleted: number; // Total words typed + sentencesCompleted: number; // Total sentences typed + paragraphsCompleted: number; // Total paragraphs typed + currentCount: number; // Count for current game mode +}; +``` + +**Platform Mode Switching Behavior:** + +When platform calls `changeGameMode()`: +- All historical counts remain (letters stay at 10 after switching to words) +- `currentCount` reflects the mode-specific count (0 for words if no words typed yet) +- Each mode accumulates independently + **Scoring Requirements:** - Score range: 0-1000 (capped) - Score calculated on game-won @@ -209,6 +233,39 @@ type ScoreManager = { --- +## Runtime Game Control + +Some games support dynamic option changes during gameplay via `changeGameMode()`: + +```typescript +// Change to a different mode mid-game +game.changeGameMode({ gameMode: 'words', downwardSpeed: 75 }); +``` + +### Delayed Mode Switching (typing-chud) + +Mode changes are deferred to respect current gameplay: +- Changes are stored as "pending options" +- Applied AFTER current letter/word/sentence/paragraph completes +- OR when player dies (text hits ship) +- Multiple rapid calls overwrite with latest options +- Ensures smooth transitions without interrupting input + +**Flow:** +``` +Platform calls changeGameMode() + ↓ +Options stored as pending + ↓ +Player finishes current letter/word + ↓ +Pending options applied + ↓ +Next letter spawns with new settings +``` + +--- + ## Game-Specific Flexibility These elements CAN vary between games: @@ -232,6 +289,14 @@ type GameOptions = { timeLimit?: number; }; +// typing-chud (Unified Platform Control) +type GameOptions = { + gameMode?: 'letters' | 'words' | 'sentences' | 'paragraphs'; + downwardSpeed?: number; // px/s (mode-agnostic) + boardDimensions?: [number, number]; // [width, height] + wordList?: string[]; // Custom word/sentence/paragraph list +}; + // vim-snake (hypothetical) type GameOptions = { boardSize?: number; @@ -245,6 +310,13 @@ type GameOptions = { - Make most options optional - Use consistent naming (e.g., `timeLimit` always in seconds) +**Platform Control (typing-chud):** +- Platforms have full control over game behavior via `GameOptions` +- `downwardSpeed` is mode-agnostic (single value for all modes) +- `wordList` allows custom content across all modes +- `changeGameMode(options)` allows runtime mode switching +- Mode changes respect current gameplay (apply after letter/word completes or death) + ### 2. Scoring Implementation Complete freedom in how scoring works internally: @@ -449,6 +521,11 @@ type ScoreManager = { - Additional metrics for display - Difficulty multipliers +**typing-chud Content Tracking:** +- Tracks all content types independently (letters, words, sentences, paragraphs) +- Preserves historical counts when mode switches +- `currentCount` reflects only the current game mode count + ### useGameStatus **Purpose:** Manage game lifecycle state machine. @@ -472,6 +549,8 @@ type GameStatusManager = { quitGame: () => void; // Optional: togglePause, reset, etc. + // Optional: setPendingOptions (for runtime changes) + setPendingOptions?: (options: GameOptions) => void; }; ``` @@ -855,6 +934,16 @@ interface UnifiedGame { ## Version History +**v2.1** (2025-11-04) +- Added delayed mode switching for runtime game control +- Documented `changeGameMode()` for platforms +- Added `GameOptions` for platform control (gameMode, downwardSpeed, wordList) +- Clarified pending options pattern for smooth transitions +- Updated GameManager interface with optional runtime control methods +- Added content tracking per mode (letters, words, sentences, paragraphs) +- `currentCount` reflects mode-specific completion count +- Historical counts preserved across mode switches + **v2.0** (2025-01-22) - Rewritten for game-agnostic specification - Clarified requirements vs flexibility diff --git a/src/types.ts b/src/types.ts index 75e148b..75437cb 100755 --- a/src/types.ts +++ b/src/types.ts @@ -105,38 +105,50 @@ export type CursorManager = { // ============================================================================ export type ScoreManager = { - timeValue: number; - totalKeystrokes: number; - score: number; - correct: number; - missed: number; - deaths: number; -}; + timeValue: number; + totalKeystrokes: number; + score: number; + correct: number; + missed: number; + deaths: number; + lettersCompleted: number; + wordsCompleted: number; + sentencesCompleted: number; + paragraphsCompleted: number; + currentCount: number; // Count for current game mode + }; // ============================================================================ // Manager Interfaces - Game Status // ============================================================================ export type GameStatusManager = { - gameStatus: GameStatus; - setGameStatus: (status: GameStatus) => void; - startGame: () => void; - quitGame: () => void; - level: number; - shipState: ShipState; - shipExplosionTime: number; - updatePlayerX: (x: number) => void; - handleTypedLetter: (letter: string) => void; - setGameLoopCallback: (callback: (deltaTime: number, letters: FallingLetter[]) => void) => void; - setGameMode: (mode: GameMode) => void; - addCorrect: () => void; - addMissed: () => void; - addDeath: () => void; - score: number; - correct: number; - missed: number; - deaths: number; -}; + gameStatus: GameStatus; + setGameStatus: (status: GameStatus) => void; + startGame: () => void; + quitGame: () => void; + level: number; + shipState: ShipState; + shipExplosionTime: number; + updatePlayerX: (x: number) => void; + handleTypedLetter: (letter: string) => void; + setGameLoopCallback: (callback: (deltaTime: number, letters: FallingLetter[]) => void) => void; + setGameMode: (mode: GameMode) => void; + setPendingOptions: (options: GameOptions) => void; + addCorrect: () => void; + addMissed: () => void; + addDeath: () => void; + trackCompletion: (mode: GameMode) => void; + score: number; + correct: number; + missed: number; + deaths: number; + lettersCompleted: number; + wordsCompleted: number; + sentencesCompleted: number; + paragraphsCompleted: number; + getCurrentCount: () => number; + }; // ============================================================================ // Manager Interfaces - Key Tracking diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts index f8b5dec..92d0be4 100755 --- a/src/useGame/useGame.ts +++ b/src/useGame/useGame.ts @@ -69,10 +69,10 @@ export function useGame( gameStatusManager.setGameMode(gameMode); }, [gameMode]); - const changeGameMode = (newOptions: GameOptions) => { - setOptions(newOptions); - // Don't quit - just switch modes and let game continue - }; + const changeGameMode = (newOptions: GameOptions) => { + setOptions(newOptions); + gameStatusManager.setPendingOptions(newOptions); + }; // Set the game loop callback useEffect(() => { diff --git a/src/useGameStatus/useGameStatus.ts b/src/useGameStatus/useGameStatus.ts index 4eafe06..ed7f9aa 100755 --- a/src/useGameStatus/useGameStatus.ts +++ b/src/useGameStatus/useGameStatus.ts @@ -23,6 +23,7 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal // Store options in refs so they can be accessed in the game loop const downwardSpeedRef = useRef(downwardSpeed); const wordListRef = useRef(wordList); + const pendingOptionsRef = useRef(null); // Update refs when options change useEffect(() => { @@ -33,13 +34,17 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal wordListRef.current = wordList; }, [wordList]); - const [gameStatus, setGameStatus] = useState("waiting"); - const shipStateRef = useRef("normal"); - const shipExplosionTimeRef = useRef(0); - const [score, setScore] = useState(0); - const [correct, setCorrect] = useState(0); - const [missed, setMissed] = useState(0); - const [deaths, setDeaths] = useState(0); + const [gameStatus, setGameStatus] = useState("waiting"); + const shipStateRef = useRef("normal"); + const shipExplosionTimeRef = useRef(0); + const [score, setScore] = useState(0); + const [correct, setCorrect] = useState(0); + const [missed, setMissed] = useState(0); + const [deaths, setDeaths] = useState(0); + const [lettersCompleted, setLettersCompleted] = useState(0); + const [wordsCompleted, setWordsCompleted] = useState(0); + const [sentencesCompleted, setSentencesCompleted] = useState(0); + const [paragraphsCompleted, setParagraphsCompleted] = useState(0); const gameLoopRef = useRef(null); const lastFrameTimeRef = useRef(0); const lettersRef = useRef([]); @@ -132,6 +137,8 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal lastSpawnRef.current = performance.now(); shipStateRef.current = 'normal'; shipExplosionTimeRef.current = 0; + // Apply pending options after death + applyPendingOptions(); }, 500); // Match the 500ms explosion animation } } @@ -265,16 +272,19 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal } } - // 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); + // Check if word is complete + if (charIndex >= fullText.length) { + // Word complete - explode and clear + currentLetter.state = 'exploding'; + currentLetter.stateStartTime = performance.now(); + trackCompletion(gameModeRef.current); + + setTimeout(() => { + lettersRef.current = []; + lastSpawnRef.current = 0; + // Apply pending options after letter completes + applyPendingOptions(); + }, 300); } else { // More characters to type - advance to next character currentLetter.charIndex = charIndex; @@ -319,6 +329,32 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal gameModeRef.current = mode; }; + const setPendingOptions = (newOptions: GameOptions) => { + pendingOptionsRef.current = newOptions; + }; + + const applyPendingOptions = () => { + if (!pendingOptionsRef.current) return; + + const pending = pendingOptionsRef.current; + pendingOptionsRef.current = null; + + // Update game mode + if (pending.gameMode) { + gameModeRef.current = pending.gameMode; + } + + // Update speed ref + if (pending.downwardSpeed !== undefined) { + downwardSpeedRef.current = pending.downwardSpeed; + } + + // Update word list ref + if (pending.wordList !== undefined) { + wordListRef.current = pending.wordList; + } + }; + const addCorrect = () => { setScore(prev => prev + 1); setCorrect(prev => prev + 1); @@ -329,29 +365,62 @@ export function useGameStatus(_onGameLoopTick?: (deltaTime: number, letters: Fal setMissed(prev => prev + 1); }; - const addDeath = () => { - setDeaths(prev => prev + 1); - }; + const addDeath = () => { + setDeaths(prev => prev + 1); + }; - return { - gameStatus, - setGameStatus, - startGame, - quitGame, - handleTypedLetter, - shipState: shipStateRef.current, // Return ref value for immediate updates - shipExplosionTime: shipExplosionTimeRef.current, // Return ref value for immediate updates - updatePlayerX, - setGameLoopCallback, - setGameMode, - addCorrect, - addMissed, - addDeath, - score, - correct, - missed, - deaths, - }; -} + const trackCompletion = (mode: GameMode) => { + if (mode === 'letters') { + setLettersCompleted(prev => prev + 1); + } else if (mode === 'words') { + setWordsCompleted(prev => prev + 1); + } else if (mode === 'sentences') { + setSentencesCompleted(prev => prev + 1); + } else if (mode === 'paragraphs') { + setParagraphsCompleted(prev => prev + 1); + } + }; + + const getCurrentCount = (): number => { + const mode = gameModeRef.current; + if (mode === 'letters') { + return lettersCompleted; + } else if (mode === 'words') { + return wordsCompleted; + } else if (mode === 'sentences') { + return sentencesCompleted; + } else if (mode === 'paragraphs') { + return paragraphsCompleted; + } + return 0; + }; + + return { + gameStatus, + setGameStatus, + startGame, + quitGame, + handleTypedLetter, + shipState: shipStateRef.current, // Return ref value for immediate updates + shipExplosionTime: shipExplosionTimeRef.current, // Return ref value for immediate updates + updatePlayerX, + setGameLoopCallback, + setGameMode, + setPendingOptions, + addCorrect, + addMissed, + addDeath, + trackCompletion, + score, + correct, + missed, + deaths, + lettersCompleted, + wordsCompleted, + sentencesCompleted, + paragraphsCompleted, + getCurrentCount, + }; + } export type UseGameStatusType = ReturnType; diff --git a/src/useScore/useScore.ts b/src/useScore/useScore.ts index 753acdc..12ea1fa 100755 --- a/src/useScore/useScore.ts +++ b/src/useScore/useScore.ts @@ -14,14 +14,19 @@ export function useScore(gameStatusManager: any): ScoreManager { return () => clearInterval(interval); }, [gameStatusManager.gameStatus]); - return { - timeValue, - totalKeystrokes: 0, // TODO: Track keystrokes from keyboard input - score: gameStatusManager.score, - correct: gameStatusManager.correct, - missed: gameStatusManager.missed, - deaths: gameStatusManager.deaths, - }; -} + return { + timeValue, + totalKeystrokes: 0, // TODO: Track keystrokes from keyboard input + score: gameStatusManager.score, + correct: gameStatusManager.correct, + missed: gameStatusManager.missed, + deaths: gameStatusManager.deaths, + lettersCompleted: gameStatusManager.lettersCompleted, + wordsCompleted: gameStatusManager.wordsCompleted, + sentencesCompleted: gameStatusManager.sentencesCompleted, + paragraphsCompleted: gameStatusManager.paragraphsCompleted, + currentCount: gameStatusManager.getCurrentCount(), + }; + } export type UseScoreType = ReturnType;