Start-Countdown 3-2-1 vor jeder Spielrunde
This commit is contained in:
parent
3c00728c62
commit
35564602b2
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
let lastBumpKey = $state(0);
|
||||
let lastCountdownTickAt = $state(0);
|
||||
let lastStartTick = $state(0);
|
||||
|
||||
function handleAnswer(value: number) {
|
||||
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
|
||||
$effect(() => {
|
||||
if ($game.status !== 'running') return;
|
||||
|
|
@ -90,16 +99,49 @@
|
|||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{#if $game.status === 'countdown'}
|
||||
<div class="countdown" aria-hidden="true">
|
||||
{#key $game.countdown}
|
||||
<span class="count">{$game.countdown}</span>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.screen {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
padding: 12px;
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { isWin, stageFor } from '../game/stages';
|
|||
import { settings } from './settings';
|
||||
import { recordResult } from './progress';
|
||||
|
||||
export type GameStatus = 'idle' | 'running' | 'won' | 'timeout';
|
||||
export type GameStatus = 'idle' | 'countdown' | 'running' | 'won' | 'timeout';
|
||||
|
||||
export type GameState = {
|
||||
status: GameStatus;
|
||||
|
|
@ -17,8 +17,11 @@ export type GameState = {
|
|||
lastWasCorrect: boolean | null;
|
||||
stageBumpKey: number; // monoton wachsend → triggert FX-Animation
|
||||
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 = {
|
||||
status: 'idle',
|
||||
target: null,
|
||||
|
|
@ -30,6 +33,7 @@ const initial: GameState = {
|
|||
lastWasCorrect: null,
|
||||
stageBumpKey: 0,
|
||||
deck: null,
|
||||
countdown: 0,
|
||||
};
|
||||
|
||||
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 lastTickAt = 0;
|
||||
let countdownHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function clearTick() {
|
||||
if (tickHandle !== null) {
|
||||
|
|
@ -46,6 +51,13 @@ function clearTick() {
|
|||
}
|
||||
}
|
||||
|
||||
function clearCountdown() {
|
||||
if (countdownHandle !== null) {
|
||||
clearTimeout(countdownHandle);
|
||||
countdownHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
function tick() {
|
||||
const now = performance.now();
|
||||
const delta = now - lastTickAt;
|
||||
|
|
@ -78,10 +90,13 @@ function finalize() {
|
|||
|
||||
export function startGame(target: Target): void {
|
||||
clearTick();
|
||||
clearCountdown();
|
||||
const totalMs = get(settings).roundSeconds * 1000;
|
||||
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({
|
||||
status: 'running',
|
||||
status: 'countdown',
|
||||
target,
|
||||
task: firstTask,
|
||||
prevTask: null,
|
||||
|
|
@ -91,9 +106,26 @@ export function startGame(target: Target): void {
|
|||
lastWasCorrect: null,
|
||||
stageBumpKey: 0,
|
||||
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();
|
||||
tickHandle = requestAnimationFrame(tick);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
export function answer(value: number): void {
|
||||
|
|
@ -134,5 +166,6 @@ export function answer(value: number): void {
|
|||
|
||||
export function abortGame(): void {
|
||||
clearTick();
|
||||
clearCountdown();
|
||||
game.set(initial);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user