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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user