feat(sasch40): improve Act III, add state persistence and fix portraits
- Fix character portrait crops and version assets to portrait_v2, ensuring correct identities (resolving Nat/Verena mismatch) - Update Schankraum scene image and configure Nginx cache-control to prevent stale asset caching - Make secret beer tap completely invisible in tavern until clicked - Reposition room hotspots in Act III to eliminate overlapping click targets - Add exit button [✕] to Bavarian Gstanzl duel minigame - Implement localStorage persistence in gameStore to maintain progress across page reloads - Enable clean game state and storage reset when clicking "Nochmal spielen" - Expand automated playtest script to cover Nat dialogue, duel exit, reload persistence, and game restart
This commit is contained in:
@@ -31,6 +31,13 @@ class SoundEngine {
|
||||
return this.isMuted
|
||||
}
|
||||
|
||||
public setMuted(val: boolean) {
|
||||
this.isMuted = val
|
||||
if (this.isMuted) {
|
||||
this.stopBGM()
|
||||
}
|
||||
}
|
||||
|
||||
// SFX: Kronkorken Collect (bright double ding)
|
||||
public playCoin() {
|
||||
if (this.isMuted) return
|
||||
|
||||
@@ -34,7 +34,7 @@ function printOrSave() {
|
||||
|
||||
<div class="king-portrait-wrapper">
|
||||
<img
|
||||
src="/assets/characters/sascha/portrait.jpg"
|
||||
src="/assets/characters/sascha/portrait_v2.jpg"
|
||||
alt="König Sascha I."
|
||||
class="king-portrait"
|
||||
/>
|
||||
|
||||
@@ -23,7 +23,7 @@ function handleStart() {
|
||||
<div class="king-hero">
|
||||
<div class="king-frame">
|
||||
<img
|
||||
src="/assets/characters/sascha/portrait.jpg"
|
||||
src="/assets/characters/sascha/portrait_v2.jpg"
|
||||
alt="König Sascha I."
|
||||
class="king-hero-img"
|
||||
/>
|
||||
|
||||
@@ -26,11 +26,20 @@ const duelStep = ref(0)
|
||||
const duelWon = ref(false)
|
||||
const zulumanMirrorActive = ref(false)
|
||||
|
||||
const secretTapDiscovered = ref(store.unlockedEasterEggs.includes('hinterzimmer'))
|
||||
|
||||
function switchRoom(room: TavernRoom) {
|
||||
sound.playDialogueBlip()
|
||||
currentRoom.value = room
|
||||
}
|
||||
|
||||
function triggerSecretTap() {
|
||||
sound.playFanfare()
|
||||
secretTapDiscovered.value = true
|
||||
store.unlockEasterEgg('hinterzimmer')
|
||||
currentRoom.value = 'hinterzimmer'
|
||||
}
|
||||
|
||||
function enterSecretHinterzimmer() {
|
||||
sound.playFanfare()
|
||||
store.unlockEasterEgg('hinterzimmer')
|
||||
@@ -229,7 +238,7 @@ function proceedToExpress() {
|
||||
|
||||
<!-- ================= 1. GASTSTUBE (SCHANKRAUM) ================= -->
|
||||
<template v-if="currentRoom === 'schankraum'">
|
||||
<img src="/assets/scenes/tavern.jpg" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
|
||||
<img src="/assets/scenes/schankraum_v2.jpg?v=2" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
|
||||
|
||||
<!-- Verena on the wooden gallery -->
|
||||
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
|
||||
@@ -267,10 +276,17 @@ function proceedToExpress() {
|
||||
<span class="hotspot-badge glow-door">Zum Marktplatz ▶</span>
|
||||
</button>
|
||||
|
||||
<!-- SECRET EASTER EGG TRIGGER: Hidden Beer Tap to Hinterzimmer -->
|
||||
<button class="hotspot secret-tap-hotspot" @click="enterSecretHinterzimmer" title="Ein sonderbarer goldener Zapfhahn...">
|
||||
<span class="secret-tap-icon pulse">🗝️</span>
|
||||
<span class="secret-badge">Geheimer Zapfhahn</span>
|
||||
<!-- SECRET EASTER EGG TRIGGER: Hidden Beer Tap to Hinterzimmer (Completely invisible until clicked) -->
|
||||
<button
|
||||
class="hotspot secret-tap-hotspot"
|
||||
:class="{ 'tap-invisible': !secretTapDiscovered && !store.unlockedEasterEggs.includes('hinterzimmer') }"
|
||||
@click="triggerSecretTap"
|
||||
:title="secretTapDiscovered || store.unlockedEasterEggs.includes('hinterzimmer') ? 'Ein sonderbarer Zapfhahn...' : undefined"
|
||||
>
|
||||
<template v-if="secretTapDiscovered || store.unlockedEasterEggs.includes('hinterzimmer')">
|
||||
<span class="secret-tap-icon pulse">🗝️</span>
|
||||
<span class="secret-badge">Geheimer Zapfhahn</span>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -465,8 +481,11 @@ function proceedToExpress() {
|
||||
<div v-if="duelActive" class="dialog-overlay">
|
||||
<div class="dialog-box duel-box">
|
||||
<div class="duel-header">
|
||||
<h3>⚔️ BAYERISCHES BELEIDIGUNGS-GSTANZL</h3>
|
||||
<span class="duel-score">Runde {{ duelStep + 1 }} von 3</span>
|
||||
<div class="duel-title-group">
|
||||
<h3>⚔️ BAYERISCHES BELEIDIGUNGS-GSTANZL</h3>
|
||||
<span class="duel-score">Runde {{ duelStep + 1 }} von 3</span>
|
||||
</div>
|
||||
<button class="duel-close-btn" @click="duelActive = false" aria-label="Minispiel verlassen">✕</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!duelWon">
|
||||
@@ -578,14 +597,30 @@ function proceedToExpress() {
|
||||
/* Secret Tap Easter Egg */
|
||||
.secret-tap-hotspot {
|
||||
position: absolute;
|
||||
top: 73%;
|
||||
left: 88%;
|
||||
top: 72%;
|
||||
left: 82%;
|
||||
background: rgba(234, 179, 8, 0.2);
|
||||
border: 1.5px dashed #ffd700;
|
||||
border-radius: 12px;
|
||||
padding: 3px 6px;
|
||||
cursor: pointer;
|
||||
z-index: 12;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.secret-tap-hotspot.tap-invisible,
|
||||
.secret-tap-hotspot.tap-invisible:hover,
|
||||
.secret-tap-hotspot.tap-invisible:active,
|
||||
.secret-tap-hotspot.tap-invisible:focus {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
transform: translate(-50%, -50%) !important;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding: 0 !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.secret-tap-icon {
|
||||
@@ -599,18 +634,18 @@ function proceedToExpress() {
|
||||
}
|
||||
|
||||
/* 1. Schankraum Hotspots */
|
||||
.hotspot-verena { top: 16%; left: 78%; }
|
||||
.hotspot-armend { top: 62%; left: 24%; }
|
||||
.hotspot-lutz { top: 38%; left: 20%; }
|
||||
.hotspot-amy { top: 52%; left: 80%; }
|
||||
.hotspot-mirror { top: 32%; left: 63%; }
|
||||
.hotspot-door-marktplatz { top: 56%; left: 90%; }
|
||||
.hotspot-verena { top: 17%; left: 28%; }
|
||||
.hotspot-mirror { top: 30%; left: 68%; }
|
||||
.hotspot-amy { top: 45%; left: 32%; }
|
||||
.hotspot-lutz { top: 54%; left: 56%; }
|
||||
.hotspot-armend { top: 62%; left: 18%; }
|
||||
.hotspot-door-marktplatz { top: 50%; left: 84%; }
|
||||
|
||||
/* 2. Marktplatz Hotspots */
|
||||
.mario-container {
|
||||
position: absolute;
|
||||
top: 52%;
|
||||
left: 34%;
|
||||
top: 44%;
|
||||
left: 30%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@@ -664,12 +699,12 @@ function proceedToExpress() {
|
||||
100% { opacity: 1; transform: scale(1.3); }
|
||||
}
|
||||
|
||||
.hotspot-nat { top: 62%; left: 74%; }
|
||||
.hotspot-door-back-gaststube { top: 72%; left: 16%; }
|
||||
.hotspot-door-hinterland { top: 36%; left: 84%; }
|
||||
.hotspot-nat { top: 62%; left: 70%; }
|
||||
.hotspot-door-back-gaststube { top: 72%; left: 20%; }
|
||||
.hotspot-door-hinterland { top: 32%; left: 76%; }
|
||||
|
||||
/* 3. Hinterland Hotspots */
|
||||
.hotspot-mane { top: 50%; left: 42%; }
|
||||
.hotspot-mane { top: 44%; left: 36%; }
|
||||
.hiking-anim {
|
||||
animation: hikeBob 1.2s infinite ease-in-out;
|
||||
}
|
||||
@@ -684,8 +719,8 @@ function proceedToExpress() {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
.hotspot-hansi { top: 66%; left: 78%; }
|
||||
.hotspot-door-back-marktplatz { top: 74%; left: 20%; }
|
||||
.hotspot-hansi { top: 60%; left: 74%; }
|
||||
.hotspot-door-back-marktplatz { top: 72%; left: 22%; }
|
||||
|
||||
/* 4. Hinterzimmer Hotspots & Strobe */
|
||||
.strobe-overlay {
|
||||
@@ -704,24 +739,10 @@ function proceedToExpress() {
|
||||
100% { background: rgba(168, 85, 247, 0.25); }
|
||||
}
|
||||
|
||||
.hotspot-pue { top: 46%; left: 48%; }
|
||||
.rave-pulse {
|
||||
animation: raveGlow 1s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes raveGlow {
|
||||
0% { box-shadow: 0 0 4px #eab308; }
|
||||
100% { box-shadow: 0 0 16px #ec4899; }
|
||||
}
|
||||
|
||||
.crystal-laser {
|
||||
font-size: 13px;
|
||||
animation: pulseAnim 0.8s infinite;
|
||||
}
|
||||
|
||||
.hotspot-flo { top: 40%; left: 22%; }
|
||||
.hotspot-viktor { top: 62%; left: 78%; }
|
||||
.hotspot-door-back-vip { top: 78%; left: 50%; }
|
||||
.hotspot-flo { top: 38%; left: 24%; }
|
||||
.hotspot-viktor { top: 52%; left: 62%; }
|
||||
.hotspot-pue { top: 64%; left: 82%; }
|
||||
.hotspot-door-back-vip { top: 74%; left: 45%; }
|
||||
|
||||
.pulse {
|
||||
animation: pulseAnim 1.5s infinite;
|
||||
@@ -958,11 +979,18 @@ function proceedToExpress() {
|
||||
|
||||
/* Gstanzl */
|
||||
.duel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid #78350f;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.duel-title-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.duel-header h3 {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 8px;
|
||||
@@ -975,6 +1003,31 @@ function proceedToExpress() {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.duel-close-btn {
|
||||
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;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.duel-close-btn:hover {
|
||||
background: #dc2626;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
|
||||
}
|
||||
|
||||
.insult-bubble {
|
||||
background: #451a03;
|
||||
border-left: 3px solid #ef4444;
|
||||
|
||||
@@ -13,7 +13,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'sascha',
|
||||
name: 'König Sascha I.',
|
||||
title: 'Der 40-jährige Imperator',
|
||||
portrait: '/assets/characters/sascha/portrait.jpg',
|
||||
portrait: '/assets/characters/sascha/portrait_v2.jpg',
|
||||
description: 'Herrscher über Haumdaucher, Träger der heiligen Sonnenbrille und Beschützer des 135-BPM-Basses.',
|
||||
famousQuote: 'O zapft is im Reiche! Wo san meine Kronkorken?!',
|
||||
themeColor: '#d4af37'
|
||||
@@ -22,7 +22,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'mop',
|
||||
name: 'Mop (Moritz)',
|
||||
title: 'Der Welten-Erschaffer & Matrix-Architekt',
|
||||
portrait: '/assets/characters/mop/portrait.jpg',
|
||||
portrait: '/assets/characters/mop/portrait_v2.jpg',
|
||||
description: 'Sitzt auf einer Wolke über dem Schloss und baut aus Pixeln und Hopfen diese Welt.',
|
||||
famousQuote: 'Just adding a few more polygons here... wait, pixels. Pixels.',
|
||||
themeColor: '#38bdf8'
|
||||
@@ -31,7 +31,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'nat',
|
||||
name: 'NAT (Josef)',
|
||||
title: 'Hof-Chronist & Barde des Wildschweins',
|
||||
portrait: '/assets/characters/nat/portrait.jpg',
|
||||
portrait: '/assets/characters/nat/portrait_v2.jpg',
|
||||
description: 'Kennt alle 3 Staffeln Bridgerton in- und auswendig, trägt florale Bomberjacken und erzählt epische Geschichten.',
|
||||
famousQuote: 'Also pass auf Sascha... ich damals mit meinen Girls ab...',
|
||||
themeColor: '#22c55e'
|
||||
@@ -40,7 +40,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'verena',
|
||||
name: 'Königin Verena',
|
||||
title: 'Herrin des Hofes',
|
||||
portrait: '/assets/characters/verena/portrait.jpg',
|
||||
portrait: '/assets/characters/verena/portrait_v2.jpg',
|
||||
description: 'Liebt Sascha über alles auf der Welt, hat aber ein unumstößliches königliches Edikt erlassen.',
|
||||
famousQuote: 'Ich hab dich lieb mein Schatz, aber ich putze nach der Party fei ned!',
|
||||
themeColor: '#ec4899'
|
||||
@@ -49,7 +49,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'hansi',
|
||||
name: 'Hansi (Johann)',
|
||||
title: 'Ritter der floralen Garde',
|
||||
portrait: '/assets/characters/hansi/portrait.jpg',
|
||||
portrait: '/assets/characters/hansi/portrait_v2.jpg',
|
||||
description: 'Perfekt gestylter Schnurrbart und florale Plattenrüstung.',
|
||||
famousQuote: 'Habe die Ehre! Der Bass im Rittersaal ist auf Punkt.',
|
||||
themeColor: '#6366f1'
|
||||
@@ -58,7 +58,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'lutz',
|
||||
name: 'Lutz',
|
||||
title: 'Der ZTUL-Hofschornsteinfeger',
|
||||
portrait: '/assets/characters/lutz/portrait.jpg',
|
||||
portrait: '/assets/characters/lutz/portrait_v2.jpg',
|
||||
description: 'Ein Hüne von Gestalt mit Kappe, runder Brille und dem legendären ZTUL-Trikot.',
|
||||
famousQuote: 'Glück auf, Euer Durchlaucht! Schornstein ist frei für den Party-Nebel.',
|
||||
themeColor: '#64748b'
|
||||
@@ -67,7 +67,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'armend',
|
||||
name: 'Armend',
|
||||
title: 'Meister-Schmied der Kronkorken-Manufaktur',
|
||||
portrait: '/assets/characters/armend/portrait.jpg',
|
||||
portrait: '/assets/characters/armend/portrait_v2.jpg',
|
||||
description: 'Prachtvoller Vollbart, formt aus 40 Kronkorken den goldenen Sound-Verstärker.',
|
||||
famousQuote: 'Schlag zu, solange das Blech warm ist!',
|
||||
themeColor: '#f97316'
|
||||
@@ -76,7 +76,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'amy',
|
||||
name: 'Amy (Amelie)',
|
||||
title: 'Die Hof-Künstlerin',
|
||||
portrait: '/assets/characters/amy/portrait.jpg',
|
||||
portrait: '/assets/characters/amy/portrait_v2.jpg',
|
||||
description: 'Kreatives Genie in Latzhose mit Dutt und feinen Kräuter-Zigarren.',
|
||||
famousQuote: 'Rave the Kingdom! Die Farben brauchen mehr Bass!',
|
||||
themeColor: '#a855f7'
|
||||
@@ -85,7 +85,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'pue',
|
||||
name: 'Pü (Jakob)',
|
||||
title: 'Lichtmeister & Kristall-Rave-Magier',
|
||||
portrait: '/assets/characters/pue/portrait.jpg',
|
||||
portrait: '/assets/characters/pue/portrait_v2.jpg',
|
||||
description: 'Lässt im geheimen Hinterzimmer die Disco-Kristalle tanzen und erhellt die Entourage mit bunten Lasern.',
|
||||
famousQuote: 'Bass auf Anschlag, Kristalle auf Stroboskop! Willkommen im geheimen Hinterzimmer!',
|
||||
themeColor: '#eab308'
|
||||
@@ -94,7 +94,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'mario',
|
||||
name: 'Mario',
|
||||
title: 'Hof-Gaukler & Kettensägen-Virtuose',
|
||||
portrait: '/assets/characters/mario/portrait.jpg',
|
||||
portrait: '/assets/characters/mario/portrait_v2.jpg',
|
||||
description: 'Jongliert furchtlos auf dem Marktplatz mit kreischenden Kettensägen und Hopfenkrügen.',
|
||||
famousQuote: 'Pass auf die Zinken auf! Kettensägen-Akrobatik für König Sascha!',
|
||||
themeColor: '#14b8a6'
|
||||
@@ -103,7 +103,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'mane',
|
||||
name: 'Mane (Ramona)',
|
||||
title: 'Königliche Alpen-Wanderin',
|
||||
portrait: '/assets/characters/mane/portrait.jpg',
|
||||
portrait: '/assets/characters/mane/portrait_v2.jpg',
|
||||
description: 'Zieht mit Wanderstock, Sonnenbrille und Flechtzopf durchs alpine Hinterland von Haumdaucher.',
|
||||
famousQuote: 'Schee ists hier! Noch 3 Kilometer über den Grat bis zum Gipfel-Bier!',
|
||||
themeColor: '#84cc16'
|
||||
@@ -112,7 +112,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'flo_maier',
|
||||
name: 'Flo Maier',
|
||||
title: 'Hof-DJ & Sound-Ingenieur',
|
||||
portrait: '/assets/characters/flo_maier/portrait.jpg',
|
||||
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!',
|
||||
themeColor: '#0284c7'
|
||||
@@ -121,7 +121,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'viktor',
|
||||
name: 'Viktor',
|
||||
title: 'Nachtwächter & Party-Ritter',
|
||||
portrait: '/assets/characters/viktor/portrait.jpg',
|
||||
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!',
|
||||
themeColor: '#b91c1c'
|
||||
@@ -130,7 +130,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'djs',
|
||||
name: 'Die Hof-DJs',
|
||||
title: 'Minnesänger der 135 BPM',
|
||||
portrait: '/assets/characters/flo_maier/portrait.jpg',
|
||||
portrait: '/assets/characters/flo_maier/portrait_v2.jpg',
|
||||
description: 'Stehen an der königlichen Kanzel am doppelten Eichenholz-Plattenteller.',
|
||||
famousQuote: 'Drop it like it\'s 1426! Mehr Bass auf die Burgzinnen!',
|
||||
themeColor: '#06b6d4'
|
||||
@@ -139,7 +139,7 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'gefolge',
|
||||
name: 'Das Haumdaucher Gefolge',
|
||||
title: 'Die feiernde Reichs-Crowd',
|
||||
portrait: '/assets/characters/viktor/portrait.jpg',
|
||||
portrait: '/assets/characters/viktor/portrait_v2.jpg',
|
||||
description: 'Viktor der Vampir-Ritter, DJ Flo Maier, Hansi, Fabian, Albin, der bärtige Guatemalteke, Markus & Nathalie.',
|
||||
famousQuote: 'Ein Hoch auf König Sascha! Hoch soll er leben!',
|
||||
themeColor: '#e11d48'
|
||||
|
||||
@@ -4,5 +4,13 @@ import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
|
||||
import { useGameStore, saveStateToStorage } from './stores/gameStore'
|
||||
const store = useGameStore()
|
||||
store.$subscribe((_mutation, state) => {
|
||||
saveStateToStorage(state)
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -17,8 +17,12 @@ export interface GameState {
|
||||
soundMuted: boolean
|
||||
}
|
||||
|
||||
export const useGameStore = defineStore('game', {
|
||||
state: (): GameState => ({
|
||||
export const STORAGE_KEY = 'sasch40_game_save_v1'
|
||||
|
||||
const VALID_ACTS: GameAct[] = ['intro', 'prologue', 'platformer', 'tavern', 'party_express', 'finale']
|
||||
|
||||
function getInitialState(): GameState {
|
||||
const defaults: GameState = {
|
||||
currentAct: 'intro',
|
||||
kronkorken: 0,
|
||||
beers: 0,
|
||||
@@ -30,7 +34,66 @@ export const useGameStore = defineStore('game', {
|
||||
startTime: Date.now(),
|
||||
elapsedSeconds: 0,
|
||||
soundMuted: false,
|
||||
}),
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined' || !window.localStorage) {
|
||||
return defaults
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return defaults
|
||||
const parsed = JSON.parse(raw)
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
const state: GameState = {
|
||||
currentAct: VALID_ACTS.includes(parsed.currentAct) ? parsed.currentAct : 'intro',
|
||||
kronkorken: typeof parsed.kronkorken === 'number' && !isNaN(parsed.kronkorken) ? parsed.kronkorken : 0,
|
||||
beers: typeof parsed.beers === 'number' && !isNaN(parsed.beers) ? parsed.beers : 0,
|
||||
guestsServed: typeof parsed.guestsServed === 'number' && !isNaN(parsed.guestsServed) ? parsed.guestsServed : 0,
|
||||
gstanzlWon: Boolean(parsed.gstanzlWon),
|
||||
partyMood: typeof parsed.partyMood === 'number' && !isNaN(parsed.partyMood) ? parsed.partyMood : 20,
|
||||
inventory: Array.isArray(parsed.inventory) ? parsed.inventory : defaults.inventory,
|
||||
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)
|
||||
}
|
||||
if (state.soundMuted) {
|
||||
sound.setMuted(true)
|
||||
}
|
||||
return state
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse saved game state:', e)
|
||||
}
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
export function saveStateToStorage(state: GameState) {
|
||||
if (typeof window === 'undefined' || !window.localStorage) return
|
||||
try {
|
||||
const dataToSave = {
|
||||
currentAct: state.currentAct,
|
||||
kronkorken: state.kronkorken,
|
||||
beers: state.beers,
|
||||
guestsServed: state.guestsServed,
|
||||
gstanzlWon: state.gstanzlWon,
|
||||
partyMood: state.partyMood,
|
||||
inventory: state.inventory,
|
||||
unlockedEasterEggs: state.unlockedEasterEggs,
|
||||
startTime: state.startTime,
|
||||
elapsedSeconds: state.elapsedSeconds,
|
||||
soundMuted: state.soundMuted
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToSave))
|
||||
} catch (e) {
|
||||
console.warn('Failed to save game state to localStorage:', e)
|
||||
}
|
||||
}
|
||||
|
||||
export const useGameStore = defineStore('game', {
|
||||
state: (): GameState => getInitialState(),
|
||||
|
||||
getters: {
|
||||
isArcadeMode(state): boolean {
|
||||
@@ -135,6 +198,13 @@ export const useGameStore = defineStore('game', {
|
||||
},
|
||||
|
||||
resetGame() {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
} catch (e) {
|
||||
console.warn('Failed to clear saved state', e)
|
||||
}
|
||||
}
|
||||
this.currentAct = 'intro'
|
||||
this.kronkorken = 0
|
||||
this.beers = 0
|
||||
@@ -145,6 +215,7 @@ export const useGameStore = defineStore('game', {
|
||||
this.unlockedEasterEggs = []
|
||||
this.startTime = Date.now()
|
||||
this.elapsedSeconds = 0
|
||||
saveStateToStorage(this.$state)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user