feat(sasch40): add Nebenraum, Marktplatz minigame & trapdoor to Hinterzimmer

- Split GameBoy chassis brand header: 'AKT III' on clean dedicated second line
- Add 5-room tavern architecture: Schankraum, Nebenraum, Marktplatz, Hinterland, Hinterzimmer
- Introduce Nebenraum (Kemenate/Atelier) with Amy at her easel painting the neon banner and Zuluman magic mirror
- Update Marktplatz scene with Mario juggling chainsaws/hops and NAT on the festival stage
- Implement Wildschwein-Schnick-Schnack-Schnuck minigame against NAT (Best of 3, Keiler > Dickicht > Lanze > Keiler)
- Replace secret tap with cellar floor trapdoor ('Luke zum Hinterraum') initially invisible
- Update automated playtest covering all 25 screens with zero console/network errors
- Document all architectural decisions in projects/sasch40/AGENTS.md
This commit is contained in:
Moritz Graf
2026-09-24 12:37:56 +02:00
parent bbae8a8c78
commit 6374ba9e8a
11 changed files with 524 additions and 101 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 972 KiB

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 972 KiB

After

Width:  |  Height:  |  Size: 489 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

+78 -28
View File
@@ -151,13 +151,52 @@ async function runPlaytest() {
await new Promise(r => setTimeout(r, 400));
}
// 6. TAVERN: MARKTPLATZ (Mario with spinning chainsaws)
// 5b. TAVERN: NEBENRAUM (Amy painting & Zuluman Mirror)
console.log('🎨 Navigating to Nebenraum (Kemenate / Atelier)...');
const nebenraumDoor = await page.$('.hotspot-door-nebenraum');
if (nebenraumDoor) {
await nebenraumDoor.click();
} else {
const nrTab = await page.$('.room-tab:nth-child(2)');
if (nrTab) await nrTab.click();
}
await new Promise(r => setTimeout(r, 1000));
console.log('📸 05b: Capturing Nebenraum (Atelier & Mirror)...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05b_tavern_nebenraum.png') });
// Test Amy in Nebenraum
console.log('🎨 Testing Amy Dialog in Nebenraum...');
const amyBtn = await page.$('.hotspot-amy-nebenraum');
if (amyBtn) {
await amyBtn.click();
await new Promise(r => setTimeout(r, 700));
console.log('📸 05c: Capturing Amy Dialog...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05c_tavern_amy_dialog.png') });
const actionBtn = await page.$('.dialog-actions .action-btn');
if (actionBtn) await actionBtn.click();
await new Promise(r => setTimeout(r, 400));
}
// Test Zuluman Mirror in Nebenraum
console.log('🪞 Testing Zuluman Magic Mirror in Nebenraum...');
const mirrorBtn = await page.$('.hotspot-mirror-nebenraum');
if (mirrorBtn) {
await mirrorBtn.click();
await new Promise(r => setTimeout(r, 700));
console.log('📸 14: Capturing Zuluman Mirror...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '14_tavern_zuluman_mirror.png') });
const closeBtn = await page.$('.mirror-box .action-btn');
if (closeBtn) await closeBtn.click();
await new Promise(r => setTimeout(r, 400));
}
// 6. TAVERN: MARKTPLATZ (Mario with spinning chainsaws & NAT on stage)
console.log('🎪 Navigating to Marktplatz...');
const marktplatzTab = await page.$('.room-tab:nth-child(2)');
const marktplatzTab = await page.$('.room-tab:nth-child(3)');
if (marktplatzTab) {
await marktplatzTab.click();
await new Promise(r => setTimeout(r, 1000));
console.log('📸 08: Capturing Marktplatz (Mario chainsaws)...');
console.log('📸 08: Capturing Marktplatz (Mario chainsaws & NAT stage)...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_marktplatz.png') });
// Click Mario
@@ -172,22 +211,45 @@ async function runPlaytest() {
await new Promise(r => setTimeout(r, 400));
}
// Click NAT (Josef)
// Click NAT (Josef) & Test Wildschwein Duell
const natBtn = await page.$('.hotspot-nat');
if (natBtn) {
await natBtn.click();
await new Promise(r => setTimeout(r, 700));
console.log('📸 09b: Capturing NAT (Josef) Dialog...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09b_tavern_nat_dialog.png') });
const actionBtn = await page.$('.dialog-actions .action-btn');
if (actionBtn) await actionBtn.click();
await new Promise(r => setTimeout(r, 400));
// Start Wild Boar Duel
console.log('🐗 Launching Wildschwein-Schnick-Schnack-Schnuck Duel...');
const startBoarBtn = await page.$('.dialog-actions .action-btn');
if (startBoarBtn) {
await startBoarBtn.click();
await new Promise(r => setTimeout(r, 800));
console.log('📸 09c: Capturing Wildschwein Duel Modal...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09c_tavern_boar_duel.png') });
// Play a move: Keiler!
const keilerBtn = await page.$('.boar-choice-btn:nth-child(1)');
if (keilerBtn) {
await keilerBtn.click();
await new Promise(r => setTimeout(r, 600));
console.log('📸 09d: Capturing Boar Duel Clash...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09d_tavern_boar_duel_round.png') });
}
// Close Boar Duel modal
const closeBoarBtn = await page.$('.boar-duel-box .duel-close-btn');
if (closeBoarBtn) {
await closeBoarBtn.click();
await new Promise(r => setTimeout(r, 400));
}
}
}
}
// 7. TAVERN: HINTERLAND (Mane hiking)
console.log('🌲 Navigating to Hinterland...');
const hinterlandTab = await page.$('.room-tab:nth-child(3)');
const hinterlandTab = await page.$('.room-tab:nth-child(4)');
if (hinterlandTab) {
await hinterlandTab.click();
await new Promise(r => setTimeout(r, 1000));
@@ -207,18 +269,18 @@ async function runPlaytest() {
}
}
// 8. TAVERN: SECRET HINTERZIMMER (VIP Rave via Secret Tap)
console.log('🗝️ Returning to Schankraum to check Secret Tap (initially invisible)...');
// 8. TAVERN: SECRET HINTERZIMMER (VIP Rave via Luke zum Hinterraum)
console.log('🗝️ Returning to Schankraum to check Luke zum Hinterraum (trapdoor)...');
const schankraumTab = await page.$('.room-tab:nth-child(1)');
if (schankraumTab) {
await schankraumTab.click();
await new Promise(r => setTimeout(r, 600));
}
console.log('🪩 Triggering Secret Tap Easter Egg...');
const secretTap = await page.$('.secret-tap-hotspot');
if (secretTap) {
await secretTap.click();
console.log('🪩 Triggering Luke zum Hinterraum Easter Egg...');
const trapdoor = await page.$('.trapdoor-hotspot');
if (trapdoor) {
await trapdoor.click();
await new Promise(r => setTimeout(r, 1000));
console.log('📸 12: Capturing Secret VIP Hinterzimmer...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '12_tavern_hinterzimmer_vip.png') });
@@ -236,25 +298,13 @@ async function runPlaytest() {
}
}
// 9. ZULUMAN MAGIC MIRROR & GSTANZL DUEL
console.log('🪞 Testing Zuluman Magic Mirror...');
// 9. GSTANZL DUEL
console.log('⚔️ Testing Gstanzl-Duel and [✕] Exit Button...');
const backToGaststube = await page.$('.hotspot-door-back-vip');
if (backToGaststube) {
await backToGaststube.click();
await new Promise(r => setTimeout(r, 600));
}
const mirrorBtn = await page.$('.hotspot-mirror');
if (mirrorBtn) {
await mirrorBtn.click();
await new Promise(r => setTimeout(r, 700));
console.log('📸 14: Capturing Zuluman Mirror...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '14_tavern_zuluman_mirror.png') });
const closeBtn = await page.$('.mirror-box .action-btn');
if (closeBtn) await closeBtn.click();
await new Promise(r => setTimeout(r, 400));
}
console.log('⚔️ Testing Gstanzl-Duel and [✕] Exit Button...');
const duelBtn = await page.$('.duel-trigger');
if (duelBtn) {
await duelBtn.click();
@@ -45,7 +45,7 @@ function toggleInventory() {
<div class="chassis-brand">
<span class="brand-text">KÖNIG SASCHAS KRONKORKENKREUZZUG</span>
<span v-if="store.currentAct !== 'intro'" class="brand-act">
{{ store.currentAct === 'prologue' ? '• AKT I' : store.currentAct === 'platformer' ? '• AKT II' : store.currentAct === 'tavern' ? '• AKT III' : store.currentAct === 'party_express' ? '• AKT IV' : store.currentAct === 'finale' ? '• AKT V' : '' }}
{{ store.currentAct === 'prologue' ? 'AKT I' : store.currentAct === 'platformer' ? 'AKT II' : store.currentAct === 'tavern' ? 'AKT III' : store.currentAct === 'party_express' ? 'AKT IV' : store.currentAct === 'finale' ? 'AKT V' : '' }}
</span>
</div>
<button class="sound-toggle" @click="toggleAudio">
@@ -203,8 +203,10 @@ function toggleInventory() {
.chassis-brand {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
justify-content: center;
gap: 2px;
overflow: hidden;
}
@@ -218,7 +220,7 @@ function toggleInventory() {
.brand-act {
font-family: 'Press Start 2P', monospace;
font-size: 6px;
font-size: 5.5px;
color: #f59e0b;
white-space: nowrap;
}
@@ -7,7 +7,7 @@ import { sound } from '../audio/synthAudio'
const store = useGameStore()
type TavernRoom = 'schankraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer'
type TavernRoom = 'schankraum' | 'nebenraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer'
const currentRoom = ref<TavernRoom>('schankraum')
interface ActiveDialog {
@@ -26,16 +26,16 @@ const duelStep = ref(0)
const duelWon = ref(false)
const zulumanMirrorActive = ref(false)
const secretTapDiscovered = ref(store.unlockedEasterEggs.includes('hinterzimmer'))
const trapdoorDiscovered = ref(store.unlockedEasterEggs.includes('hinterzimmer'))
function switchRoom(room: TavernRoom) {
sound.playDialogueBlip()
currentRoom.value = room
}
function triggerSecretTap() {
function triggerTrapdoor() {
sound.playFanfare()
secretTapDiscovered.value = true
trapdoorDiscovered.value = true
store.unlockEasterEgg('hinterzimmer')
currentRoom.value = 'hinterzimmer'
}
@@ -46,6 +46,91 @@ function enterSecretHinterzimmer() {
currentRoom.value = 'hinterzimmer'
}
// Wildschwein Schnick-Schnack-Schnuck Minigame with NAT
type WildBoarChoice = 'keiler' | 'dickicht' | 'lanze'
const wildBoarDuelActive = ref(false)
const wildBoarPlayerScore = ref(0)
const wildBoarNatScore = ref(0)
const wildBoarRound = ref(1)
const wildBoarResultMsg = ref('')
const wildBoarLastPlayerMove = ref<WildBoarChoice | null>(null)
const wildBoarLastNatMove = ref<WildBoarChoice | null>(null)
const wildBoarGameOver = ref(false)
const wildBoarPlayerWon = ref(false)
function startWildBoarDuel() {
sound.playDialogueBlip()
wildBoarDuelActive.value = true
wildBoarPlayerScore.value = 0
wildBoarNatScore.value = 0
wildBoarRound.value = 1
wildBoarResultMsg.value = 'Wähle deine Taktik für Runde 1!'
wildBoarLastPlayerMove.value = null
wildBoarLastNatMove.value = null
wildBoarGameOver.value = false
wildBoarPlayerWon.value = false
}
function formatBoarMove(m: WildBoarChoice): string {
if (m === 'keiler') return '🐗 Keiler'
if (m === 'dickicht') return '🌿 Dickicht'
return '🏹 Lanze'
}
function playWildBoarMove(playerChoice: WildBoarChoice) {
if (wildBoarGameOver.value) return
const choices: WildBoarChoice[] = ['keiler', 'dickicht', 'lanze']
const natChoice = choices[Math.floor(Math.random() * choices.length)]
wildBoarLastPlayerMove.value = playerChoice
wildBoarLastNatMove.value = natChoice
if (playerChoice === natChoice) {
sound.playDialogueBlip()
wildBoarResultMsg.value = 'Unentschieden! Zwei Keiler prallen mit den Köpfen zusammen!'
} else if (
(playerChoice === 'keiler' && natChoice === 'dickicht') ||
(playerChoice === 'dickicht' && natChoice === 'lanze') ||
(playerChoice === 'lanze' && natChoice === 'keiler')
) {
sound.playCoin()
wildBoarPlayerScore.value++
const explanation =
playerChoice === 'keiler'
? 'Dein Keiler bricht durch NATs Dickicht!'
: playerChoice === 'dickicht'
? 'Dein Eichen-Dickicht fängt NATs Lanze ab!'
: 'Deine Lanze stoppt NATs Keiler!'
wildBoarResultMsg.value = `Punkt für Sascha! ${explanation}`
} else {
sound.playGulp()
wildBoarNatScore.value++
const explanation =
natChoice === 'keiler'
? 'NATs Keiler bricht durch dein Dickicht!'
: natChoice === 'dickicht'
? 'NATs Dickicht verheddert deine Lanze!'
: 'NATs Lanze stoppt deinen Keiler!'
wildBoarResultMsg.value = `Punkt für NAT! ${explanation}`
}
wildBoarRound.value++
if (wildBoarPlayerScore.value >= 2) {
wildBoarGameOver.value = true
wildBoarPlayerWon.value = true
sound.playFanfare()
store.addKronkorken(10)
store.addBeer()
store.unlockEasterEgg('wildschwein_meister')
} else if (wildBoarNatScore.value >= 2) {
wildBoarGameOver.value = true
wildBoarPlayerWon.value = false
}
}
function openCharacterDialog(charId: string) {
const char = CHARACTERS[charId]
if (!char) return
@@ -123,12 +208,11 @@ function openCharacterDialog(charId: string) {
name: char.name,
title: char.title,
portrait: char.portrait,
text: 'Servus Sascha! Schau dir meinen Wildschwein-Harnisch an – der glänzt wie ein Kronjuwel! Ich hab dir schon 3 kühle Helle und 5 Kronkorken gesichert, bevor die Meute alles wegsäuft!',
actionLabel: 'Sauber Josef! (+5 Kronkorken & Helles)',
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)',
actionFn: () => {
store.addKronkorken(5)
store.addBeer()
activeDialog.value = null
startWildBoarDuel()
}
}
} else if (charId === 'mane') {
@@ -238,7 +322,7 @@ function proceedToExpress() {
<!-- ================= 1. GASTSTUBE (SCHANKRAUM) ================= -->
<template v-if="currentRoom === 'schankraum'">
<img src="/assets/scenes/schankraum_v2.jpg?v=2" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
<img src="/assets/scenes/schankraum_v2.jpg?v=3" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
<!-- Verena on the wooden gallery -->
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
@@ -246,6 +330,12 @@ function proceedToExpress() {
<span class="hotspot-badge">Verena 👑</span>
</button>
<!-- Door to Nebenraum (Kemenate / Atelier) -->
<button class="hotspot door-hotspot hotspot-door-nebenraum" @click="switchRoom('nebenraum')">
<span class="door-icon">🚪</span>
<span class="hotspot-badge glow-door">Zum Nebenraum ▶</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" />
@@ -258,43 +348,60 @@ function proceedToExpress() {
<span class="hotspot-badge">Lutz (ZTUL) 🎩</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>
<!-- 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>
<!-- Doorway to Marktplatz -->
<button class="hotspot door-hotspot hotspot-door-marktplatz" @click="switchRoom('marktplatz')">
<span class="door-icon">🚪</span>
<span class="door-icon">🎪</span>
<span class="hotspot-badge glow-door">Zum Marktplatz ▶</span>
</button>
<!-- SECRET EASTER EGG TRIGGER: Hidden Beer Tap to Hinterzimmer (Completely invisible until clicked) -->
<!-- Luke zum Hinterraum (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"
class="hotspot trapdoor-hotspot"
:class="{ 'trap-invisible': !trapdoorDiscovered && !store.unlockedEasterEggs.includes('hinterzimmer') }"
@click="triggerTrapdoor"
:title="trapdoorDiscovered || store.unlockedEasterEggs.includes('hinterzimmer') ? 'Luke zum Hinterraum' : undefined"
>
<template v-if="secretTapDiscovered || store.unlockedEasterEggs.includes('hinterzimmer')">
<span class="secret-tap-icon pulse">🗝️</span>
<span class="secret-badge">Geheimer Zapfhahn</span>
<template v-if="trapdoorDiscovered || store.unlockedEasterEggs.includes('hinterzimmer')">
<span class="trapdoor-icon">🚪</span>
<span class="trapdoor-badge">Luke zum Hinterraum</span>
</template>
</button>
</template>
<!-- ================= 2. MARKTPLATZ ================= -->
<template v-else-if="currentRoom === 'marktplatz'">
<img src="/assets/scenes/marktplatz.jpg" alt="Marktplatz Haumdaucher" class="tavern-bg" />
<!-- ================= 2. NEBENRAUM (KEMENATE & ATELIER) ================= -->
<template v-else-if="currentRoom === 'nebenraum'">
<img src="/assets/scenes/nebenraum_v2.jpg?v=2" alt="Königlicher Nebenraum & Atelier" class="tavern-bg" />
<!-- Gaukler Mario juggling chainsaws with animations & flying sparks -->
<!-- Zuluman Magic Mirror on the timber wall -->
<button class="hotspot hotspot-mirror-nebenraum" @click="inspectMirror">
<span class="mirror-icon">🪞</span>
<span class="hotspot-badge pulse">Zuluman-Spiegel</span>
</button>
<!-- Amy at the easel painting the canvas -->
<button class="hotspot hotspot-amy-nebenraum" @click="openCharacterDialog('amy')">
<img :src="CHARACTERS.amy.portrait" class="hotspot-thumb" alt="Amy" />
<span class="hotspot-badge">Amy 🎨</span>
</button>
<!-- Door back to Gaststube (Schankraum) -->
<button class="hotspot door-hotspot hotspot-door-back-gaststube-nr" @click="switchRoom('schankraum')">
<span class="door-icon">🍺</span>
<span class="hotspot-badge glow-door">◀ Zur Gaststube</span>
</button>
</template>
<!-- ================= 3. MARKTPLATZ ================= -->
<template v-else-if="currentRoom === 'marktplatz'">
<img src="/assets/scenes/marktplatz_v2.jpg?v=2" alt="Marktplatz Haumdaucher" class="tavern-bg" />
<!-- NAT on the festival stage telling stories of his girls and heroic deeds -->
<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>
<!-- Gaukler Mario juggling chainsaws & hops on the cobblestones -->
<div class="mario-container">
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
<div class="chainsaw-circus">
@@ -308,15 +415,9 @@ function proceedToExpress() {
</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="door-icon">🍺</span>
<span class="hotspot-badge glow-door">◀ Zur Gaststube</span>
</button>
@@ -388,6 +489,7 @@ function proceedToExpress() {
<div class="tavern-bar-title">
{{
currentRoom === 'schankraum' ? '🍻 AKT III: GASTSTUBE ZUM WUMMERNDEN RITTER 🍻' :
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 🪩'
@@ -403,6 +505,13 @@ function proceedToExpress() {
>
🍺 Gaststube
</button>
<button
class="room-tab"
:class="{ active: currentRoom === 'nebenraum' }"
@click="switchRoom('nebenraum')"
>
🎨 Nebenraum
</button>
<button
class="room-tab"
:class="{ active: currentRoom === 'marktplatz' }"
@@ -514,6 +623,79 @@ function proceedToExpress() {
</div>
</div>
</div>
<!-- Wildschwein Schnick-Schnack-Schnuck Minigame Modal -->
<div v-if="wildBoarDuelActive" class="dialog-overlay">
<div class="dialog-box boar-duel-box">
<div class="boar-header">
<div class="boar-title-group">
<h3>🐗 WILDSCHWEIN-SCHNICK-SCHNACK-SCHNUCK</h3>
<span class="boar-score">Stand: Sascha {{ wildBoarPlayerScore }} : {{ wildBoarNatScore }} NAT (Best of 3)</span>
</div>
<button class="duel-close-btn" @click="wildBoarDuelActive = false" aria-label="Duell verlassen">✕</button>
</div>
<!-- Rules / Explanations -->
<div class="boar-rules-banner">
🐗 Keiler bricht 🌿 Dickicht | 🌿 Dickicht fängt 🏹 Lanze | 🏹 Lanze erlegt 🐗 Keiler
</div>
<div class="boar-arena">
<div class="boar-status-msg">
{{ wildBoarResultMsg }}
</div>
<div v-if="wildBoarLastPlayerMove && wildBoarLastNatMove" class="boar-clash">
<div class="clash-side">
<span class="clash-label">Sascha:</span>
<span class="clash-move">{{ formatBoarMove(wildBoarLastPlayerMove) }}</span>
</div>
<span class="clash-vs">VS</span>
<div class="clash-side">
<span class="clash-label">NAT:</span>
<span class="clash-move">{{ formatBoarMove(wildBoarLastNatMove) }}</span>
</div>
</div>
</div>
<!-- Move Choices -->
<div v-if="!wildBoarGameOver" class="boar-choices">
<button class="boar-choice-btn" @click="playWildBoarMove('keiler')">
<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')">
<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')">
<span class="choice-icon">🏹</span>
<span class="choice-name">Lanze</span>
<span class="choice-sub">Erlegt Keiler</span>
</button>
</div>
<!-- Game Over Screen -->
<div v-else class="boar-game-over">
<template v-if="wildBoarPlayerWon">
<h2 class="victory-title">👑 SASCHA IST WILDSCHWEIN-MEISTER!</h2>
<p class="boar-victory-desc">NAT verneigt sich ehrfürchtig! „Du kennst das Dickicht besser als jeder Keiler!“</p>
<p class="reward-text">+10 Kronkorken, 1 Kaltes Helles & Titel freigeschaltet!</p>
<button class="action-btn" @click="wildBoarDuelActive = false">Herrlich! Prost NAT!</button>
</template>
<template v-else>
<h2 class="defeat-title">🐗 NAT GEWINNT DAS DUELL!</h2>
<p class="boar-defeat-desc">NAT lacht schallend: „Das Haumdaucher Dickicht verlangt höchste Meisterschaft, Sascha!“</p>
<div class="boar-retry-actions">
<button class="action-btn retry-btn" @click="startWildBoarDuel">Revanche fordern!</button>
<button class="close-btn" @click="wildBoarDuelActive = false">Vielleicht später</button>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
@@ -594,24 +776,27 @@ function proceedToExpress() {
color: #38bdf8;
}
/* Secret Tap Easter Egg */
.secret-tap-hotspot {
/* Trapdoor to Hinterzimmer Easter Egg */
.trapdoor-hotspot {
position: absolute;
top: 72%;
top: 73%;
left: 82%;
background: rgba(234, 179, 8, 0.2);
background: rgba(180, 83, 9, 0.4);
border: 1.5px dashed #ffd700;
border-radius: 12px;
border-radius: 8px;
padding: 3px 6px;
cursor: pointer;
z-index: 12;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
.secret-tap-hotspot.tap-invisible,
.secret-tap-hotspot.tap-invisible:hover,
.secret-tap-hotspot.tap-invisible:active,
.secret-tap-hotspot.tap-invisible:focus {
.trapdoor-hotspot.trap-invisible,
.trapdoor-hotspot.trap-invisible:hover,
.trapdoor-hotspot.trap-invisible:active,
.trapdoor-hotspot.trap-invisible:focus {
background: transparent !important;
border: none !important;
box-shadow: none !important;
@@ -623,29 +808,34 @@ function proceedToExpress() {
cursor: pointer;
}
.secret-tap-icon {
.trapdoor-icon {
font-size: 13px;
}
.secret-badge {
.trapdoor-badge {
font-family: 'Press Start 2P', monospace;
font-size: 5.5px;
color: #fef08a;
white-space: nowrap;
}
/* 1. Schankraum Hotspots */
.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-door-nebenraum { top: 42%; left: 34%; }
.hotspot-armend { top: 62%; left: 18%; }
.hotspot-lutz { top: 54%; left: 56%; }
.hotspot-door-marktplatz { top: 50%; left: 84%; }
/* 2. Marktplatz Hotspots */
/* 2. Nebenraum Hotspots */
.hotspot-mirror-nebenraum { top: 36%; left: 24%; }
.hotspot-door-back-gaststube-nr { top: 48%; left: 56%; }
.hotspot-amy-nebenraum { top: 64%; left: 80%; }
/* 3. Marktplatz Hotspots */
.mario-container {
position: absolute;
top: 44%;
left: 30%;
top: 66%;
left: 70%;
z-index: 10;
}
@@ -699,9 +889,9 @@ function proceedToExpress() {
100% { opacity: 1; transform: scale(1.3); }
}
.hotspot-nat { top: 62%; left: 70%; }
.hotspot-door-back-gaststube { top: 72%; left: 20%; }
.hotspot-door-hinterland { top: 32%; left: 76%; }
.hotspot-nat { top: 54%; left: 22%; }
.hotspot-door-back-gaststube { top: 74%; left: 20%; }
.hotspot-door-hinterland { top: 34%; left: 76%; }
/* 3. Hinterland Hotspots */
.hotspot-mane { top: 44%; left: 36%; }
@@ -1074,4 +1264,170 @@ function proceedToExpress() {
font-size: 12px;
font-family: 'Press Start 2P', monospace;
}
/* Wild Boar Duel Modal */
.boar-duel-box {
max-width: 360px;
}
.boar-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1px solid #78350f;
margin-bottom: 10px;
padding-bottom: 6px;
}
.boar-title-group {
flex: 1;
}
.boar-title-group h3 {
font-family: 'Press Start 2P', monospace;
font-size: 7.5px;
color: #22c55e;
margin: 0 0 4px 0;
line-height: 1.3;
}
.boar-score {
font-family: 'Press Start 2P', monospace;
font-size: 6.5px;
color: #fef08a;
}
.boar-rules-banner {
background: rgba(34, 197, 94, 0.15);
border: 1px dashed #22c55e;
border-radius: 4px;
padding: 5px 8px;
font-size: 8.5px;
color: #bbf7d0;
text-align: center;
margin-bottom: 10px;
font-family: 'MedievalSharp', serif;
}
.boar-arena {
background: #1a0f0a;
border: 1.5px solid #78350f;
border-radius: 6px;
padding: 10px;
text-align: center;
margin-bottom: 12px;
}
.boar-status-msg {
font-family: 'MedievalSharp', serif;
font-size: 14px;
color: #fef3c7;
min-height: 38px;
display: flex;
align-items: center;
justify-content: center;
}
.boar-clash {
display: flex;
align-items: center;
justify-content: space-around;
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #451a03;
}
.clash-side {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.clash-label {
font-family: 'Press Start 2P', monospace;
font-size: 6px;
color: #94a3b8;
}
.clash-move {
font-size: 13px;
font-weight: bold;
color: #ffd700;
}
.clash-vs {
font-family: 'Press Start 2P', monospace;
font-size: 9px;
color: #ef4444;
}
.boar-choices {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.boar-choice-btn {
background: #3b2014;
border: 1.5px solid #16a34a;
border-radius: 6px;
padding: 8px 4px;
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
cursor: pointer;
transition: all 0.15s ease;
}
.boar-choice-btn:hover {
background: #14532d;
border-color: #4ade80;
transform: translateY(-2px);
}
.choice-icon {
font-size: 20px;
}
.choice-name {
font-family: 'Press Start 2P', monospace;
font-size: 7px;
color: #fef08a;
}
.choice-sub {
font-size: 8.5px;
color: #94a3b8;
font-family: 'MedievalSharp', serif;
}
.boar-game-over {
text-align: center;
}
.boar-victory-desc, .boar-defeat-desc {
font-family: 'MedievalSharp', serif;
font-size: 13.5px;
margin: 6px 0;
color: #fef3c7;
}
.defeat-title {
color: #ef4444;
font-size: 10px;
font-family: 'Press Start 2P', monospace;
}
.boar-retry-actions {
display: flex;
gap: 8px;
margin-top: 10px;
}
.retry-btn {
background: #16a34a;
border-color: #86efac;
}
</style>