zahlzerlegung/src/lib/stores/game.ts

137 lines
3.4 KiB
TypeScript

import { get, writable, derived } from 'svelte/store';
import { createDeck, drawTask, type Target, type Task, type TaskDeck } from '../game/tasks';
import { isWin, stageFor } from '../game/stages';
import { settings } from './settings';
import { recordResult } from './progress';
export type GameStatus = 'idle' | 'running' | 'won' | 'timeout';
export type GameState = {
status: GameStatus;
target: Target | null;
task: Task | null;
prevTask: Task | null;
correctCount: number;
timeLeftMs: number;
totalMs: number;
lastWasCorrect: boolean | null;
stageBumpKey: number; // monoton wachsend → triggert FX-Animation
deck: TaskDeck | null; // Ziehbeutel für abwechslungsreiche Aufgabenfolge
};
const initial: GameState = {
status: 'idle',
target: null,
task: null,
prevTask: null,
correctCount: 0,
timeLeftMs: 0,
totalMs: 0,
lastWasCorrect: null,
stageBumpKey: 0,
deck: null,
};
export const game = writable<GameState>(initial);
export const timeLeftSeconds = derived(game, ($g) => Math.max(0, Math.ceil($g.timeLeftMs / 1000)));
let tickHandle: number | null = null;
let lastTickAt = 0;
function clearTick() {
if (tickHandle !== null) {
cancelAnimationFrame(tickHandle);
tickHandle = null;
}
}
function tick() {
const now = performance.now();
const delta = now - lastTickAt;
lastTickAt = now;
let ended = false;
game.update((g) => {
if (g.status !== 'running') return g;
const next = Math.max(0, g.timeLeftMs - delta);
if (next <= 0) {
ended = true;
return { ...g, timeLeftMs: 0, status: 'timeout' };
}
return { ...g, timeLeftMs: next };
});
if (ended) {
clearTick();
finalize();
return;
}
tickHandle = requestAnimationFrame(tick);
}
function finalize() {
const g = get(game);
if (g.target === null) return;
recordResult(g.target, stageFor(g.correctCount));
}
export function startGame(target: Target): void {
clearTick();
const totalMs = get(settings).roundSeconds * 1000;
const { task: firstTask, deck } = drawTask(createDeck(target));
game.set({
status: 'running',
target,
task: firstTask,
prevTask: null,
correctCount: 0,
timeLeftMs: totalMs,
totalMs,
lastWasCorrect: null,
stageBumpKey: 0,
deck,
});
lastTickAt = performance.now();
tickHandle = requestAnimationFrame(tick);
}
export function answer(value: number): void {
const g = get(game);
if (g.status !== 'running' || !g.task || g.target === null) return;
const correct = value === g.task.answer;
if (correct) {
const nextCount = g.correctCount + 1;
const prevStage = stageFor(g.correctCount);
const newStage = stageFor(nextCount);
const stageBump = newStage > prevStage;
if (isWin(nextCount)) {
clearTick();
game.update((s) => ({
...s,
status: 'won',
correctCount: nextCount,
lastWasCorrect: true,
stageBumpKey: s.stageBumpKey + 1,
}));
finalize();
return;
}
const { task: nextTask, deck } = drawTask(g.deck ?? createDeck(g.target));
game.update((s) => ({
...s,
correctCount: nextCount,
prevTask: s.task,
task: nextTask,
deck,
lastWasCorrect: true,
stageBumpKey: stageBump ? s.stageBumpKey + 1 : s.stageBumpKey,
}));
} else {
game.update((s) => ({ ...s, lastWasCorrect: false }));
}
}
export function abortGame(): void {
clearTick();
game.set(initial);
}