Start-Countdown 3-2-1 vor jeder Spielrunde

This commit is contained in:
schmop 2026-05-31 22:26:56 +02:00
parent 3c00728c62
commit 35564602b2
2 changed files with 79 additions and 4 deletions

View File

@ -17,6 +17,7 @@
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');
@ -41,6 +42,14 @@
} }
}); });
// 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;
@ -90,16 +99,49 @@
{/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;

View File

@ -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' | 'running' | 'won' | 'timeout'; export type GameStatus = 'idle' | 'countdown' | 'running' | 'won' | 'timeout';
export type GameState = { export type GameState = {
status: GameStatus; status: GameStatus;
@ -17,8 +17,11 @@ 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,
@ -30,6 +33,7 @@ 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);
@ -38,6 +42,7 @@ 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) {
@ -46,6 +51,13 @@ 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;
@ -78,10 +90,13 @@ 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: 'running', status: 'countdown',
target, target,
task: firstTask, task: firstTask,
prevTask: null, prevTask: null,
@ -91,9 +106,26 @@ export function startGame(target: Target): void {
lastWasCorrect: null, lastWasCorrect: null,
stageBumpKey: 0, stageBumpKey: 0,
deck, deck,
countdown: COUNTDOWN_FROM,
}); });
lastTickAt = performance.now(); scheduleCountdownStep();
tickHandle = requestAnimationFrame(tick); }
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();
tickHandle = requestAnimationFrame(tick);
}
}, 1000);
} }
export function answer(value: number): void { export function answer(value: number): void {
@ -134,5 +166,6 @@ export function answer(value: number): void {
export function abortGame(): void { export function abortGame(): void {
clearTick(); clearTick();
clearCountdown();
game.set(initial); game.set(initial);
} }