diff --git a/example/src/App.tsx b/example/src/App.tsx index d5f92e1..82c5bb2 100755 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -3,15 +3,33 @@ import "@vimazing/vim-snake/game.css"; import { useKeyBindings } from "./useKeyBindings"; function App() { - const { containerRef, gameStatus } = useGame(30, 20, useKeyBindings); + const { containerRef, gameStatus, scoreManager, keyLog } = useGame(30, 20, useKeyBindings); + + const formatTime = (ms: number) => { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; + }; return (

VIMazing Snake

+ +
+
+ Time: {formatTime(scoreManager.timeValue)} +
+
+ Keys: {keyLog.length} +
+
+
+
{gameStatus === "waiting" &&

Press space to start

} - {gameStatus === "started" &&

Use hjkl to move • Press q to quit

} + {gameStatus === "started" &&

Use hjkl to change direction • Press q to quit

} {gameStatus === "game-over" &&

Game Over! Press space to restart

}
diff --git a/example/src/useKeyBindings.ts b/example/src/useKeyBindings.ts index 60bbc65..4e66c5b 100755 --- a/example/src/useKeyBindings.ts +++ b/example/src/useKeyBindings.ts @@ -5,16 +5,16 @@ export const useKeyBindings = (gameManager: UseGameType) => { const { gameStatus, startGame, stopGame, clearLog, resetCount } = gameManager; useEffect(() => { - const handler = (e: KeyboardEvent) => { - console.log("key pressed:", e.key, e.code); + const handler = (ev: KeyboardEvent) => { + // start game - if (e.code === "Space") { + if (ev.code === "Space") { if (["waiting", "game-over", "game-won"].includes(gameStatus)) { - // clearLog(); - // resetCount(); + clearLog(); + resetCount(); if (gameStatus === "game-over" || gameStatus === "game-won") { stopGame(); - // setTimeout(() => initGame(true), 0); + setTimeout(() => startGame(), 0); } else { startGame(); } @@ -22,26 +22,18 @@ export const useKeyBindings = (gameManager: UseGameType) => { } } - // ignore other keys while waiting - if (gameStatus === "waiting") return; - - // cancel count or stop game - if (e.key === "Escape") { - stopGame(); - resetCount(); - return; - } - // quit - if (e.key === "q" || e.key === "Q") { - stopGame(); - resetCount(); - return; + if (ev.key === "q" || ev.key === "Q") { + if (gameStatus === "started") { + stopGame(); + resetCount(); + return; + } } }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); + window.addEventListener("keydown", handler, { capture: true }); + return () => window.removeEventListener("keydown", handler, { capture: true }); }, [gameStatus, startGame, stopGame, clearLog, resetCount]); }; diff --git a/src/useGame/useGame.ts b/src/useGame/useGame.ts index 56fba49..31dfd8a 100755 --- a/src/useGame/useGame.ts +++ b/src/useGame/useGame.ts @@ -3,6 +3,7 @@ import { useRenderer } from "./useRenderer"; import { useSnake } from "./useSnake"; import { useGameStatus } from "./useGameStatus"; import { useKeyBindings, type UseKeyBindingsType } from "./useKeyBindings"; +import { useScore } from "../useScore"; export function useGame(cols: number, rows: number, platformHook?: unknown) { const rendererManager = useRenderer(); @@ -21,12 +22,12 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) { const keyBindings: UseKeyBindingsType = useKeyBindings({ gameManager: { gameStatus, - startGame, - stopGame, changeDirection, }, }); + const scoreManager = useScore({ gameStatus }); + const fullGameManager = { containerRef, gameStatus, @@ -34,6 +35,7 @@ export function useGame(cols: number, rows: number, platformHook?: unknown) { stopGame, ...snakeManager, ...keyBindings, + scoreManager, }; if (typeof platformHook === "function") { diff --git a/src/useGame/useKeyBindings.ts b/src/useGame/useKeyBindings.ts index 5f75d14..1257be9 100755 --- a/src/useGame/useKeyBindings.ts +++ b/src/useGame/useKeyBindings.ts @@ -1,10 +1,10 @@ -import { useEffect } from "react"; +import { useEffect, useRef, useState } from "react"; import type { GameStatus, Direction } from "../types"; +export type KeyLogEntry = { key: string; timestamp: number }; + export type GameBindingContext = { gameStatus: GameStatus; - startGame: () => void; - stopGame: () => void; changeDirection: (dir: Direction) => void; }; @@ -13,37 +13,73 @@ type UseKeyBindingsParams = { }; export function useKeyBindings({ gameManager }: UseKeyBindingsParams) { - const { gameStatus, startGame, stopGame, changeDirection } = gameManager; + const { gameStatus, changeDirection } = gameManager; + + const [keyLog, setKeyLog] = useState([]); + const logRef = useRef([]); + const countRef = useRef(""); + + const recordKey = (key: string) => { + const entry = { key, timestamp: performance.now() }; + logRef.current.push(entry); + setKeyLog([...logRef.current]); + }; + + const clearLog = () => { + logRef.current = []; + setKeyLog([]); + }; + + const resetCount = () => { + countRef.current = ""; + }; useEffect(() => { const handler = (ev: KeyboardEvent) => { if (!["started"].includes(gameStatus)) return; + recordKey(ev.key); + + let handled = false; + switch (ev.key) { case "h": case "H": changeDirection("left"); + handled = true; break; case "j": case "J": changeDirection("down"); + handled = true; break; case "k": case "K": changeDirection("up"); + handled = true; break; case "l": case "L": changeDirection("right"); + handled = true; break; } + + if (handled) { + ev.stopPropagation(); + } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); - }, [gameStatus, startGame, stopGame, changeDirection]); + }, [gameStatus, changeDirection]); - return {}; + return { + keyLog, + clearLog, + resetCount, + getLog: () => logRef.current, + }; } export type UseKeyBindingsType = ReturnType; diff --git a/src/useScore/useScore.ts b/src/useScore/useScore.ts index a4db2bf..ea5f781 100755 --- a/src/useScore/useScore.ts +++ b/src/useScore/useScore.ts @@ -1,8 +1,21 @@ -export function useScore() { +import { useTimer } from "./hooks/useTimer"; +import { useScoreTime } from "./useScoreTime"; +import type { GameStatus } from "../types"; + +export type UseScoreParams = { + gameStatus: GameStatus; +}; + +export function useScore({ gameStatus }: UseScoreParams) { + const timer = useTimer(); + const { timeValue, startTimer, stopTimer, resetTimer } = timer; + + useScoreTime({ gameStatus, timer }); + return { - timeValue: 0, - startTimer: () => {}, - stopTimer: () => {}, - resetTimer: () => {}, + timeValue, + startTimer, + stopTimer, + resetTimer, }; } diff --git a/src/useScore/useScoreTime.ts b/src/useScore/useScoreTime.ts index 06c9c79..4f76b85 100755 --- a/src/useScore/useScoreTime.ts +++ b/src/useScore/useScoreTime.ts @@ -1,3 +1,30 @@ -export function useScoreTime() { - return {}; +import { useEffect, useRef } from "react"; +import type { UseTimerReturn } from "./hooks/useTimer"; +import type { GameStatus } from "../types"; + +type UseScoreTimeParams = { + gameStatus: GameStatus; + timer: UseTimerReturn; +}; + +export function useScoreTime({ gameStatus, timer }: UseScoreTimeParams) { + const prevStatusRef = useRef(null); + const { startTimer, stopTimer, resetTimer } = timer; + + useEffect(() => { + prevStatusRef.current = gameStatus; + + switch (gameStatus) { + case "started": + resetTimer(); + startTimer(); + break; + + case "game-over": + case "game-won": + case "waiting": + stopTimer(); + break; + } + }, [gameStatus, startTimer, stopTimer, resetTimer]); }