import { get, writable, derived } from 'svelte/store'; import { generateTask, type Target, type Task } 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 }; const initial: GameState = { status: 'idle', target: null, task: null, prevTask: null, correctCount: 0, timeLeftMs: 0, totalMs: 0, lastWasCorrect: null, stageBumpKey: 0, }; export const game = writable(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 firstTask = generateTask(target); game.set({ status: 'running', target, task: firstTask, prevTask: null, correctCount: 0, timeLeftMs: totalMs, totalMs, lastWasCorrect: null, stageBumpKey: 0, }); 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 nextTask = generateTask(g.target, g.task); game.update((s) => ({ ...s, correctCount: nextCount, prevTask: s.task, task: nextTask, lastWasCorrect: true, stageBumpKey: stageBump ? s.stageBumpKey + 1 : s.stageBumpKey, })); } else { game.update((s) => ({ ...s, lastWasCorrect: false })); } } export function nextTaskAfterWrong(): void { const g = get(game); if (g.status !== 'running' || !g.task || g.target === null) return; const nextTask = generateTask(g.target, g.task); game.update((s) => ({ ...s, prevTask: s.task, task: nextTask, lastWasCorrect: null })); } export function abortGame(): void { clearTick(); game.set(initial); }