zahlzerlegung/src/lib/screens/GameScreen.svelte

186 lines
5.0 KiB
Svelte

<script lang="ts">
import { fade } from 'svelte/transition';
import type { Target } from '../game/tasks';
import { game, timeLeftSeconds, answer, abortGame } from '../stores/game';
import { goHome, goResult } from '../stores/route';
import { play } from '../audio/soundManager';
import HeightTrack from '../components/game/HeightTrack.svelte';
import TaskPrompt from '../components/game/TaskPrompt.svelte';
import AnswerButtons from '../components/game/AnswerButtons.svelte';
import Timer from '../components/game/Timer.svelte';
import BurstFx from '../components/game/BurstFx.svelte';
import IconButton from '../components/shared/IconButton.svelte';
import Star from '../components/svg/Star.svelte';
type Props = { target: Target };
let { target }: Props = $props();
let lastBumpKey = $state(0);
let lastCountdownTickAt = $state(0);
let lastStartTick = $state(0);
function handleAnswer(value: number) {
play('tap');
if ($game.task && value === $game.task.answer) {
play('correct');
play('boost');
}
answer(value);
}
function handleAbort() {
abortGame();
goHome();
}
// Stufenaufstieg → level-Sound zusätzlich abspielen
$effect(() => {
if ($game.stageBumpKey > lastBumpKey) {
lastBumpKey = $game.stageBumpKey;
if ($game.status === 'won') play('fanfare');
else play('level');
}
});
// 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;
const sec = $timeLeftSeconds;
if (sec > 0 && sec <= 5 && sec !== lastCountdownTickAt) {
lastCountdownTickAt = sec;
play('countdown');
}
});
// Bei Spielende → Result-Screen
$effect(() => {
if ($game.status === 'won' || $game.status === 'timeout') {
const t = setTimeout(() => goResult(target), $game.status === 'won' ? 1600 : 600);
return () => clearTimeout(t);
}
});
</script>
<div class="screen" in:fade={{ duration: 220 }}>
<header class="topbar">
<Timer secondsLeft={$timeLeftSeconds} totalSeconds={Math.max(1, Math.round($game.totalMs / 1000))} />
<IconButton label="Startseite" onClick={handleAbort}>
<svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true">
<path d="M14 4 L25 14 L22 14 L22 24 L16.5 24 L16.5 17.5 L11.5 17.5 L11.5 24 L6 24 L6 14 L3 14 Z" fill="currentColor" />
</svg>
</IconButton>
</header>
<div class="layout">
<aside class="track-wrap">
<HeightTrack correct={$game.correctCount} won={$game.status === 'won'} />
<BurstFx triggerKey={$game.stageBumpKey} />
</aside>
<main class="play">
{#if $game.task && $game.status === 'running'}
<TaskPrompt task={$game.task} />
<AnswerButtons
choices={$game.task.choices}
correctAnswer={$game.task.answer}
onAnswer={handleAnswer}
/>
{:else if $game.status === 'won'}
<div class="end-message">
<span class="big"><Star size={200} /></span>
</div>
{/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;
gap: 12px;
align-items: center;
}
.layout {
display: grid;
grid-template-columns: 30% 1fr;
gap: 14px;
min-height: 0;
}
.track-wrap {
position: relative;
min-height: 0;
}
.play {
display: grid;
grid-template-rows: 1fr auto;
gap: 24px;
align-items: center;
padding-bottom: 12px;
}
.end-message {
display: grid;
place-items: center;
height: 100%;
}
.big {
font-size: clamp(80px, 18vw, 200px);
animation: pop 0.6s ease;
}
@keyframes pop {
0% { transform: scale(0.4); opacity: 0; }
60% { transform: scale(1.15); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
@media (max-width: 640px) {
.layout { grid-template-columns: 24% 1fr; }
}
</style>