zahlzerlegung/src/lib/screens/GameScreen.svelte
2026-04-28 01:54:27 +02:00

142 lines
3.8 KiB
Svelte

<script lang="ts">
import { fade } from 'svelte/transition';
import type { Target } from '../game/tasks';
import { game, stage, 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';
type Props = { target: Target };
let { target }: Props = $props();
let lastBumpKey = $state(0);
let lastCountdownTickAt = $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');
}
});
// 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">
<IconButton label="Zurück" onClick={handleAbort}>
<svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true">
<path d="M18 5 L8 14 L18 23" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</IconButton>
<Timer secondsLeft={$timeLeftSeconds} totalSeconds={Math.max(1, Math.round($game.totalMs / 1000))} />
</header>
<div class="layout">
<aside class="track-wrap">
<HeightTrack stage={$stage} 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">🌈</span>
</div>
{/if}
</main>
</div>
</div>
<style>
.screen {
height: 100%;
display: grid;
grid-template-rows: auto 1fr;
padding: 12px;
gap: 8px;
}
.topbar {
display: flex;
justify-content: space-between;
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>