Compare commits
No commits in common. "35564602b2c5c7a421fb9c46cfcac309d753339a" and "34bcef5da8695ca4858778c7a4c7c9dba2a03e78" have entirely different histories.
35564602b2
...
34bcef5da8
|
|
@ -35,23 +35,6 @@ describe('progress', () => {
|
||||||
expect(stages).toEqual([4, 3, 2, 1]);
|
expect(stages).toEqual([4, 3, 2, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('orders same-stage runs by faster time first', () => {
|
|
||||||
let p = emptyProgress();
|
|
||||||
p = recordRun(p, 8, 4, '2026-01-01T00:00:00.000Z', 40000);
|
|
||||||
p = recordRun(p, 8, 4, '2026-01-02T00:00:00.000Z', 20000);
|
|
||||||
p = recordRun(p, 8, 4, '2026-01-03T00:00:00.000Z', 30000);
|
|
||||||
const times = p.perTarget[8].top5.map((r) => r.timeMs);
|
|
||||||
expect(times).toEqual([20000, 30000, 40000]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ranks timed runs ahead of untimed ones at the same stage', () => {
|
|
||||||
let p = emptyProgress();
|
|
||||||
p = recordRun(p, 9, 4, '2026-01-02T00:00:00.000Z'); // keine Zeit
|
|
||||||
p = recordRun(p, 9, 4, '2026-01-01T00:00:00.000Z', 25000);
|
|
||||||
expect(p.perTarget[9].top5[0].timeMs).toBe(25000);
|
|
||||||
expect(p.perTarget[9].top5[1].timeMs).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('limits top5 to five entries', () => {
|
it('limits top5 to five entries', () => {
|
||||||
let p = emptyProgress();
|
let p = emptyProgress();
|
||||||
for (let i = 0; i < 10; i++) p = recordRun(p, 6, ((i % 4) + 1) as 1 | 2 | 3 | 4);
|
for (let i = 0; i < 10; i++) p = recordRun(p, 6, ((i % 4) + 1) as 1 | 2 | 3 | 4);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export type RunRecord = { stage: Stage; date: string; timeMs?: number };
|
||||||
|
|
||||||
export type TargetProgress = {
|
export type TargetProgress = {
|
||||||
runs: number;
|
runs: number;
|
||||||
top5: RunRecord[]; // sortiert: höchste Stufe zuerst, bei Gleichstand schnellere Zeit, dann neueste
|
top5: RunRecord[]; // sortiert: höchste Stufe zuerst, bei Gleichstand neueste zuerst
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Progress = {
|
export type Progress = {
|
||||||
|
|
@ -94,10 +94,6 @@ export function recordRun(
|
||||||
const next: RunRecord = { stage, date, timeMs };
|
const next: RunRecord = { stage, date, timeMs };
|
||||||
const merged = [...prev.top5, next].sort((a, b) => {
|
const merged = [...prev.top5, next].sort((a, b) => {
|
||||||
if (b.stage !== a.stage) return b.stage - a.stage;
|
if (b.stage !== a.stage) return b.stage - a.stage;
|
||||||
// Gleiche Stufe: schneller ist besser. Läufe ohne gemessene Zeit (Altdaten) hinten anstellen.
|
|
||||||
const ta = a.timeMs ?? Infinity;
|
|
||||||
const tb = b.timeMs ?? Infinity;
|
|
||||||
if (ta !== tb) return ta - tb;
|
|
||||||
return b.date.localeCompare(a.date);
|
return b.date.localeCompare(a.date);
|
||||||
});
|
});
|
||||||
const top5 = merged.slice(0, 5);
|
const top5 = merged.slice(0, 5);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
let lastBumpKey = $state(0);
|
let lastBumpKey = $state(0);
|
||||||
let lastCountdownTickAt = $state(0);
|
let lastCountdownTickAt = $state(0);
|
||||||
let lastStartTick = $state(0);
|
|
||||||
|
|
||||||
function handleAnswer(value: number) {
|
function handleAnswer(value: number) {
|
||||||
play('tap');
|
play('tap');
|
||||||
|
|
@ -42,14 +41,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start-Countdown: pro neuer Zahl (3/2/1) ein Tick-Sound.
|
|
||||||
$effect(() => {
|
|
||||||
if ($game.status === 'countdown' && $game.countdown !== lastStartTick) {
|
|
||||||
lastStartTick = $game.countdown;
|
|
||||||
play('countdown');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Countdown letzte 5 Sekunden
|
// Countdown letzte 5 Sekunden
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($game.status !== 'running') return;
|
if ($game.status !== 'running') return;
|
||||||
|
|
@ -99,49 +90,16 @@
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $game.status === 'countdown'}
|
|
||||||
<div class="countdown" aria-hidden="true">
|
|
||||||
{#key $game.countdown}
|
|
||||||
<span class="count">{$game.countdown}</span>
|
|
||||||
{/key}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.screen {
|
.screen {
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.countdown {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: rgba(8, 14, 34, 0.55);
|
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
z-index: 10;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.count {
|
|
||||||
font-size: clamp(120px, 32vw, 320px);
|
|
||||||
font-weight: 900;
|
|
||||||
color: white;
|
|
||||||
text-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);
|
|
||||||
/* Jede Zahl frisch animiert (per {#key}): groß rein, leicht raus. */
|
|
||||||
animation: tick 1s ease-out both;
|
|
||||||
}
|
|
||||||
@keyframes tick {
|
|
||||||
0% { transform: scale(0.3); opacity: 0; }
|
|
||||||
20% { transform: scale(1.1); opacity: 1; }
|
|
||||||
70% { transform: scale(1); opacity: 1; }
|
|
||||||
100% { transform: scale(0.85); opacity: 0; }
|
|
||||||
}
|
|
||||||
.topbar {
|
.topbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { isWin, stageFor } from '../game/stages';
|
||||||
import { settings } from './settings';
|
import { settings } from './settings';
|
||||||
import { recordResult } from './progress';
|
import { recordResult } from './progress';
|
||||||
|
|
||||||
export type GameStatus = 'idle' | 'countdown' | 'running' | 'won' | 'timeout';
|
export type GameStatus = 'idle' | 'running' | 'won' | 'timeout';
|
||||||
|
|
||||||
export type GameState = {
|
export type GameState = {
|
||||||
status: GameStatus;
|
status: GameStatus;
|
||||||
|
|
@ -17,11 +17,8 @@ export type GameState = {
|
||||||
lastWasCorrect: boolean | null;
|
lastWasCorrect: boolean | null;
|
||||||
stageBumpKey: number; // monoton wachsend → triggert FX-Animation
|
stageBumpKey: number; // monoton wachsend → triggert FX-Animation
|
||||||
deck: TaskDeck | null; // Ziehbeutel für abwechslungsreiche Aufgabenfolge
|
deck: TaskDeck | null; // Ziehbeutel für abwechslungsreiche Aufgabenfolge
|
||||||
countdown: number; // 3..1 während des Start-Countdowns, sonst 0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const COUNTDOWN_FROM = 3;
|
|
||||||
|
|
||||||
const initial: GameState = {
|
const initial: GameState = {
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
target: null,
|
target: null,
|
||||||
|
|
@ -33,7 +30,6 @@ const initial: GameState = {
|
||||||
lastWasCorrect: null,
|
lastWasCorrect: null,
|
||||||
stageBumpKey: 0,
|
stageBumpKey: 0,
|
||||||
deck: null,
|
deck: null,
|
||||||
countdown: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const game = writable<GameState>(initial);
|
export const game = writable<GameState>(initial);
|
||||||
|
|
@ -42,7 +38,6 @@ export const timeLeftSeconds = derived(game, ($g) => Math.max(0, Math.ceil($g.ti
|
||||||
|
|
||||||
let tickHandle: number | null = null;
|
let tickHandle: number | null = null;
|
||||||
let lastTickAt = 0;
|
let lastTickAt = 0;
|
||||||
let countdownHandle: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
function clearTick() {
|
function clearTick() {
|
||||||
if (tickHandle !== null) {
|
if (tickHandle !== null) {
|
||||||
|
|
@ -51,13 +46,6 @@ function clearTick() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearCountdown() {
|
|
||||||
if (countdownHandle !== null) {
|
|
||||||
clearTimeout(countdownHandle);
|
|
||||||
countdownHandle = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
const delta = now - lastTickAt;
|
const delta = now - lastTickAt;
|
||||||
|
|
@ -90,13 +78,10 @@ function finalize() {
|
||||||
|
|
||||||
export function startGame(target: Target): void {
|
export function startGame(target: Target): void {
|
||||||
clearTick();
|
clearTick();
|
||||||
clearCountdown();
|
|
||||||
const totalMs = get(settings).roundSeconds * 1000;
|
const totalMs = get(settings).roundSeconds * 1000;
|
||||||
const { task: firstTask, deck } = drawTask(createDeck(target));
|
const { task: firstTask, deck } = drawTask(createDeck(target));
|
||||||
// Erst ein 3-2-1-Countdown, damit das Kind sich auf den Start einstellen kann.
|
|
||||||
// Der Rundentimer läuft erst, wenn der Countdown durch ist (status → 'running').
|
|
||||||
game.set({
|
game.set({
|
||||||
status: 'countdown',
|
status: 'running',
|
||||||
target,
|
target,
|
||||||
task: firstTask,
|
task: firstTask,
|
||||||
prevTask: null,
|
prevTask: null,
|
||||||
|
|
@ -106,27 +91,10 @@ export function startGame(target: Target): void {
|
||||||
lastWasCorrect: null,
|
lastWasCorrect: null,
|
||||||
stageBumpKey: 0,
|
stageBumpKey: 0,
|
||||||
deck,
|
deck,
|
||||||
countdown: COUNTDOWN_FROM,
|
|
||||||
});
|
});
|
||||||
scheduleCountdownStep();
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleCountdownStep(): void {
|
|
||||||
countdownHandle = setTimeout(() => {
|
|
||||||
countdownHandle = null;
|
|
||||||
const g = get(game);
|
|
||||||
if (g.status !== 'countdown') return;
|
|
||||||
const next = g.countdown - 1;
|
|
||||||
if (next > 0) {
|
|
||||||
game.update((s) => ({ ...s, countdown: next }));
|
|
||||||
scheduleCountdownStep();
|
|
||||||
} else {
|
|
||||||
game.update((s) => ({ ...s, status: 'running', countdown: 0 }));
|
|
||||||
lastTickAt = performance.now();
|
lastTickAt = performance.now();
|
||||||
tickHandle = requestAnimationFrame(tick);
|
tickHandle = requestAnimationFrame(tick);
|
||||||
}
|
}
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function answer(value: number): void {
|
export function answer(value: number): void {
|
||||||
const g = get(game);
|
const g = get(game);
|
||||||
|
|
@ -166,6 +134,5 @@ export function answer(value: number): void {
|
||||||
|
|
||||||
export function abortGame(): void {
|
export function abortGame(): void {
|
||||||
clearTick();
|
clearTick();
|
||||||
clearCountdown();
|
|
||||||
game.set(initial);
|
game.set(initial);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user