186 lines
5.0 KiB
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>
|