feat(sasch40): multi-room tavern, authentic character portraits, pre-level start screens & bollerwagen redesign

This commit is contained in:
Moritz Graf
2026-09-24 10:02:57 +02:00
parent 4488749d6f
commit 7a7038e2c2
26 changed files with 1243 additions and 334 deletions
@@ -7,6 +7,9 @@ import { sound } from '../audio/synthAudio'
const store = useGameStore()
type TavernRoom = 'schankraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer'
const currentRoom = ref<TavernRoom>('schankraum')
interface ActiveDialog {
characterId: string
name: string
@@ -23,6 +26,17 @@ const duelStep = ref(0)
const duelWon = ref(false)
const zulumanMirrorActive = ref(false)
function switchRoom(room: TavernRoom) {
sound.playDialogueBlip()
currentRoom.value = room
}
function enterSecretHinterzimmer() {
sound.playFanfare()
store.unlockEasterEgg('hinterzimmer')
currentRoom.value = 'hinterzimmer'
}
function openCharacterDialog(charId: string) {
const char = CHARACTERS[charId]
if (!char) return
@@ -81,42 +95,16 @@ function openCharacterDialog(charId: string) {
activeDialog.value = null
}
}
} else if (charId === 'pue') {
activeDialog.value = {
characterId: charId,
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Majestät! Die magischen Kristall-Fackeln sind verkabelt und synchronisiert. Sobald der Drop bei 135 BPM einschlägt, brennt der Saal!',
actionLabel: 'Zünde den Stroboskop-Zauber!',
actionFn: () => {
store.addKronkorken(4)
activeDialog.value = null
}
}
} else if (charId === 'mane') {
activeDialog.value = {
characterId: charId,
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Servus beinand! Ich wandere grad gemütlich durch die Schenke. Wander-Tipp: Wenn du nachher auf den Festwagen steigst, halte dich an die Kronkorken-Spur!',
actionLabel: 'Danke Mane! Berg heil!',
actionFn: () => {
store.addKronkorken(2)
activeDialog.value = null
}
}
} else if (charId === 'mario') {
activeDialog.value = {
characterId: charId,
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Hoppla die Waldfee! Schau wie die Bierkrüge und Hopfendolden durch die Luft wirbeln! Ein Hoch auf König Sascha!',
actionLabel: 'Sauber jongliert, Mario! (+3 Kronkorken)',
text: 'Hoppla die Waldfee! Schau wie die kreischenden Kettensägen und Hopfenkrüge durch die Luft wirbeln! Keine Sorge Majestät, die Ketten sind frisch geölt! Nimm diese 4 Meister-Korken!',
actionLabel: 'Sauber jongliert, Mario! (+4 Kronkorken)',
actionFn: () => {
store.addKronkorken(3)
store.addKronkorken(4)
activeDialog.value = null
}
}
@@ -134,6 +122,72 @@ function openCharacterDialog(charId: string) {
activeDialog.value = null
}
}
} else if (charId === 'mane') {
activeDialog.value = {
characterId: charId,
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Servus beinand! Ich wandere grad über den Haumdaucher Höhenweg. Wander-Tipp: Wer rastet, der rostet – und wer nicht tanzt, kriegt keinen Schaum auf die Halbe! Hier, 4 Kronkorken für den Gipfelsturm!',
actionLabel: 'Danke Mane! Berg heil! (+4 Kronkorken)',
actionFn: () => {
store.addKronkorken(4)
activeDialog.value = null
}
}
} else if (charId === 'hansi') {
activeDialog.value = {
characterId: charId,
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)',
actionFn: () => {
store.addKronkorken(4)
activeDialog.value = null
}
}
} else if (charId === 'pue') {
activeDialog.value = {
characterId: charId,
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!',
actionLabel: 'Zünde den Kristall-Zauber, Pü! (+5 Kronkorken)',
actionFn: () => {
store.addKronkorken(5)
activeDialog.value = null
}
}
} else if (charId === 'flo_maier') {
activeDialog.value = {
characterId: charId,
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Habe die Ehre Sascha! Die Monitore wummern mit 135 BPM! Ich hab dir eine geheime Bass-Platte aufgelegt – hier sind 5 Meister-Kronkorken für den nächsten Drop!',
actionLabel: 'Dreh die Bässe auf, Flo! (+5 Kronkorken)',
actionFn: () => {
store.addKronkorken(5)
activeDialog.value = null
}
}
} else if (charId === 'viktor') {
activeDialog.value = {
characterId: charId,
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!',
actionLabel: 'Prost Viktor! (+3 Kronkorken & Helles)',
actionFn: () => {
store.addKronkorken(3)
store.addBeer()
activeDialog.value = null
}
}
}
}
@@ -172,57 +226,192 @@ function proceedToExpress() {
<template>
<div class="tavern-container">
<div class="scene-viewport">
<img src="/assets/scenes/tavern.jpg" alt="Taverne Zum wummernden Ritter" class="tavern-bg" />
<!-- Interactive Hotspots on the visual scene with Avatar Thumbnails -->
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
<img :src="CHARACTERS.verena.portrait" class="hotspot-thumb" alt="Königin Verena" />
<span class="hotspot-badge">Verena 👑</span>
</button>
<!-- ================= 1. GASTSTUBE (SCHANKRAUM) ================= -->
<template v-if="currentRoom === 'schankraum'">
<img src="/assets/scenes/tavern.jpg" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
<button class="hotspot hotspot-armend" @click="openCharacterDialog('armend')">
<img :src="CHARACTERS.armend.portrait" class="hotspot-thumb" alt="Armend" />
<span class="hotspot-badge">Armend 🔨</span>
</button>
<!-- Verena on the wooden gallery -->
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
<img :src="CHARACTERS.verena.portrait" class="hotspot-thumb" alt="Königin Verena" />
<span class="hotspot-badge">Verena 👑</span>
</button>
<button class="hotspot hotspot-lutz" @click="openCharacterDialog('lutz')">
<img :src="CHARACTERS.lutz.portrait" class="hotspot-thumb" alt="Lutz" />
<span class="hotspot-badge">Lutz (ZTUL) 🎩</span>
</button>
<!-- Armend at the forge/anvil -->
<button class="hotspot hotspot-armend" @click="openCharacterDialog('armend')">
<img :src="CHARACTERS.armend.portrait" class="hotspot-thumb" alt="Armend" />
<span class="hotspot-badge">Armend 🔨</span>
</button>
<button class="hotspot hotspot-amy" @click="openCharacterDialog('amy')">
<img :src="CHARACTERS.amy.portrait" class="hotspot-thumb" alt="Amy" />
<span class="hotspot-badge">Amy 🎨</span>
</button>
<!-- Lutz at the fireplace -->
<button class="hotspot hotspot-lutz" @click="openCharacterDialog('lutz')">
<img :src="CHARACTERS.lutz.portrait" class="hotspot-thumb" alt="Lutz" />
<span class="hotspot-badge">Lutz (ZTUL) 🎩</span>
</button>
<button class="hotspot hotspot-mirror" @click="inspectMirror">
<span class="mirror-icon">🪞</span>
<span class="hotspot-badge pulse">Zuluman-Spiegel</span>
</button>
<!-- Amy at the tavern table -->
<button class="hotspot hotspot-amy" @click="openCharacterDialog('amy')">
<img :src="CHARACTERS.amy.portrait" class="hotspot-thumb" alt="Amy" />
<span class="hotspot-badge">Amy 🎨</span>
</button>
<button class="hotspot hotspot-pue" @click="openCharacterDialog('pue')">
<img :src="CHARACTERS.pue.portrait" class="hotspot-thumb" alt="Pü" />
<span class="hotspot-badge">Pü (Licht) 💡</span>
</button>
<!-- Zuluman Magic Mirror on the pillar -->
<button class="hotspot hotspot-mirror" @click="inspectMirror">
<span class="mirror-icon">🪞</span>
<span class="hotspot-badge pulse">Zuluman-Spiegel</span>
</button>
<button class="hotspot hotspot-mane" @click="openCharacterDialog('mane')">
<img :src="CHARACTERS.mane.portrait" class="hotspot-thumb" alt="Mane" />
<span class="hotspot-badge">Mane 🥾</span>
</button>
<!-- Doorway to Marktplatz -->
<button class="hotspot door-hotspot hotspot-door-marktplatz" @click="switchRoom('marktplatz')">
<span class="door-icon">🚪</span>
<span class="hotspot-badge glow-door">Zum Marktplatz ▶</span>
</button>
<button class="hotspot hotspot-nat" @click="openCharacterDialog('nat')">
<img :src="CHARACTERS.nat.portrait" class="hotspot-thumb" alt="NAT" />
<span class="hotspot-badge">NAT 🐗</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>
</button>
</template>
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
<img :src="CHARACTERS.mario.portrait" class="hotspot-thumb" alt="Mario" />
<span class="hotspot-badge">Mario 🤹</span>
</button>
<!-- ================= 2. MARKTPLATZ ================= -->
<template v-else-if="currentRoom === 'marktplatz'">
<img src="/assets/scenes/marktplatz.jpg" alt="Marktplatz Haumdaucher" class="tavern-bg" />
<!-- Gaukler Mario juggling chainsaws with animations & flying sparks -->
<div class="mario-container">
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
<div class="chainsaw-circus">
<span class="saw saw-left">🪚</span>
<span class="saw saw-right">🪚</span>
<span class="spark spark-1">⚡</span>
<span class="spark spark-2">✨</span>
</div>
<img :src="CHARACTERS.mario.portrait" class="hotspot-thumb" alt="Mario" />
<span class="hotspot-badge">Mario (Kettensägen!) 🤹</span>
</button>
</div>
<!-- NAT at the fountain -->
<button class="hotspot hotspot-nat" @click="openCharacterDialog('nat')">
<img :src="CHARACTERS.nat.portrait" class="hotspot-thumb" alt="NAT" />
<span class="hotspot-badge">NAT (Josef) 🐗</span>
</button>
<!-- Door back to Gaststube -->
<button class="hotspot door-hotspot hotspot-door-back-gaststube" @click="switchRoom('schankraum')">
<span class="door-icon">🚪</span>
<span class="hotspot-badge glow-door">◀ Zur Gaststube</span>
</button>
<!-- Trail to Hinterland -->
<button class="hotspot door-hotspot hotspot-door-hinterland" @click="switchRoom('hinterland')">
<span class="door-icon">🌲</span>
<span class="hotspot-badge glow-door">Zum Hinterland ▶</span>
</button>
</template>
<!-- ================= 3. HINTERLAND ================= -->
<template v-else-if="currentRoom === 'hinterland'">
<img src="/assets/scenes/hinterland.jpg" alt="Hinterland Haumdaucher Alm" class="tavern-bg" />
<!-- Wanderin Mane (Ramona) hiking with animated stride -->
<button class="hotspot hotspot-mane hiking-anim" @click="openCharacterDialog('mane')">
<span class="hiking-stick">🦯</span>
<img :src="CHARACTERS.mane.portrait" class="hotspot-thumb" alt="Mane" />
<span class="hotspot-badge">Mane (Wanderin) 🥾</span>
</button>
<!-- Hansi enjoying the view on the bench -->
<button class="hotspot hotspot-hansi" @click="openCharacterDialog('hansi')">
<img :src="CHARACTERS.hansi.portrait" class="hotspot-thumb" alt="Hansi" />
<span class="hotspot-badge">Hansi 🧔</span>
</button>
<!-- Trail back to Marktplatz -->
<button class="hotspot door-hotspot hotspot-door-back-marktplatz" @click="switchRoom('marktplatz')">
<span class="door-icon">🎪</span>
<span class="hotspot-badge glow-door">◀ Zum Marktplatz</span>
</button>
</template>
<!-- ================= 4. HINTERZIMMER (SECRET VIP RAVE) ================= -->
<template v-else-if="currentRoom === 'hinterzimmer'">
<img src="/assets/scenes/hinterzimmer.jpg" alt="Geheimes VIP Hinterzimmer" class="tavern-bg" />
<div class="strobe-overlay"></div>
<!-- Pü with pulsing crystal staff and disco lasers -->
<button class="hotspot hotspot-pue rave-pulse" @click="openCharacterDialog('pue')">
<span class="crystal-laser">💎✨</span>
<img :src="CHARACTERS.pue.portrait" class="hotspot-thumb" alt="Pü" />
<span class="hotspot-badge">Pü (Kristall-Zauber) 💡</span>
</button>
<!-- Flo Maier at mixing deck -->
<button class="hotspot hotspot-flo" @click="openCharacterDialog('flo_maier')">
<img :src="CHARACTERS.flo_maier.portrait" class="hotspot-thumb" alt="Flo Maier" />
<span class="hotspot-badge">DJ Flo Maier 🎧</span>
</button>
<!-- Viktor the vampire night-guard -->
<button class="hotspot hotspot-viktor" @click="openCharacterDialog('viktor')">
<img :src="CHARACTERS.viktor.portrait" class="hotspot-thumb" alt="Viktor" />
<span class="hotspot-badge">Viktor 🧛</span>
</button>
<!-- 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>
</button>
</template>
<!-- Bottom Scene Navigation & Duel -->
<div class="scene-bottom-nav">
<div class="tavern-bar-title">🍻 TAVERNE ZUM WUMMERNDEN RITTER 🍻</div>
<!-- Room Title Badge -->
<div class="tavern-bar-title">
{{
currentRoom === 'schankraum' ? '🍻 AKT III: GASTSTUBE ZUM WUMMERNDEN RITTER 🍻' :
currentRoom === 'marktplatz' ? '🎪 AKT III: HAUMDAUCHER MARKTPLATZ 🎪' :
currentRoom === 'hinterland' ? '🌲 AKT III: ALPINES HINTERLAND 🌲' :
'🪩 AKT III: GEHEIMES VIP-HINTERZIMMER 🪩'
}}
</div>
<!-- Quick Room Navigation Tabs -->
<div class="room-nav-tabs">
<button
class="room-tab"
:class="{ active: currentRoom === 'schankraum' }"
@click="switchRoom('schankraum')"
>
🍺 Gaststube
</button>
<button
class="room-tab"
:class="{ active: currentRoom === 'marktplatz' }"
@click="switchRoom('marktplatz')"
>
🎪 Marktplatz
</button>
<button
class="room-tab"
:class="{ active: currentRoom === 'hinterland' }"
@click="switchRoom('hinterland')"
>
🌲 Hinterland
</button>
<button
v-if="store.unlockedEasterEggs.includes('hinterzimmer') || currentRoom === 'hinterzimmer'"
class="room-tab vip-tab"
:class="{ active: currentRoom === 'hinterzimmer' }"
@click="switchRoom('hinterzimmer')"
>
🪩 Hinterzimmer
</button>
</div>
<!-- Action Row (Duel & Next Level) -->
<div class="tavern-nav-buttons">
<button class="duel-trigger" @click="startGstanzlDuel">
⚔️ Gstanzl-Fechten
@@ -338,7 +527,7 @@ function proceedToExpress() {
display: flex;
align-items: center;
gap: 4px;
background: rgba(26, 15, 10, 0.85);
background: rgba(26, 15, 10, 0.88);
border: 1.5px solid #ffd700;
border-radius: 16px;
padding: 2px 6px 2px 2px;
@@ -363,8 +552,8 @@ function proceedToExpress() {
object-fit: cover;
}
.mirror-icon {
font-size: 16px;
.mirror-icon, .door-icon {
font-size: 15px;
padding: 0 2px;
}
@@ -375,15 +564,164 @@ function proceedToExpress() {
white-space: nowrap;
}
.hotspot-verena { top: 15%; left: 80%; }
/* Door Hotspots */
.door-hotspot {
background: rgba(15, 23, 42, 0.9);
border: 1.5px solid #38bdf8;
padding: 4px 8px;
}
.glow-door {
color: #38bdf8;
}
/* Secret Tap Easter Egg */
.secret-tap-hotspot {
position: absolute;
top: 73%;
left: 88%;
background: rgba(234, 179, 8, 0.2);
border: 1.5px dashed #ffd700;
border-radius: 12px;
padding: 3px 6px;
cursor: pointer;
z-index: 12;
}
.secret-tap-icon {
font-size: 13px;
}
.secret-badge {
font-family: 'Press Start 2P', monospace;
font-size: 5.5px;
color: #fef08a;
}
/* 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-amy { top: 52%; left: 82%; }
.hotspot-pue { top: 20%; left: 35%; }
.hotspot-mane { top: 45%; left: 66%; }
.hotspot-nat { top: 63%; left: 85%; }
.hotspot-mario { top: 36%; left: 90%; }
.hotspot-door-marktplatz { top: 56%; left: 90%; }
/* 2. Marktplatz Hotspots */
.mario-container {
position: absolute;
top: 52%;
left: 34%;
z-index: 10;
}
.hotspot-mario {
position: relative;
transform: translate(-50%, -50%);
}
.chainsaw-circus {
position: absolute;
top: -24px;
left: 50%;
transform: translateX(-50%);
width: 50px;
height: 24px;
pointer-events: none;
}
.saw {
position: absolute;
font-size: 14px;
animation: spinSaw 0.3s infinite linear;
}
.saw-left {
left: -4px;
animation-direction: normal;
}
.saw-right {
right: -4px;
animation-direction: reverse;
}
.spark {
position: absolute;
font-size: 10px;
animation: sparkFlicker 0.2s infinite alternate;
}
.spark-1 { top: -6px; left: 10px; color: #f59e0b; }
.spark-2 { top: 4px; right: 10px; color: #ef4444; }
@keyframes spinSaw {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes sparkFlicker {
0% { opacity: 0.2; transform: scale(0.8); }
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%; }
/* 3. Hinterland Hotspots */
.hotspot-mane { top: 50%; left: 42%; }
.hiking-anim {
animation: hikeBob 1.2s infinite ease-in-out;
}
@keyframes hikeBob {
0%, 100% { transform: translate(-50%, -50%) translateY(0); }
50% { transform: translate(-50%, -50%) translateY(-6px) rotate(-2deg); }
}
.hiking-stick {
font-size: 12px;
transform: rotate(20deg);
}
.hotspot-hansi { top: 66%; left: 78%; }
.hotspot-door-back-marktplatz { top: 74%; left: 20%; }
/* 4. Hinterzimmer Hotspots & Strobe */
.strobe-overlay {
position: absolute;
inset: 0;
pointer-events: none;
mix-blend-mode: overlay;
animation: discoFlash 0.6s infinite alternate;
z-index: 5;
}
@keyframes discoFlash {
0% { background: rgba(236, 72, 153, 0.25); }
33% { background: rgba(59, 130, 246, 0.25); }
66% { background: rgba(234, 179, 8, 0.25); }
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%; }
.pulse {
animation: pulseAnim 1.5s infinite;
@@ -394,12 +732,13 @@ function proceedToExpress() {
50% { box-shadow: 0 0 12px #ec4899; }
}
/* Bottom Navigation */
.scene-bottom-nav {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 84px;
height: 94px;
background: linear-gradient(180deg, #451a03 0%, #26140b 100%);
border-top: 3px solid #b45309;
box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.95);
@@ -407,20 +746,53 @@ function proceedToExpress() {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 12px;
gap: 5px;
padding: 6px 10px;
z-index: 15;
}
.tavern-bar-title {
font-family: 'Press Start 2P', monospace;
font-size: 7px;
font-size: 6.5px;
color: #fef08a;
text-shadow: 1px 1px 0 #000;
letter-spacing: 0.5px;
letter-spacing: 0.3px;
text-align: center;
}
.room-nav-tabs {
display: flex;
gap: 4px;
width: 100%;
justify-content: center;
}
.room-tab {
background: #2b1810;
border: 1px solid #78350f;
color: #cbd5e1;
font-family: 'Press Start 2P', monospace;
font-size: 6px;
padding: 4px 6px;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
.room-tab.active {
background: #b45309;
border-color: #ffd700;
color: #fff;
box-shadow: 0 0 6px rgba(255, 215, 0, 0.4);
}
.vip-tab {
background: #3b0764;
border-color: #a855f7;
color: #f0abfc;
animation: pulseAnim 1.5s infinite;
}
.tavern-nav-buttons {
display: flex;
width: 100%;
@@ -435,8 +807,8 @@ function proceedToExpress() {
border: 1.5px solid #ffd700;
border-radius: 6px;
font-family: 'Press Start 2P', monospace;
font-size: 7.5px;
padding: 8px 10px;
font-size: 7px;
padding: 6px 8px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
}
@@ -447,8 +819,8 @@ function proceedToExpress() {
border: 1.5px solid #86efac;
border-radius: 6px;
font-family: 'Press Start 2P', monospace;
font-size: 7.5px;
padding: 8px 10px;
font-size: 7px;
padding: 6px 8px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.6);
}