feat: implement complete score tracking system
- Track score (starts at 0) - +1 score and +1 correct for each correct character typed - -1 score and +1 missed for each wrong character typed - +1 deaths for each collision with ship - Score resets when game starts - All metrics exposed through ScoreManager - Updated example app UI with colorful scoreboard showing: - Time (gray) - Score (neutral) - Correct (green) - Missed (yellow) - Deaths (red) - Keystrokes (neutral) - Uses grid layout for organized metric display - Score cannot go below 0 (clamped to minimum)
This commit is contained in:
Executable → Regular
+2
-2
@@ -5,7 +5,7 @@
|
||||
"name": "vimazing-vim-snake-example",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vimazing/vim-snake": "file:../",
|
||||
"@vimazing/typing-chud": "file:../",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
},
|
||||
@@ -211,7 +211,7 @@
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
|
||||
|
||||
"@vimazing/vim-snake": ["@vimazing/vim-snake@file:..", { "devDependencies": { "@types/node": "^24.7.2", "@types/react": "^19.2.2", "typescript": "~5.9.3" }, "peerDependencies": { "react": "^19.1.1" } }],
|
||||
"@vimazing/typing-chud": ["@vimazing/typing-chud@file:..", { "devDependencies": { "@types/node": "^24.7.2", "@types/react": "^19.2.2", "typescript": "~5.9.3" }, "peerDependencies": { "react": "^19.1.1" } }],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.0.4", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.38", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA=="],
|
||||
|
||||
|
||||
+21
-3
@@ -17,12 +17,30 @@ function App() {
|
||||
<div className="relative mx-auto my-4 w-fit space-y-4">
|
||||
<h1 className="text-2xl font-bold text-center">Typing Chud</h1>
|
||||
|
||||
<div className="flex gap-4 justify-center text-sm font-mono">
|
||||
<div className="grid grid-cols-6 gap-2 justify-center text-sm font-mono">
|
||||
<div className="px-3 py-1 bg-muted rounded">
|
||||
Time: {formatTime(scoreManager.timeValue)}
|
||||
<div className="text-xs text-muted-foreground">Time</div>
|
||||
<div className="font-bold">{formatTime(scoreManager.timeValue)}</div>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-muted rounded">
|
||||
Keystrokes: {scoreManager.totalKeystrokes}
|
||||
<div className="text-xs text-muted-foreground">Score</div>
|
||||
<div className="font-bold">{scoreManager.score}</div>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-green-900 rounded">
|
||||
<div className="text-xs text-green-200">Correct</div>
|
||||
<div className="font-bold text-green-100">{scoreManager.correct}</div>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-yellow-900 rounded">
|
||||
<div className="text-xs text-yellow-200">Missed</div>
|
||||
<div className="font-bold text-yellow-100">{scoreManager.missed}</div>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-red-900 rounded">
|
||||
<div className="text-xs text-red-200">Deaths</div>
|
||||
<div className="font-bold text-red-100">{scoreManager.deaths}</div>
|
||||
</div>
|
||||
<div className="px-3 py-1 bg-muted rounded">
|
||||
<div className="text-xs text-muted-foreground">Keystrokes</div>
|
||||
<div className="font-bold">{scoreManager.totalKeystrokes}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "cd example && bun run dev",
|
||||
"build": "tsc --outDir dist --declaration --emitDeclarationOnly false && node -e \"const fs = require('node:fs'); fs.mkdirSync('dist', { recursive: true }); fs.copyFileSync('src/game.css', 'dist/game.css'); fs.copyFileSync('package.json', 'dist/package.json');\"",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
|
||||
+11
-2
@@ -82,6 +82,10 @@ export type CursorManager = {
|
||||
export type ScoreManager = {
|
||||
timeValue: number;
|
||||
totalKeystrokes: number;
|
||||
score: number;
|
||||
correct: number;
|
||||
missed: number;
|
||||
deaths: number;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -94,13 +98,18 @@ export type GameStatusManager = {
|
||||
startGame: () => void;
|
||||
quitGame: () => void;
|
||||
level: number;
|
||||
score: number;
|
||||
togglePause: () => void;
|
||||
shipState: ShipState;
|
||||
shipExplosionTime: number;
|
||||
updatePlayerX: (x: number) => void;
|
||||
handleTypedLetter: (letter: string) => void;
|
||||
setGameLoopCallback: (callback: (deltaTime: number, letters: FallingLetter[]) => void) => void;
|
||||
addCorrect: () => void;
|
||||
addMissed: () => void;
|
||||
addDeath: () => void;
|
||||
score: number;
|
||||
correct: number;
|
||||
missed: number;
|
||||
deaths: number;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -14,6 +14,10 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall
|
||||
const [gameStatus, setGameStatus] = useState<GameStatus>("waiting");
|
||||
const [shipState, setShipState] = useState<ShipState>("normal");
|
||||
const [shipExplosionTime, setShipExplosionTime] = useState(0);
|
||||
const [score, setScore] = useState(0);
|
||||
const [correct, setCorrect] = useState(0);
|
||||
const [missed, setMissed] = useState(0);
|
||||
const [deaths, setDeaths] = useState(0);
|
||||
const gameLoopRef = useRef<number | null>(null);
|
||||
const lastFrameTimeRef = useRef<number>(0);
|
||||
const lettersRef = useRef<FallingLetter[]>([]);
|
||||
@@ -61,6 +65,7 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall
|
||||
letter.stateStartTime = currentTime;
|
||||
setShipState('exploding');
|
||||
setShipExplosionTime(performance.now());
|
||||
setDeaths(prev => prev + 1);
|
||||
|
||||
// Clear letters and reset spawn timer after explosion animation
|
||||
setTimeout(() => {
|
||||
@@ -88,6 +93,10 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall
|
||||
lastFrameTimeRef.current = performance.now();
|
||||
lastSpawnRef.current = performance.now();
|
||||
lettersRef.current = [];
|
||||
setScore(0);
|
||||
setCorrect(0);
|
||||
setMissed(0);
|
||||
setDeaths(0);
|
||||
setGameStatus("started");
|
||||
gameLoopRef.current = requestAnimationFrame(gameLoop);
|
||||
}, [gameLoop]);
|
||||
@@ -108,6 +117,8 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall
|
||||
if (typedLetter.toUpperCase() === currentLetter.letter) {
|
||||
currentLetter.state = 'exploding';
|
||||
currentLetter.stateStartTime = performance.now();
|
||||
setScore(prev => prev + 1);
|
||||
setCorrect(prev => prev + 1);
|
||||
|
||||
setTimeout(() => {
|
||||
lettersRef.current = [];
|
||||
@@ -116,6 +127,8 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall
|
||||
} else {
|
||||
currentLetter.state = 'wrong';
|
||||
currentLetter.stateStartTime = performance.now();
|
||||
setScore(prev => Math.max(0, prev - 1));
|
||||
setMissed(prev => prev + 1);
|
||||
|
||||
setTimeout(() => {
|
||||
if (currentLetter.state === 'wrong') {
|
||||
@@ -133,6 +146,20 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall
|
||||
onGameLoopTickRef.current = callback;
|
||||
}, []);
|
||||
|
||||
const addCorrect = useCallback(() => {
|
||||
setScore(prev => prev + 1);
|
||||
setCorrect(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
const addMissed = useCallback(() => {
|
||||
setScore(prev => Math.max(0, prev - 1));
|
||||
setMissed(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
const addDeath = useCallback(() => {
|
||||
setDeaths(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
gameStatus,
|
||||
setGameStatus,
|
||||
@@ -143,6 +170,13 @@ export function useGameStatus(onGameLoopTick?: (deltaTime: number, letters: Fall
|
||||
shipExplosionTime,
|
||||
updatePlayerX,
|
||||
setGameLoopCallback,
|
||||
addCorrect,
|
||||
addMissed,
|
||||
addDeath,
|
||||
score,
|
||||
correct,
|
||||
missed,
|
||||
deaths,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ export function useScore(gameStatusManager: any): ScoreManager {
|
||||
|
||||
return {
|
||||
timeValue,
|
||||
totalKeystrokes: 0, // TODO: Track keystrokes
|
||||
totalKeystrokes: 0, // TODO: Track keystrokes from keyboard input
|
||||
score: gameStatusManager.score,
|
||||
correct: gameStatusManager.correct,
|
||||
missed: gameStatusManager.missed,
|
||||
deaths: gameStatusManager.deaths,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user