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:
Moritz Graf
2026-09-24 11:03:22 +02:00
parent 7a7038e2c2
commit e45dbc2e19
34 changed files with 363 additions and 78 deletions
@@ -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)
}
}
})