137 lines
3.4 KiB
TypeScript
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);
|
|
}
|