feat(sasch40): major overhaul with doodle jump, party bunker, and guitar hero

- Add global settings & pause modal (gear icon) with mute and reset, removing obsolete bag button
- Extend Prologue with Flo Maier (castle fire game over & Löschzug restart) and Viktor (vampire bite game over & Knoblauch restart)
- Rebuild Akt II as 3-level vertical Doodle Jump climber to rescue sisters Saskia, Maja, and Miriam with custom pixel portraits
- Update Akt III Schankraum with seamless oak floor, double-tap 'Luke zum Party Bunker', duel intro modals, and Hansi's departure in Hinterland
- Rebuild Akt IV as 3-lane medieval Guitar Hero rock concert with live crowd meter, combo multipliers, and Hansi duo
- Update AGENTS.md architectural documentation and playtest test suite
This commit is contained in:
Moritz Graf
2026-09-24 18:22:28 +02:00
parent 6374ba9e8a
commit 8a0b65c3fa
22 changed files with 2307 additions and 1250 deletions
@@ -162,6 +162,116 @@ class SoundEngine {
})
}
// SFX: Sister Rescue Fanfare (bright sweet jingle)
public playRescueFanfare() {
if (this.isMuted) return
this.initCtx()
if (!this.ctx) return
const notes = [523.25, 659.25, 783.99, 1046.50] // C5, E5, G5, C6
notes.forEach((freq, idx) => {
if (!this.ctx) return
const now = this.ctx.currentTime + idx * 0.1
const osc = this.ctx.createOscillator()
const gain = this.ctx.createGain()
osc.type = 'sine'
osc.frequency.setValueAtTime(freq, now)
gain.gain.setValueAtTime(0.25, now)
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35)
osc.connect(gain)
gain.connect(this.ctx.destination)
osc.start(now)
osc.stop(now + 0.35)
})
}
// SFX: Spring / Trampoline Bounce (Doodle Jump super bounce)
public playSpringBounce() {
if (this.isMuted) return
this.initCtx()
if (!this.ctx) return
const now = this.ctx.currentTime
const osc = this.ctx.createOscillator()
const gain = this.ctx.createGain()
osc.type = 'triangle'
osc.frequency.setValueAtTime(220, now)
osc.frequency.exponentialRampToValueAtTime(880, now + 0.25)
gain.gain.setValueAtTime(0.3, now)
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3)
osc.connect(gain)
gain.connect(this.ctx.destination)
osc.start(now)
osc.stop(now + 0.3)
}
// SFX: Hit / Impact / Thud
public playBoarHit() {
if (this.isMuted) return
this.initCtx()
if (!this.ctx) return
const now = this.ctx.currentTime
const osc = this.ctx.createOscillator()
const gain = this.ctx.createGain()
osc.type = 'sawtooth'
osc.frequency.setValueAtTime(160, now)
osc.frequency.exponentialRampToValueAtTime(40, now + 0.2)
gain.gain.setValueAtTime(0.3, now)
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2)
osc.connect(gain)
gain.connect(this.ctx.destination)
osc.start(now)
osc.stop(now + 0.2)
}
// SFX: Medieval Lute Hero Note (String Index 0: Low, 1: Mid, 2: High)
public playLuteNote(lane: number, success: boolean = true) {
if (this.isMuted) return
this.initCtx()
if (!this.ctx) return
const now = this.ctx.currentTime
const osc = this.ctx.createOscillator()
const gain = this.ctx.createGain()
if (!success) {
// Missed clunk / muted buzz
osc.type = 'sawtooth'
osc.frequency.setValueAtTime(90, now)
gain.gain.setValueAtTime(0.12, now)
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12)
} else {
// Resonant lute string twang
const freqs = [293.66, 392.00, 587.33] // D4, G4, D5
const freq = freqs[lane % 3] || 440
osc.type = 'triangle'
osc.frequency.setValueAtTime(freq, now)
gain.gain.setValueAtTime(0.28, now)
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.32)
}
osc.connect(gain)
gain.connect(this.ctx.destination)
osc.start(now)
osc.stop(now + (success ? 0.32 : 0.12))
}
// 135 BPM Medieval Techno Chiptune Beat
public startBGM() {
if (this.isMuted || this.bgmPlaying) return
@@ -11,18 +11,32 @@ import CertificateModal from './CertificateModal.vue'
const store = useGameStore()
const platformerRef = ref<InstanceType<typeof PlatformerView> | null>(null)
const inventoryOpen = ref(false)
const concertRef = ref<InstanceType<typeof PartyExpressView> | null>(null)
function handleJump() {
if (platformerRef.value) {
if (store.currentAct === 'platformer' && platformerRef.value) {
platformerRef.value.onJump()
} else if (store.currentAct === 'party_express' && concertRef.value) {
concertRef.value.hitLane(2)
}
}
function handleDpad(dir: 'left' | 'right', pressed: boolean) {
if (platformerRef.value) {
function handleButtonB() {
if (store.currentAct === 'party_express' && concertRef.value) {
concertRef.value.hitLane(1)
} else {
store.addBeer()
}
}
function handleDpad(dir: 'left' | 'right' | 'down', pressed: boolean) {
if (store.currentAct === 'platformer' && platformerRef.value) {
if (dir === 'left') platformerRef.value.onLeft(pressed)
if (dir === 'right') platformerRef.value.onRight(pressed)
} else if (store.currentAct === 'party_express' && concertRef.value && pressed) {
if (dir === 'left') concertRef.value.hitLane(0)
if (dir === 'down') concertRef.value.hitLane(1)
if (dir === 'right') concertRef.value.hitLane(2)
}
}
@@ -30,8 +44,26 @@ function toggleAudio() {
store.toggleMute()
}
function toggleInventory() {
inventoryOpen.value = !inventoryOpen.value
const settingsOpen = ref(false)
function openSettings() {
sound.playDialogueBlip()
settingsOpen.value = true
store.pauseGame()
}
function closeSettings() {
sound.playDialogueBlip()
settingsOpen.value = false
store.resumeGame()
}
function resetGameFromSettings() {
if (confirm('Möchtest du das gesamte Spiel wirklich von vorne neu beginnen?')) {
settingsOpen.value = false
store.resumeGame()
store.resetGame()
}
}
</script>
@@ -48,8 +80,8 @@ function toggleInventory() {
{{ store.currentAct === 'prologue' ? 'AKT I' : store.currentAct === 'platformer' ? 'AKT II' : store.currentAct === 'tavern' ? 'AKT III' : store.currentAct === 'party_express' ? 'AKT IV' : store.currentAct === 'finale' ? 'AKT V' : '' }}
</span>
</div>
<button class="sound-toggle" @click="toggleAudio">
{{ store.soundMuted ? '🔇 TON' : '🔊 135 BPM' }}
<button class="settings-toggle-btn" @click="openSettings" title="Einstellungen & Pause" aria-label="Einstellungen">
⚙️
</button>
</div>
@@ -60,7 +92,7 @@ function toggleInventory() {
<PrologueView v-else-if="store.currentAct === 'prologue'" />
<PlatformerView v-else-if="store.currentAct === 'platformer'" ref="platformerRef" />
<TavernView v-else-if="store.currentAct === 'tavern'" />
<PartyExpressView v-else-if="store.currentAct === 'party_express'" />
<PartyExpressView v-else-if="store.currentAct === 'party_express'" ref="concertRef" />
<CertificateModal v-else-if="store.currentAct === 'finale'" />
</div>
</div>
@@ -99,7 +131,14 @@ function toggleInventory() {
@mousedown="handleDpad('right', true)"
@mouseup="handleDpad('right', false)"
>▶</button>
<button class="dpad-btn dpad-down" disabled></button>
<button
class="dpad-btn dpad-down"
:disabled="store.currentAct !== 'party_express'"
@touchstart.prevent="handleDpad('down', true)"
@touchend.prevent="handleDpad('down', false)"
@mousedown="handleDpad('down', true)"
@mouseup="handleDpad('down', false)"
>▼</button>
</div>
<span class="control-label">D-PAD</span>
</div>
@@ -108,9 +147,9 @@ function toggleInventory() {
<div class="center-controls">
<div class="royal-crest">👑</div>
<div class="pill-buttons">
<button class="pill-btn" @click="toggleInventory">
<button class="pill-btn" @click="openSettings">
<span class="pill-key">SELECT</span>
<span class="pill-sub">BEUTEL</span>
<span class="pill-sub">PAUSE</span>
</button>
<button class="pill-btn" @click="toggleAudio">
<span class="pill-key">START</span>
@@ -124,7 +163,7 @@ function toggleInventory() {
<div class="action-buttons">
<button
class="action-round-btn btn-b"
@click="store.addBeer"
@click="handleButtonB"
>
<span class="btn-letter">B</span>
</button>
@@ -137,8 +176,8 @@ function toggleInventory() {
</button>
</div>
<div class="action-labels">
<span>O'ZAPFT (B)</span>
<span>SPRUNG (A)</span>
<span>{{ store.currentAct === 'party_express' ? 'SAITE G (B)' : "O'ZAPFT (B)" }}</span>
<span>{{ store.currentAct === 'party_express' ? 'SAITE A (A)' : 'SPRUNG (A)' }}</span>
</div>
</div>
</div>
@@ -154,21 +193,27 @@ function toggleInventory() {
<span class="adv-icon">🍻</span>
<span class="adv-val">{{ store.beers }} Helles</span>
</div>
<button class="inv-mini-btn" @click="toggleInventory">
🎒 Beutel
</button>
</div>
<!-- Inventory Drawer Modal -->
<div v-if="inventoryOpen" class="inventory-drawer" @click="inventoryOpen = false">
<div class="inventory-card" @click.stop>
<h3>👑 KÖNIGLICHER LEERGUTSACK</h3>
<ul class="inventory-items">
<li v-for="item in store.inventory" :key="item" class="inv-item">
✨ {{ item }}
</li>
</ul>
<button class="close-inv-btn" @click="inventoryOpen = false">Schließen</button>
<!-- Settings & Pause Modal -->
<div v-if="settingsOpen" class="settings-overlay" @click="closeSettings">
<div class="settings-card" @click.stop>
<div class="settings-header">
<h3 class="settings-title">⚙️ EINSTELLUNGEN & PAUSE</h3>
<button class="settings-close-x" @click="closeSettings">✕</button>
</div>
<p class="settings-paused-hint">⏸️ Spiel und Timer pausiert</p>
<div class="settings-actions">
<button class="settings-btn" @click="toggleAudio">
{{ store.soundMuted ? '🔇 Ton: AUS (Einschalten)' : '🔊 Ton: 135 BPM AN (Stummschalten)' }}
</button>
<button class="settings-btn reset-danger-btn" @click="resetGameFromSettings">
🔄 Spiel neu starten (Reset)
</button>
<button class="settings-btn resume-btn" @click="closeSettings">
▶ Weiterspielen
</button>
</div>
</div>
</div>
</div>
@@ -237,15 +282,23 @@ function toggleInventory() {
border-radius: 2px;
}
.sound-toggle {
background: #78350f;
color: #fef08a;
.settings-toggle-btn {
background: rgba(120, 53, 15, 0.7);
color: #ffd700;
border: 1px solid #ffd700;
border-radius: 4px;
font-family: 'Press Start 2P', monospace;
font-size: 7px;
padding: 4px 6px;
font-size: 13px;
padding: 2px 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
}
.settings-toggle-btn:hover, .settings-toggle-btn:active {
background: #b45309;
transform: scale(1.1);
}
/* Screen Bezel */
@@ -303,17 +356,6 @@ function toggleInventory() {
color: #ffd700;
}
.inv-mini-btn {
background: #78350f;
color: #fef08a;
border: 1px solid #b45309;
border-radius: 4px;
padding: 4px 8px;
font-family: 'Press Start 2P', monospace;
font-size: 7px;
cursor: pointer;
}
/* Parchment Status Bar */
.parchment-scroll {
background: #fdf6e2;
@@ -510,54 +552,119 @@ function toggleInventory() {
color: #d4af37;
}
/* Inventory Modal */
.inventory-drawer {
/* Settings & Pause Modal */
.settings-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.75);
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
z-index: 250;
padding: 16px;
}
.inventory-card {
.settings-card {
background: #2b1810;
border: 3px solid #d4af37;
border-radius: 8px;
padding: 16px;
width: 80%;
padding: 18px;
width: 100%;
max-width: 320px;
color: #fef3c7;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.9);
}
.inventory-card h3 {
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1.5px solid #78350f;
padding-bottom: 8px;
margin-bottom: 12px;
}
.settings-title {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
font-size: 8px;
color: #ffd700;
margin-top: 0;
margin: 0;
}
.inventory-items {
list-style: none;
padding: 0;
margin: 12px 0;
}
.inv-item {
font-family: 'MedievalSharp', serif;
font-size: 15px;
padding: 4px 0;
border-bottom: 1px dashed #78350f;
}
.close-inv-btn {
background: #78350f;
color: #fff;
border: 1px solid #d4af37;
border-radius: 4px;
padding: 6px 12px;
font-family: 'Press Start 2P', monospace;
font-size: 7px;
.settings-close-x {
background: #7f1d1d;
color: #fef08a;
border: 1px solid #ef4444;
border-radius: 50%;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: bold;
cursor: pointer;
line-height: 1;
transition: all 0.15s ease;
}
.settings-close-x:hover {
background: #dc2626;
transform: scale(1.1);
}
.settings-paused-hint {
font-family: 'Press Start 2P', monospace;
font-size: 6.5px;
color: #38bdf8;
text-align: center;
margin: 0 0 16px 0;
}
.settings-actions {
display: flex;
flex-direction: column;
gap: 10px;
}
.settings-btn {
background: #3b2014;
color: #fef08a;
border: 1.5px solid #b45309;
border-radius: 6px;
padding: 10px 8px;
font-family: 'MedievalSharp', serif;
font-size: 14px;
font-weight: bold;
cursor: pointer;
text-align: center;
transition: all 0.15s ease;
}
.settings-btn:hover {
background: #78350f;
border-color: #ffd700;
}
.reset-danger-btn {
background: rgba(127, 29, 29, 0.4);
border-color: #ef4444;
color: #fca5a5;
}
.reset-danger-btn:hover {
background: #991b1b;
color: #fff;
border-color: #f87171;
}
.resume-btn {
background: #15803d;
border-color: #86efac;
color: #fff;
}
.resume-btn:hover {
background: #16a34a;
}
</style>
@@ -51,10 +51,10 @@ function handleStart() {
<div class="scroll-section">
<h3 class="section-title">🎮 WAS DICH IM SPIEL ERWARTET</h3>
<ul class="features-list">
<li><strong>🏃‍♂️ Retro Jump & Run:</strong> Hüpfe über die Dächer der Hopfen-Pfade und sammle die verlorenen Kronkorken ein.</li>
<li><strong>🍻 Tavernen-Abenteuer:</strong> Sprich mit Verena, Lutz (ZTUL), Armend, Amy, Mane & Pü und gewinne das bayerische Beleidigungs-Gstanzl-Fechten!</li>
<li><strong>☁️ Doodle Jump Schwesternrettung:</strong> Hüpfe von Wolke zu Wolke hinauf und rette Saschas drei Schwestern Saskia, Maja und Miriam!</li>
<li><strong>🍻 Tavernen-Abenteuer:</strong> Erkunde Gaststube, Atelier, Marktplatz, Hinterland & den geheimen Party Bunker. Bezwinge NAT im Wildschwein-Duell und gewinne das bayerische Gstanzl-Fechten!</li>
<li><strong>🌴 Der Zauberspiegel:</strong> Finde das geheime Urlaubserinnerungs-Easter-Egg von Buschkönig Zuluman.</li>
<li><strong>🔊 Der 135-BPM-Express:</strong> Rase mit den Hof-DJs auf dem Bollerwagen und versorge das feiernde Gefolge.</li>
<li><strong>🎸 Mittelalter Guitar Hero:</strong> Rocke als Barden-Duo mit Ritter Hansi auf der Schlossbühne sein legendäres Abschiedskonzert nach Münchanien!</li>
<li><strong>🏆 Highscore-Urkunde:</strong> Hol dir am Ende deine persönliche königliche Punktzahl und vergleiche dich mit allen Partygästen!</li>
</ul>
</div>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -17,6 +17,10 @@ function selectChoice(choice: typeof PROLOGUE_DIALOGUES['start']['choices'][0])
if (choice.action === 'unlockBridgerton') {
store.unlockEasterEgg('bridgerton')
}
if (choice.action === 'resetPrologue') {
currentNodeId.value = 'start'
return
}
if (choice.action === 'startPlatformer') {
store.setAct('platformer')
return
@@ -24,6 +28,9 @@ function selectChoice(choice: typeof PROLOGUE_DIALOGUES['start']['choices'][0])
if (choice.nextId && PROLOGUE_DIALOGUES[choice.nextId]) {
currentNodeId.value = choice.nextId
if (PROLOGUE_DIALOGUES[choice.nextId].isGameOver) {
sound.playBoarHit()
}
}
}
@@ -32,14 +39,14 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
</script>
<template>
<div class="prologue-container">
<div class="prologue-container" :class="{ 'game-over-active': currentNode().isGameOver }">
<!-- Balcony Visual Scene with Regal Portrait Frame -->
<div class="scene-visual">
<div class="scene-visual" :class="{ 'fire-visual': currentNodeId === 'flo_game_over', 'vampire-visual': currentNodeId === 'viktor_game_over' }">
<div class="balcony-backdrop"></div>
<div class="scene-tag">
<span class="scene-title">SCHLOSS HAUMDAUCHER – DER BALKON</span>
<span class="scene-title">{{ currentNode().isGameOver ? '💀 GAME OVER – HAUMDAUCHER KAPUTT!' : 'SCHLOSS HAUMDAUCHER – DER BALKON' }}</span>
</div>
<div class="speaker-frame">
<div class="speaker-frame" :class="{ 'game-over-frame': currentNode().isGameOver }">
<img
:src="currentSpeaker().portrait"
:alt="currentSpeaker().name"
@@ -49,7 +56,7 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
</div>
<!-- Retro Dialogue Card -->
<div class="dialogue-card">
<div class="dialogue-card" :class="{ 'game-over-card': currentNode().isGameOver }">
<div class="dialogue-header">
<div class="speaker-name-badge">
<span class="speaker-name">{{ currentSpeaker().name }}</span>
@@ -57,7 +64,7 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
</div>
</div>
<div class="dialogue-body">
<div class="dialogue-body" :class="{ 'game-over-body': currentNode().isGameOver }">
<p class="dialogue-text">„{{ currentNode().text }}“</p>
</div>
@@ -66,9 +73,10 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
v-for="(choice, idx) in currentNode().choices"
:key="idx"
class="choice-btn"
:class="{ 'game-over-btn': currentNode().isGameOver }"
@click="selectChoice(choice)"
>
<span class="choice-arrow">▶</span>
<span class="choice-arrow">{{ currentNode().isGameOver ? '🔄' : '▶' }}</span>
<span class="choice-text">„{{ choice.text }}“</span>
</button>
</div>
@@ -227,4 +235,54 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
color: #f59e0b;
font-size: 10px;
}
/* Game Over Styles */
.game-over-active {
background: #200505;
}
.fire-visual {
background: radial-gradient(circle at 50% 40%, #7f1d1d 0%, #1c0505 100%) !important;
border-bottom-color: #ef4444 !important;
box-shadow: 0 0 30px rgba(239, 68, 68, 0.5) inset;
}
.vampire-visual {
background: radial-gradient(circle at 50% 40%, #581c87 0%, #0f051d 100%) !important;
border-bottom-color: #a855f7 !important;
box-shadow: 0 0 30px rgba(168, 85, 247, 0.5) inset;
}
.game-over-frame {
border-color: #ef4444 !important;
box-shadow: 0 0 20px #ef4444 !important;
}
.game-over-card {
background: #1c0808 !important;
}
.game-over-body {
border-left-color: #ef4444 !important;
background: rgba(127, 29, 29, 0.3) !important;
}
.game-over-btn {
background: #7f1d1d !important;
border-color: #f87171 !important;
color: #fef08a !important;
font-weight: bold;
animation: pulse-border 1.5s infinite;
}
.game-over-btn:hover {
background: #991b1b !important;
transform: scale(1.02);
}
@keyframes pulse-border {
0%, 100% { box-shadow: 0 0 4px #ef4444; }
50% { box-shadow: 0 0 14px #ef4444; }
}
</style>
@@ -24,15 +24,33 @@ const activeDialog = ref<ActiveDialog | null>(null)
const duelActive = ref(false)
const duelStep = ref(0)
const duelWon = ref(false)
const duelIntroOpen = ref(false)
const zulumanMirrorActive = ref(false)
const trapdoorDiscovered = ref(store.unlockedEasterEggs.includes('hinterzimmer'))
let lastTrapTap = 0
function switchRoom(room: TavernRoom) {
sound.playDialogueBlip()
currentRoom.value = room
}
function handleTrapdoorClick() {
if (trapdoorDiscovered.value || store.unlockedEasterEggs.includes('hinterzimmer')) {
sound.playFanfare()
currentRoom.value = 'hinterzimmer'
return
}
const now = Date.now()
if (now - lastTrapTap < 450) {
// Double tap / double click
triggerTrapdoor()
} else {
lastTrapTap = now
sound.playDialogueBlip()
}
}
function triggerTrapdoor() {
sound.playFanfare()
trapdoorDiscovered.value = true
@@ -40,7 +58,7 @@ function triggerTrapdoor() {
currentRoom.value = 'hinterzimmer'
}
function enterSecretHinterzimmer() {
function enterPartyBunker() {
sound.playFanfare()
store.unlockEasterEgg('hinterzimmer')
currentRoom.value = 'hinterzimmer'
@@ -49,6 +67,7 @@ function enterSecretHinterzimmer() {
// Wildschwein Schnick-Schnack-Schnuck Minigame with NAT
type WildBoarChoice = 'keiler' | 'dickicht' | 'lanze'
const wildBoarIntroOpen = ref(false)
const wildBoarDuelActive = ref(false)
const wildBoarPlayerScore = ref(0)
const wildBoarNatScore = ref(0)
@@ -59,8 +78,14 @@ const wildBoarLastNatMove = ref<WildBoarChoice | null>(null)
const wildBoarGameOver = ref(false)
const wildBoarPlayerWon = ref(false)
function openWildBoarIntro() {
sound.playDialogueBlip()
wildBoarIntroOpen.value = true
}
function startWildBoarDuel() {
sound.playDialogueBlip()
wildBoarIntroOpen.value = false
wildBoarDuelActive.value = true
wildBoarPlayerScore.value = 0
wildBoarNatScore.value = 0
@@ -78,7 +103,10 @@ function formatBoarMove(m: WildBoarChoice): string {
return '🏹 Lanze'
}
function playWildBoarMove(playerChoice: WildBoarChoice) {
function playWildBoarMove(playerChoice: WildBoarChoice, event?: Event) {
if (event?.currentTarget) {
(event.currentTarget as HTMLElement).blur()
}
if (wildBoarGameOver.value) return
const choices: WildBoarChoice[] = ['keiler', 'dickicht', 'lanze']
@@ -209,10 +237,10 @@ function openCharacterDialog(charId: string) {
title: char.title,
portrait: char.portrait,
text: 'Servus Sascha! Schau dir meine Fest-Bühne an! Ich erzähle den Leuten gerade epische Geschichten von meinen Girls und meinen Heldentaten im Haumdaucher Dickicht. Bevor du meine Kronkorken kriegst, forder ich dich zum Hof-Duell heraus: Wildschwein-Schnick-Schnack-Schnuck!',
actionLabel: '🐗 Wildschwein-Duell starten! (Best of 3)',
actionLabel: '🐗 Zum Wildschwein-Duell!',
actionFn: () => {
activeDialog.value = null
startWildBoarDuel()
openWildBoarIntro()
}
}
} else if (charId === 'mane') {
@@ -234,8 +262,8 @@ function openCharacterDialog(charId: string) {
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Habe die Ehre Sascha! Schau dir dieses Panorama an. Ich halte hier die Stellung und habe dir 4 Kronkorken gesichert!',
actionLabel: 'Danke Hansi! (+4 Kronkorken)',
text: 'Habe die Ehre Sascha! Der Abschied naht... ich breche bald auf nach Münchanien. Aber vorher rocken wir heute Abend auf der Schlossbühne noch das gigantischste Abschiedskonzert der Geschichte! Nimm diese 4 Kronkorken als Proviant!',
actionLabel: 'Auf das Konzert, Hansi! (+4 Kronkorken)',
actionFn: () => {
store.addKronkorken(4)
activeDialog.value = null
@@ -247,7 +275,7 @@ function openCharacterDialog(charId: string) {
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Majestät! Willkommen im innersten VIP-Kreis des Basses! Die magischen Kristalle vibrieren auf 135 BPM und die Lasershow brennt! Hier sind 5 Kronkorken für den Stroboskop-Zauber!',
text: 'Majestät! Willkommen im innersten VIP-Party-Bunker des Basses! Die magischen Kristalle vibrieren auf 135 BPM und die Lasershow brennt! Hier sind 5 Kronkorken für den Stroboskop-Zauber!',
actionLabel: 'Zünde den Kristall-Zauber, Pü! (+5 Kronkorken)',
actionFn: () => {
store.addKronkorken(5)
@@ -273,7 +301,7 @@ function openCharacterDialog(charId: string) {
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Servus König Sascha! Im Hinterzimmer fließt das Helle kälter als die Nacht! Hoch die Krüge auf 40 Jahre Regentschaft!',
text: 'Servus König Sascha! Im Party Bunker fließt das Helle kälter als die Nacht! Hoch die Krüge auf 40 Jahre Regentschaft!',
actionLabel: 'Prost Viktor! (+3 Kronkorken & Helles)',
actionFn: () => {
store.addKronkorken(3)
@@ -290,8 +318,14 @@ function inspectMirror() {
store.unlockEasterEgg('zuluman')
}
function openGstanzlIntro() {
sound.playDialogueBlip()
duelIntroOpen.value = true
}
function startGstanzlDuel() {
sound.playDialogueBlip()
duelIntroOpen.value = false
duelActive.value = true
duelStep.value = 0
duelWon.value = false
@@ -354,16 +388,17 @@ function proceedToExpress() {
<span class="hotspot-badge glow-door">Zum Marktplatz ▶</span>
</button>
<!-- Luke zum Hinterraum (Completely invisible until clicked) -->
<!-- Luke zum Party Bunker (100% invisible until double-clicked) -->
<button
class="hotspot trapdoor-hotspot"
:class="{ 'trap-invisible': !trapdoorDiscovered && !store.unlockedEasterEggs.includes('hinterzimmer') }"
@click="triggerTrapdoor"
:title="trapdoorDiscovered || store.unlockedEasterEggs.includes('hinterzimmer') ? 'Luke zum Hinterraum' : undefined"
@click="handleTrapdoorClick"
@dblclick="triggerTrapdoor"
:title="trapdoorDiscovered || store.unlockedEasterEggs.includes('hinterzimmer') ? 'Luke zum Party Bunker' : undefined"
>
<template v-if="trapdoorDiscovered || store.unlockedEasterEggs.includes('hinterzimmer')">
<span class="trapdoor-icon">🚪</span>
<span class="trapdoor-badge">Luke zum Hinterraum</span>
<span class="trapdoor-badge">Luke zum Party Bunker 🪩</span>
</template>
</button>
</template>
@@ -479,7 +514,7 @@ function proceedToExpress() {
<!-- Door back to Gaststube -->
<button class="hotspot door-hotspot hotspot-door-back-vip" @click="switchRoom('schankraum')">
<span class="door-icon">🚪</span>
<span class="hotspot-badge glow-door">◀ Zurück in die Gaststube</span>
<span class="hotspot-badge glow-door">◀ Zurück in den Schankraum</span>
</button>
</template>
@@ -492,7 +527,7 @@ function proceedToExpress() {
currentRoom === 'nebenraum' ? '🎨 AKT III: KÖNIGLICHER NEBENRAUM & ATELIER 🎨' :
currentRoom === 'marktplatz' ? '🎪 AKT III: HAUMDAUCHER MARKTPLATZ 🎪' :
currentRoom === 'hinterland' ? '🌲 AKT III: ALPINES HINTERLAND 🌲' :
'🪩 AKT III: GEHEIMES VIP-HINTERZIMMER 🪩'
'🪩 AKT III: DER GEHEIME PARTY BUNKER 🪩'
}}
</div>
@@ -532,18 +567,18 @@ function proceedToExpress() {
:class="{ active: currentRoom === 'hinterzimmer' }"
@click="switchRoom('hinterzimmer')"
>
🪩 Hinterzimmer
🪩 Party Bunker
</button>
</div>
<!-- Action Row (Duel & Next Level) -->
<div class="tavern-nav-buttons">
<button class="duel-trigger" @click="startGstanzlDuel">
<button class="duel-trigger" @click="openGstanzlIntro">
⚔️ Gstanzl-Fechten
</button>
<button class="next-level-btn" @click="proceedToExpress">
➡️ Zum Fest-Konvoi!
🎸 Zu Akt IV: Abschiedskonzert!
</button>
</div>
</div>
@@ -586,6 +621,50 @@ function proceedToExpress() {
</div>
</div>
<!-- Gstanzl Intro Modal -->
<div v-if="duelIntroOpen" class="dialog-overlay" @click="duelIntroOpen = false">
<div class="dialog-box intro-modal-box" @click.stop>
<div class="intro-modal-header">
<span class="intro-tag">MINISPIEL INTRO</span>
<button class="duel-close-btn" @click="duelIntroOpen = false" aria-label="Schließen">✕</button>
</div>
<h2 class="intro-modal-title">⚔️ BAYERISCHES GSTANZL-FECHTEN</h2>
<p class="intro-modal-story">
Ein frecher Raubritter zweifelt an Saschas Regentschaft und lästert über 135-BPM-Techno! Zeige ihm, dass die bayerische Zunge schärfer sticht als jedes Schwert!
</p>
<div class="intro-modal-rules">
<div class="rule-line">🎶 <strong>3 Runden:</strong> Wähle den treffendsten bayerischen Reim-Konter.</div>
<div class="rule-line">🏆 <strong>Belohnung:</strong> +15 Kronkorken & 1.500 Punkte für den Highscore!</div>
</div>
<button class="action-btn start-duel-action-btn" @click="startGstanzlDuel">
🎤 Auf geht's zum Gstanzl!
</button>
</div>
</div>
<!-- Wildschwein Duel Intro Modal -->
<div v-if="wildBoarIntroOpen" class="dialog-overlay" @click="wildBoarIntroOpen = false">
<div class="dialog-box intro-modal-box" @click.stop>
<div class="intro-modal-header">
<span class="intro-tag">MINISPIEL INTRO</span>
<button class="duel-close-btn" @click="wildBoarIntroOpen = false" aria-label="Schließen">✕</button>
</div>
<h2 class="intro-modal-title">🐗 WILDSCHWEIN-SCHNICK-SCHNACK-SCHNUCK</h2>
<p class="intro-modal-story">
Hof-Chronist NAT fordert König Sascha zum traditionellen Haumdaucher Dickicht-Duell heraus! Wer beherrscht die Taktik des Waldes?
</p>
<div class="intro-modal-rules">
<div class="rule-line">🐗 <strong>Keiler</strong> bricht durch 🌿 <strong>Dickicht</strong></div>
<div class="rule-line">🌿 <strong>Dickicht</strong> fängt 🏹 <strong>Lanze</strong> ab</div>
<div class="rule-line">🏹 <strong>Lanze</strong> erlegt 🐗 <strong>Keiler</strong></div>
<div class="rule-line">👑 <strong>Format:</strong> Best of 3 (wer zuerst 2 Siege holt!).</div>
</div>
<button class="action-btn start-duel-action-btn" @click="startWildBoarDuel">
⚔️ Duell starten!
</button>
</div>
</div>
<!-- Gstanzl Duel Modal -->
<div v-if="duelActive" class="dialog-overlay">
<div class="dialog-box duel-box">
@@ -660,17 +739,17 @@ function proceedToExpress() {
<!-- Move Choices -->
<div v-if="!wildBoarGameOver" class="boar-choices">
<button class="boar-choice-btn" @click="playWildBoarMove('keiler')">
<button class="boar-choice-btn" @click="playWildBoarMove('keiler', $event)">
<span class="choice-icon">🐗</span>
<span class="choice-name">Keiler</span>
<span class="choice-sub">Bricht Dickicht</span>
</button>
<button class="boar-choice-btn" @click="playWildBoarMove('dickicht')">
<button class="boar-choice-btn" @click="playWildBoarMove('dickicht', $event)">
<span class="choice-icon">🌿</span>
<span class="choice-name">Dickicht</span>
<span class="choice-sub">Fängt Lanze</span>
</button>
<button class="boar-choice-btn" @click="playWildBoarMove('lanze')">
<button class="boar-choice-btn" @click="playWildBoarMove('lanze', $event)">
<span class="choice-icon">🏹</span>
<span class="choice-name">Lanze</span>
<span class="choice-sub">Erlegt Keiler</span>
@@ -776,21 +855,23 @@ function proceedToExpress() {
color: #38bdf8;
}
/* Trapdoor to Hinterzimmer Easter Egg */
/* Trapdoor to Party Bunker Easter Egg */
.trapdoor-hotspot {
position: absolute;
top: 73%;
left: 82%;
background: rgba(180, 83, 9, 0.4);
border: 1.5px dashed #ffd700;
border-radius: 8px;
padding: 3px 6px;
top: 84%;
left: 35%;
transform: translate(-50%, -50%);
background: rgba(212, 175, 55, 0.25);
border: 2px dashed #ffd700;
border-radius: 6px;
padding: 6px 10px;
cursor: pointer;
z-index: 12;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 4px;
gap: 6px;
box-shadow: 0 0 12px rgba(255, 215, 0, 0.4);
}
.trapdoor-hotspot.trap-invisible,
@@ -802,19 +883,19 @@ function proceedToExpress() {
box-shadow: none !important;
outline: none !important;
transform: translate(-50%, -50%) !important;
width: 50px;
height: 50px;
width: 140px;
height: 65px;
padding: 0 !important;
cursor: pointer;
}
.trapdoor-icon {
font-size: 13px;
font-size: 14px;
}
.trapdoor-badge {
font-family: 'Press Start 2P', monospace;
font-size: 5.5px;
font-size: 6px;
color: #fef08a;
white-space: nowrap;
}
@@ -894,7 +975,7 @@ function proceedToExpress() {
.hotspot-door-hinterland { top: 34%; left: 76%; }
/* 3. Hinterland Hotspots */
.hotspot-mane { top: 44%; left: 36%; }
.hotspot-mane { top: 54%; left: 36%; }
.hiking-anim {
animation: hikeBob 1.2s infinite ease-in-out;
}
@@ -909,7 +990,7 @@ function proceedToExpress() {
transform: rotate(20deg);
}
.hotspot-hansi { top: 60%; left: 74%; }
.hotspot-hansi { top: 56%; left: 74%; }
.hotspot-door-back-marktplatz { top: 72%; left: 22%; }
/* 4. Hinterzimmer Hotspots & Strobe */
@@ -1378,13 +1459,26 @@ function proceedToExpress() {
align-items: center;
gap: 3px;
cursor: pointer;
outline: none;
transition: all 0.15s ease;
-webkit-tap-highlight-color: transparent;
}
.boar-choice-btn:hover {
.boar-choice-btn:focus {
outline: none;
}
.boar-choice-btn:active {
transform: scale(0.95);
background: #14532d;
border-color: #4ade80;
transform: translateY(-2px);
}
@media (hover: hover) {
.boar-choice-btn:hover {
background: #14532d;
border-color: #4ade80;
transform: translateY(-2px);
}
}
.choice-icon {
@@ -1430,4 +1524,76 @@ function proceedToExpress() {
background: #16a34a;
border-color: #86efac;
}
/* Intro Modals for Minigames */
.intro-modal-box {
max-width: 320px;
width: 95%;
background: #231209;
border: 3px double #d4af37;
border-radius: 10px;
padding: 14px;
color: #fef3c7;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.95);
}
.intro-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.intro-tag {
font-family: 'Press Start 2P', monospace;
font-size: 6px;
color: #f59e0b;
}
.intro-modal-title {
font-family: 'Press Start 2P', monospace;
font-size: 8.5px;
color: #ffd700;
margin: 0 0 8px 0;
line-height: 1.35;
}
.intro-modal-story {
font-family: 'MedievalSharp', serif;
font-size: 13px;
color: #fef08a;
line-height: 1.35;
margin: 0 0 10px 0;
}
.intro-modal-rules {
background: rgba(0, 0, 0, 0.45);
border-left: 3px solid #ffd700;
border-radius: 4px;
padding: 8px 10px;
margin-bottom: 12px;
display: flex;
flex-direction: column;
gap: 5px;
}
.rule-line {
font-family: 'MedievalSharp', serif;
font-size: 12px;
color: #fed7aa;
line-height: 1.3;
}
.start-duel-action-btn {
width: 100%;
background: #15803d !important;
border-color: #86efac !important;
font-family: 'Press Start 2P', monospace;
font-size: 7.5px;
padding: 10px !important;
}
.start-duel-action-btn:hover {
background: #16a34a !important;
}
</style>
@@ -111,21 +111,48 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
flo_maier: {
id: 'flo_maier',
name: 'Flo Maier',
title: 'Hof-DJ & Sound-Ingenieur',
title: 'Hof-DJ & Feierdeifl',
portrait: '/assets/characters/flo_maier/portrait_v2.jpg',
description: 'Mastermind an den Turntables und Behüter der heiligen 135-BPM-Frequenz.',
famousQuote: 'Dieser Track drückt mehr als die königliche Kutsche!',
description: 'Mastermind an den Turntables und berüchtigter Feierdeifl des Reiches.',
famousQuote: 'Soll i ois ozindn?! A riesigs Freudenfeuer!',
themeColor: '#0284c7'
},
viktor: {
id: 'viktor',
name: 'Viktor',
title: 'Nachtwächter & Party-Ritter',
title: 'Partyvampir & Nachtwächter',
portrait: '/assets/characters/viktor/portrait_v2.jpg',
description: 'Wacht im samtenen Vampir-Umhang über das Nachtleben und sorgt für eiskaltes Helles.',
famousQuote: 'Die Nacht ist jung und der Gerstensaft fließt in Strömen!',
description: 'Wacht im samtenen Vampir-Umhang über das Nachtleben und lauert auf frisches Helles.',
famousQuote: 'Ein Biss, und du ravst für alle Ewigkeit!',
themeColor: '#b91c1c'
},
saskia: {
id: 'saskia',
name: 'Schwester Saskia',
title: 'Die Erstgeborene Schwester',
portrait: '/assets/characters/saskia/portrait.jpg',
description: 'Wurde auf den ersten Turm in den Wolken verschleppt und ruft nach König Sascha!',
famousQuote: 'Sascha, beeil di! Der Turm wackelt im 135-BPM-Takt!',
themeColor: '#f43f5e'
},
maja: {
id: 'maja',
name: 'Schwester Maja',
title: 'Die Zweite Schwester',
portrait: '/assets/characters/maja/portrait.jpg',
description: 'Gefangen in den Wipfeln des Haumdaucher Himmelsgartens.',
famousQuote: 'Hol mi runter, Sascha, i hob an riesen Durst auf a Helles!',
themeColor: '#8b5cf6'
},
miriam: {
id: 'miriam',
name: 'Schwester Miriam',
title: 'Die Jüngste Schwester',
portrait: '/assets/characters/miriam/portrait.jpg',
description: 'Hockt auf den höchsten Himmelszinnen und wartet auf ihren großen Bruder.',
famousQuote: 'Bester Bruder! Alle drei Schwestern gerettet! Jetzt aber zügig zum Schankraum!',
themeColor: '#06b6d4'
},
djs: {
id: 'djs',
name: 'Die Hof-DJs',
@@ -145,3 +172,4 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
themeColor: '#e11d48'
}
}
@@ -1,7 +1,7 @@
export interface DialogueChoice {
text: string
nextId?: string
action?: 'addKronkorken' | 'unlockZuluman' | 'unlockBridgerton' | 'startPlatformer' | 'startExpress' | 'startFinale'
action?: 'addKronkorken' | 'unlockZuluman' | 'unlockBridgerton' | 'startPlatformer' | 'startExpress' | 'startFinale' | 'resetPrologue'
reward?: number
reply?: {
speakerId: string
@@ -13,6 +13,7 @@ export interface DialogueNode {
id: string
speakerId: string
text: string
isGameOver?: boolean
choices: DialogueChoice[]
}
@@ -24,12 +25,12 @@ export const PROLOGUE_DIALOGUES: Record<string, DialogueNode> = {
choices: [
{
text: 'Zefix Josef! Ohne Kronkorken kein Bier, und ohne Bier kein Bass!',
nextId: 'mop_appears',
nextId: 'flo_maier_arrives',
action: 'unlockBridgerton'
},
{
text: 'Bridgerton?! Josef, konzentrier dich! Wo san meine 40 Kronkorken?!',
nextId: 'mop_appears'
nextId: 'flo_maier_arrives'
},
{
text: 'Lass mich raten... Du mit deinen Girls ab und zack, Leergut weg?',
@@ -45,18 +46,72 @@ export const PROLOGUE_DIALOGUES: Record<string, DialogueNode> = {
text: 'Ja mei, woher weißt du das immer?! Wir zogen durch die Lande, die Musik war laut, der Gerstensaft floss in Strömen... und dann hat der Wind die Korken über die Dächer geweht!',
choices: [
{
text: 'Na servus. Dann muss der König wohl selbst ran.',
text: 'Na servus. Mal schauen wer da noch auf dem Balkon lauert...',
nextId: 'flo_maier_arrives'
}
]
},
flo_maier_arrives: {
id: 'flo_maier_arrives',
speakerId: 'flo_maier',
text: 'Habe die Ehre Sascha! Zum 40er zünd i da den ganzen Schlosshof o! A riesigs Freudenfeuer aus Hopfen und Eichenbalken! Des drückt mehr als jeder Bass! Was moanst?!',
choices: [
{
text: 'Ja freilich, zünd ois o, Flo! Freudenfeuer marsch! 🔥',
nextId: 'flo_game_over'
},
{
text: 'Na, spinnst du Feierdeifl?! Mei Schloss bleibt stehn, heid wird friedlich geraved!',
nextId: 'viktor_arrives'
}
]
},
flo_game_over: {
id: 'flo_game_over',
speakerId: 'flo_maier',
text: '💥 WUUUUSCH! Alles steht lichterloh in Flammen! Das Schloss ist Asche, der DJ-Pult geschmolzen und das Bier verdampft. Niemand feiert auf verbrannter Erde!',
isGameOver: true,
choices: [
{
text: '🔄 Löschzug rufen & von vorn beginnen',
action: 'resetPrologue'
}
]
},
viktor_arrives: {
id: 'viktor_arrives',
speakerId: 'viktor',
text: 'Guten Abend, mein König... Wie wär\'s mit einem kleinen Biss in deinen königlichen Hals? Dann ravt die gesamte Sippschaft unsterblich als Partyvampire bis in alle Ewigkeit!',
choices: [
{
text: 'Ja geil, mach mi unsterblich, Viktor! *Hals hinhalten* 🧛',
nextId: 'viktor_game_over'
},
{
text: 'Na danke Viktor, heid gibt\'s nur kühles Helles statt Blut!',
nextId: 'mop_appears'
}
]
},
viktor_game_over: {
id: 'viktor_game_over',
speakerId: 'viktor',
text: '🧛 *CHOMP!* Sascha ist nun ein Partyvampir! Doch ach weh: Es ist strahlender Sonnenschein über Haumdaucher... Du zerfällst zu feinem Glitzerstaub auf den Schlosszinnen!',
isGameOver: true,
choices: [
{
text: '🔄 Knoblauch essen & von vorn beginnen',
action: 'resetPrologue'
}
]
},
mop_appears: {
id: 'mop_appears',
speakerId: 'mop',
text: 'Habe die Ehre, Sascha! Keine Panik, dein Erschaffer hat schon die Sprungfedern in deine königlichen Stiefel programmiert. Sammle mindestens 15 Kronkorken auf den Hopfen-Pfaden, bevor wir in die Taverne einziehen!',
text: 'Habe die Ehre, Sascha! Gut, dass das Schloss noch steht. Aber ein Notruf erreichte die Matrix: Deine drei Schwestern Saskia, Maja und Miriam wurden von Kronkorken-Kobolden hoch in die Wolken entführt! Du musst mit deinen Sprungstiefeln von Plattform zu Plattform klettern und alle drei befreien!',
choices: [
{
text: 'Auf geht\'s Mop! Starte den Kronkorken-Kreuzzug!',
text: 'Auf geht\'s Mop! Meine Schwestern warten – Kronkorken-Sprungfedern an!',
action: 'startPlatformer'
},
{
@@ -68,16 +123,41 @@ export const PROLOGUE_DIALOGUES: Record<string, DialogueNode> = {
sascha_ready: {
id: 'sascha_ready',
speakerId: 'sascha',
text: 'Sonnenbrille sitzt. Hermelinmantel weht im Wind. Haumdaucher, der König kommt!',
text: 'Sonnenbrille sitzt. Hermelinmantel weht im Wind. Saskia, Maja, Miriam – euer königlicher Bruder holt euch da runter!',
choices: [
{
text: 'Level 1 starten: Die Hopfen-Pfade!',
text: 'Akt II starten: Die Schwesternrettung!',
action: 'startPlatformer'
}
]
}
}
export const SISTER_RESCUE_DIALOGUES = {
saskia: {
id: 'rescue_saskia',
speakerId: 'saskia',
name: 'Schwester Saskia',
text: 'Sascha! Du hast mich gerettet! Die Kobolde wollten mich zwingen, Radler zu trinken! Hier, nimm diesen königlichen Kronkorken! Aber beeil dich: Maja ist noch weiter oben im Himmelsgarten gefangen!',
btnText: 'Weiter zu Level 2: Maja retten! ☁️'
},
maja: {
id: 'rescue_maja',
speakerId: 'maja',
name: 'Schwester Maja',
text: 'Königlicher Bruder! Endlich! I hab scho dacht, i verdurste hier oben in den Wolken! Nimm dieses kühle Helle und diesen Kronkorken zur Stärkung. Aber gib Gas: Miriam hockt ganz oben auf den höchsten Zinnen!',
btnText: 'Weiter zu Level 3: Miriam retten! 🏰'
},
miriam: {
id: 'rescue_miriam',
speakerId: 'miriam',
name: 'Schwester Miriam',
text: 'JAAAA! Sascha, bester Bruder aller Zeiten! Alle drei Schwestern sind gerettet und in Sicherheit! Jetzt aber nix wie runter in die Taverne zur königlichen Audienz!',
btnText: 'Zu Akt III: Der Schankraum! 🍻'
}
}
export const GSTANZL_DUEL = [
{
insult: 'Dein Techno-Bass ist so dünn wie schales Exportbier!',
@@ -15,6 +15,7 @@ export interface GameState {
startTime: number
elapsedSeconds: number
soundMuted: boolean
isPaused: boolean
}
export const STORAGE_KEY = 'sasch40_game_save_v1'
@@ -34,6 +35,7 @@ function getInitialState(): GameState {
startTime: Date.now(),
elapsedSeconds: 0,
soundMuted: false,
isPaused: false,
}
if (typeof window === 'undefined' || !window.localStorage) {
@@ -56,7 +58,8 @@ function getInitialState(): GameState {
unlockedEasterEggs: Array.isArray(parsed.unlockedEasterEggs) ? parsed.unlockedEasterEggs : [],
startTime: typeof parsed.startTime === 'number' ? parsed.startTime : defaults.startTime,
elapsedSeconds: typeof parsed.elapsedSeconds === 'number' ? parsed.elapsedSeconds : 0,
soundMuted: Boolean(parsed.soundMuted)
soundMuted: Boolean(parsed.soundMuted),
isPaused: false
}
if (state.soundMuted) {
sound.setMuted(true)
@@ -197,6 +200,14 @@ export const useGameStore = defineStore('game', {
this.soundMuted = sound.toggleMute()
},
pauseGame() {
this.isPaused = true
},
resumeGame() {
this.isPaused = false
},
resetGame() {
if (typeof window !== 'undefined' && window.localStorage) {
try {