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
@@ -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;