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:
@@ -162,6 +162,116 @@ class SoundEngine {
|
||||
})
|
||||
}
|
||||
|
||||
// SFX: Sister Rescue Fanfare (bright sweet jingle)
|
||||
public playRescueFanfare() {
|
||||
if (this.isMuted) return
|
||||
this.initCtx()
|
||||
if (!this.ctx) return
|
||||
|
||||
const notes = [523.25, 659.25, 783.99, 1046.50] // C5, E5, G5, C6
|
||||
notes.forEach((freq, idx) => {
|
||||
if (!this.ctx) return
|
||||
const now = this.ctx.currentTime + idx * 0.1
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
|
||||
osc.type = 'sine'
|
||||
osc.frequency.setValueAtTime(freq, now)
|
||||
|
||||
gain.gain.setValueAtTime(0.25, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35)
|
||||
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
|
||||
osc.start(now)
|
||||
osc.stop(now + 0.35)
|
||||
})
|
||||
}
|
||||
|
||||
// SFX: Spring / Trampoline Bounce (Doodle Jump super bounce)
|
||||
public playSpringBounce() {
|
||||
if (this.isMuted) return
|
||||
this.initCtx()
|
||||
if (!this.ctx) return
|
||||
|
||||
const now = this.ctx.currentTime
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
|
||||
osc.type = 'triangle'
|
||||
osc.frequency.setValueAtTime(220, now)
|
||||
osc.frequency.exponentialRampToValueAtTime(880, now + 0.25)
|
||||
|
||||
gain.gain.setValueAtTime(0.3, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3)
|
||||
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
|
||||
osc.start(now)
|
||||
osc.stop(now + 0.3)
|
||||
}
|
||||
|
||||
// SFX: Hit / Impact / Thud
|
||||
public playBoarHit() {
|
||||
if (this.isMuted) return
|
||||
this.initCtx()
|
||||
if (!this.ctx) return
|
||||
|
||||
const now = this.ctx.currentTime
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
|
||||
osc.type = 'sawtooth'
|
||||
osc.frequency.setValueAtTime(160, now)
|
||||
osc.frequency.exponentialRampToValueAtTime(40, now + 0.2)
|
||||
|
||||
gain.gain.setValueAtTime(0.3, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2)
|
||||
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
|
||||
osc.start(now)
|
||||
osc.stop(now + 0.2)
|
||||
}
|
||||
|
||||
// SFX: Medieval Lute Hero Note (String Index 0: Low, 1: Mid, 2: High)
|
||||
public playLuteNote(lane: number, success: boolean = true) {
|
||||
if (this.isMuted) return
|
||||
this.initCtx()
|
||||
if (!this.ctx) return
|
||||
|
||||
const now = this.ctx.currentTime
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
|
||||
if (!success) {
|
||||
// Missed clunk / muted buzz
|
||||
osc.type = 'sawtooth'
|
||||
osc.frequency.setValueAtTime(90, now)
|
||||
gain.gain.setValueAtTime(0.12, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12)
|
||||
} else {
|
||||
// Resonant lute string twang
|
||||
const freqs = [293.66, 392.00, 587.33] // D4, G4, D5
|
||||
const freq = freqs[lane % 3] || 440
|
||||
osc.type = 'triangle'
|
||||
osc.frequency.setValueAtTime(freq, now)
|
||||
|
||||
gain.gain.setValueAtTime(0.28, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.32)
|
||||
}
|
||||
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
|
||||
osc.start(now)
|
||||
osc.stop(now + (success ? 0.32 : 0.12))
|
||||
}
|
||||
|
||||
|
||||
// 135 BPM Medieval Techno Chiptune Beat
|
||||
public startBGM() {
|
||||
if (this.isMuted || this.bgmPlaying) return
|
||||
|
||||
@@ -11,18 +11,32 @@ import CertificateModal from './CertificateModal.vue'
|
||||
|
||||
const store = useGameStore()
|
||||
const platformerRef = ref<InstanceType<typeof PlatformerView> | null>(null)
|
||||
const inventoryOpen = ref(false)
|
||||
const concertRef = ref<InstanceType<typeof PartyExpressView> | null>(null)
|
||||
|
||||
function handleJump() {
|
||||
if (platformerRef.value) {
|
||||
if (store.currentAct === 'platformer' && platformerRef.value) {
|
||||
platformerRef.value.onJump()
|
||||
} else if (store.currentAct === 'party_express' && concertRef.value) {
|
||||
concertRef.value.hitLane(2)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDpad(dir: 'left' | 'right', pressed: boolean) {
|
||||
if (platformerRef.value) {
|
||||
function handleButtonB() {
|
||||
if (store.currentAct === 'party_express' && concertRef.value) {
|
||||
concertRef.value.hitLane(1)
|
||||
} else {
|
||||
store.addBeer()
|
||||
}
|
||||
}
|
||||
|
||||
function handleDpad(dir: 'left' | 'right' | 'down', pressed: boolean) {
|
||||
if (store.currentAct === 'platformer' && platformerRef.value) {
|
||||
if (dir === 'left') platformerRef.value.onLeft(pressed)
|
||||
if (dir === 'right') platformerRef.value.onRight(pressed)
|
||||
} else if (store.currentAct === 'party_express' && concertRef.value && pressed) {
|
||||
if (dir === 'left') concertRef.value.hitLane(0)
|
||||
if (dir === 'down') concertRef.value.hitLane(1)
|
||||
if (dir === 'right') concertRef.value.hitLane(2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +44,26 @@ function toggleAudio() {
|
||||
store.toggleMute()
|
||||
}
|
||||
|
||||
function toggleInventory() {
|
||||
inventoryOpen.value = !inventoryOpen.value
|
||||
const settingsOpen = ref(false)
|
||||
|
||||
function openSettings() {
|
||||
sound.playDialogueBlip()
|
||||
settingsOpen.value = true
|
||||
store.pauseGame()
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
sound.playDialogueBlip()
|
||||
settingsOpen.value = false
|
||||
store.resumeGame()
|
||||
}
|
||||
|
||||
function resetGameFromSettings() {
|
||||
if (confirm('Möchtest du das gesamte Spiel wirklich von vorne neu beginnen?')) {
|
||||
settingsOpen.value = false
|
||||
store.resumeGame()
|
||||
store.resetGame()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -48,8 +80,8 @@ function toggleInventory() {
|
||||
{{ 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">
|
||||
{{ store.soundMuted ? '🔇 TON' : '🔊 135 BPM' }}
|
||||
<button class="settings-toggle-btn" @click="openSettings" title="Einstellungen & Pause" aria-label="Einstellungen">
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +92,7 @@ function toggleInventory() {
|
||||
<PrologueView v-else-if="store.currentAct === 'prologue'" />
|
||||
<PlatformerView v-else-if="store.currentAct === 'platformer'" ref="platformerRef" />
|
||||
<TavernView v-else-if="store.currentAct === 'tavern'" />
|
||||
<PartyExpressView v-else-if="store.currentAct === 'party_express'" />
|
||||
<PartyExpressView v-else-if="store.currentAct === 'party_express'" ref="concertRef" />
|
||||
<CertificateModal v-else-if="store.currentAct === 'finale'" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,7 +131,14 @@ function toggleInventory() {
|
||||
@mousedown="handleDpad('right', true)"
|
||||
@mouseup="handleDpad('right', false)"
|
||||
>▶</button>
|
||||
<button class="dpad-btn dpad-down" disabled></button>
|
||||
<button
|
||||
class="dpad-btn dpad-down"
|
||||
:disabled="store.currentAct !== 'party_express'"
|
||||
@touchstart.prevent="handleDpad('down', true)"
|
||||
@touchend.prevent="handleDpad('down', false)"
|
||||
@mousedown="handleDpad('down', true)"
|
||||
@mouseup="handleDpad('down', false)"
|
||||
>▼</button>
|
||||
</div>
|
||||
<span class="control-label">D-PAD</span>
|
||||
</div>
|
||||
@@ -108,9 +147,9 @@ function toggleInventory() {
|
||||
<div class="center-controls">
|
||||
<div class="royal-crest">👑</div>
|
||||
<div class="pill-buttons">
|
||||
<button class="pill-btn" @click="toggleInventory">
|
||||
<button class="pill-btn" @click="openSettings">
|
||||
<span class="pill-key">SELECT</span>
|
||||
<span class="pill-sub">BEUTEL</span>
|
||||
<span class="pill-sub">PAUSE</span>
|
||||
</button>
|
||||
<button class="pill-btn" @click="toggleAudio">
|
||||
<span class="pill-key">START</span>
|
||||
@@ -124,7 +163,7 @@ function toggleInventory() {
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
class="action-round-btn btn-b"
|
||||
@click="store.addBeer"
|
||||
@click="handleButtonB"
|
||||
>
|
||||
<span class="btn-letter">B</span>
|
||||
</button>
|
||||
@@ -137,8 +176,8 @@ function toggleInventory() {
|
||||
</button>
|
||||
</div>
|
||||
<div class="action-labels">
|
||||
<span>O'ZAPFT (B)</span>
|
||||
<span>SPRUNG (A)</span>
|
||||
<span>{{ store.currentAct === 'party_express' ? 'SAITE G (B)' : "O'ZAPFT (B)" }}</span>
|
||||
<span>{{ store.currentAct === 'party_express' ? 'SAITE A (A)' : 'SPRUNG (A)' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,21 +193,27 @@ function toggleInventory() {
|
||||
<span class="adv-icon">🍻</span>
|
||||
<span class="adv-val">{{ store.beers }} Helles</span>
|
||||
</div>
|
||||
<button class="inv-mini-btn" @click="toggleInventory">
|
||||
🎒 Beutel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Inventory Drawer Modal -->
|
||||
<div v-if="inventoryOpen" class="inventory-drawer" @click="inventoryOpen = false">
|
||||
<div class="inventory-card" @click.stop>
|
||||
<h3>👑 KÖNIGLICHER LEERGUTSACK</h3>
|
||||
<ul class="inventory-items">
|
||||
<li v-for="item in store.inventory" :key="item" class="inv-item">
|
||||
✨ {{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
<button class="close-inv-btn" @click="inventoryOpen = false">Schließen</button>
|
||||
<!-- Settings & Pause Modal -->
|
||||
<div v-if="settingsOpen" class="settings-overlay" @click="closeSettings">
|
||||
<div class="settings-card" @click.stop>
|
||||
<div class="settings-header">
|
||||
<h3 class="settings-title">⚙️ EINSTELLUNGEN & PAUSE</h3>
|
||||
<button class="settings-close-x" @click="closeSettings">✕</button>
|
||||
</div>
|
||||
<p class="settings-paused-hint">⏸️ Spiel und Timer pausiert</p>
|
||||
<div class="settings-actions">
|
||||
<button class="settings-btn" @click="toggleAudio">
|
||||
{{ store.soundMuted ? '🔇 Ton: AUS (Einschalten)' : '🔊 Ton: 135 BPM AN (Stummschalten)' }}
|
||||
</button>
|
||||
<button class="settings-btn reset-danger-btn" @click="resetGameFromSettings">
|
||||
🔄 Spiel neu starten (Reset)
|
||||
</button>
|
||||
<button class="settings-btn resume-btn" @click="closeSettings">
|
||||
▶ Weiterspielen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,15 +282,23 @@ function toggleInventory() {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.sound-toggle {
|
||||
background: #78350f;
|
||||
color: #fef08a;
|
||||
.settings-toggle-btn {
|
||||
background: rgba(120, 53, 15, 0.7);
|
||||
color: #ffd700;
|
||||
border: 1px solid #ffd700;
|
||||
border-radius: 4px;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7px;
|
||||
padding: 4px 6px;
|
||||
font-size: 13px;
|
||||
padding: 2px 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-toggle-btn:hover, .settings-toggle-btn:active {
|
||||
background: #b45309;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Screen Bezel */
|
||||
@@ -303,17 +356,6 @@ function toggleInventory() {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.inv-mini-btn {
|
||||
background: #78350f;
|
||||
color: #fef08a;
|
||||
border: 1px solid #b45309;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Parchment Status Bar */
|
||||
.parchment-scroll {
|
||||
background: #fdf6e2;
|
||||
@@ -510,54 +552,119 @@ function toggleInventory() {
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
/* Inventory Modal */
|
||||
.inventory-drawer {
|
||||
/* Settings & Pause Modal */
|
||||
.settings-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 200;
|
||||
z-index: 250;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.inventory-card {
|
||||
.settings-card {
|
||||
background: #2b1810;
|
||||
border: 3px solid #d4af37;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
width: 80%;
|
||||
padding: 18px;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
color: #fef3c7;
|
||||
box-shadow: 0 0 24px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.inventory-card h3 {
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1.5px solid #78350f;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 9px;
|
||||
font-size: 8px;
|
||||
color: #ffd700;
|
||||
margin-top: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.inventory-items {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.inv-item {
|
||||
font-family: 'MedievalSharp', serif;
|
||||
font-size: 15px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed #78350f;
|
||||
}
|
||||
|
||||
.close-inv-btn {
|
||||
background: #78350f;
|
||||
color: #fff;
|
||||
border: 1px solid #d4af37;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7px;
|
||||
.settings-close-x {
|
||||
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;
|
||||
line-height: 1;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-close-x:hover {
|
||||
background: #dc2626;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.settings-paused-hint {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6.5px;
|
||||
color: #38bdf8;
|
||||
text-align: center;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
background: #3b2014;
|
||||
color: #fef08a;
|
||||
border: 1.5px solid #b45309;
|
||||
border-radius: 6px;
|
||||
padding: 10px 8px;
|
||||
font-family: 'MedievalSharp', serif;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-btn:hover {
|
||||
background: #78350f;
|
||||
border-color: #ffd700;
|
||||
}
|
||||
|
||||
.reset-danger-btn {
|
||||
background: rgba(127, 29, 29, 0.4);
|
||||
border-color: #ef4444;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.reset-danger-btn:hover {
|
||||
background: #991b1b;
|
||||
color: #fff;
|
||||
border-color: #f87171;
|
||||
}
|
||||
|
||||
.resume-btn {
|
||||
background: #15803d;
|
||||
border-color: #86efac;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.resume-btn:hover {
|
||||
background: #16a34a;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -51,10 +51,10 @@ function handleStart() {
|
||||
<div class="scroll-section">
|
||||
<h3 class="section-title">🎮 WAS DICH IM SPIEL ERWARTET</h3>
|
||||
<ul class="features-list">
|
||||
<li><strong>🏃♂️ Retro Jump & Run:</strong> Hüpfe über die Dächer der Hopfen-Pfade und sammle die verlorenen Kronkorken ein.</li>
|
||||
<li><strong>🍻 Tavernen-Abenteuer:</strong> Sprich mit Verena, Lutz (ZTUL), Armend, Amy, Mane & Pü und gewinne das bayerische Beleidigungs-Gstanzl-Fechten!</li>
|
||||
<li><strong>☁️ Doodle Jump Schwesternrettung:</strong> Hüpfe von Wolke zu Wolke hinauf und rette Saschas drei Schwestern Saskia, Maja und Miriam!</li>
|
||||
<li><strong>🍻 Tavernen-Abenteuer:</strong> Erkunde Gaststube, Atelier, Marktplatz, Hinterland & den geheimen Party Bunker. Bezwinge NAT im Wildschwein-Duell und gewinne das bayerische Gstanzl-Fechten!</li>
|
||||
<li><strong>🌴 Der Zauberspiegel:</strong> Finde das geheime Urlaubserinnerungs-Easter-Egg von Buschkönig Zuluman.</li>
|
||||
<li><strong>🔊 Der 135-BPM-Express:</strong> Rase mit den Hof-DJs auf dem Bollerwagen und versorge das feiernde Gefolge.</li>
|
||||
<li><strong>🎸 Mittelalter Guitar Hero:</strong> Rocke als Barden-Duo mit Ritter Hansi auf der Schlossbühne sein legendäres Abschiedskonzert nach Münchanien!</li>
|
||||
<li><strong>🏆 Highscore-Urkunde:</strong> Hol dir am Ende deine persönliche königliche Punktzahl und vergleiche dich mit allen Partygästen!</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,10 @@ function selectChoice(choice: typeof PROLOGUE_DIALOGUES['start']['choices'][0])
|
||||
if (choice.action === 'unlockBridgerton') {
|
||||
store.unlockEasterEgg('bridgerton')
|
||||
}
|
||||
if (choice.action === 'resetPrologue') {
|
||||
currentNodeId.value = 'start'
|
||||
return
|
||||
}
|
||||
if (choice.action === 'startPlatformer') {
|
||||
store.setAct('platformer')
|
||||
return
|
||||
@@ -24,6 +28,9 @@ function selectChoice(choice: typeof PROLOGUE_DIALOGUES['start']['choices'][0])
|
||||
|
||||
if (choice.nextId && PROLOGUE_DIALOGUES[choice.nextId]) {
|
||||
currentNodeId.value = choice.nextId
|
||||
if (PROLOGUE_DIALOGUES[choice.nextId].isGameOver) {
|
||||
sound.playBoarHit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +39,14 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prologue-container">
|
||||
<div class="prologue-container" :class="{ 'game-over-active': currentNode().isGameOver }">
|
||||
<!-- Balcony Visual Scene with Regal Portrait Frame -->
|
||||
<div class="scene-visual">
|
||||
<div class="scene-visual" :class="{ 'fire-visual': currentNodeId === 'flo_game_over', 'vampire-visual': currentNodeId === 'viktor_game_over' }">
|
||||
<div class="balcony-backdrop"></div>
|
||||
<div class="scene-tag">
|
||||
<span class="scene-title">SCHLOSS HAUMDAUCHER – DER BALKON</span>
|
||||
<span class="scene-title">{{ currentNode().isGameOver ? '💀 GAME OVER – HAUMDAUCHER KAPUTT!' : 'SCHLOSS HAUMDAUCHER – DER BALKON' }}</span>
|
||||
</div>
|
||||
<div class="speaker-frame">
|
||||
<div class="speaker-frame" :class="{ 'game-over-frame': currentNode().isGameOver }">
|
||||
<img
|
||||
:src="currentSpeaker().portrait"
|
||||
:alt="currentSpeaker().name"
|
||||
@@ -49,7 +56,7 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
||||
</div>
|
||||
|
||||
<!-- Retro Dialogue Card -->
|
||||
<div class="dialogue-card">
|
||||
<div class="dialogue-card" :class="{ 'game-over-card': currentNode().isGameOver }">
|
||||
<div class="dialogue-header">
|
||||
<div class="speaker-name-badge">
|
||||
<span class="speaker-name">{{ currentSpeaker().name }}</span>
|
||||
@@ -57,7 +64,7 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialogue-body">
|
||||
<div class="dialogue-body" :class="{ 'game-over-body': currentNode().isGameOver }">
|
||||
<p class="dialogue-text">„{{ currentNode().text }}“</p>
|
||||
</div>
|
||||
|
||||
@@ -66,9 +73,10 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
||||
v-for="(choice, idx) in currentNode().choices"
|
||||
:key="idx"
|
||||
class="choice-btn"
|
||||
:class="{ 'game-over-btn': currentNode().isGameOver }"
|
||||
@click="selectChoice(choice)"
|
||||
>
|
||||
<span class="choice-arrow">▶</span>
|
||||
<span class="choice-arrow">{{ currentNode().isGameOver ? '🔄' : '▶' }}</span>
|
||||
<span class="choice-text">„{{ choice.text }}“</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -227,4 +235,54 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
||||
color: #f59e0b;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Game Over Styles */
|
||||
.game-over-active {
|
||||
background: #200505;
|
||||
}
|
||||
|
||||
.fire-visual {
|
||||
background: radial-gradient(circle at 50% 40%, #7f1d1d 0%, #1c0505 100%) !important;
|
||||
border-bottom-color: #ef4444 !important;
|
||||
box-shadow: 0 0 30px rgba(239, 68, 68, 0.5) inset;
|
||||
}
|
||||
|
||||
.vampire-visual {
|
||||
background: radial-gradient(circle at 50% 40%, #581c87 0%, #0f051d 100%) !important;
|
||||
border-bottom-color: #a855f7 !important;
|
||||
box-shadow: 0 0 30px rgba(168, 85, 247, 0.5) inset;
|
||||
}
|
||||
|
||||
.game-over-frame {
|
||||
border-color: #ef4444 !important;
|
||||
box-shadow: 0 0 20px #ef4444 !important;
|
||||
}
|
||||
|
||||
.game-over-card {
|
||||
background: #1c0808 !important;
|
||||
}
|
||||
|
||||
.game-over-body {
|
||||
border-left-color: #ef4444 !important;
|
||||
background: rgba(127, 29, 29, 0.3) !important;
|
||||
}
|
||||
|
||||
.game-over-btn {
|
||||
background: #7f1d1d !important;
|
||||
border-color: #f87171 !important;
|
||||
color: #fef08a !important;
|
||||
font-weight: bold;
|
||||
animation: pulse-border 1.5s infinite;
|
||||
}
|
||||
|
||||
.game-over-btn:hover {
|
||||
background: #991b1b !important;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0%, 100% { box-shadow: 0 0 4px #ef4444; }
|
||||
50% { box-shadow: 0 0 14px #ef4444; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -111,21 +111,48 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
flo_maier: {
|
||||
id: 'flo_maier',
|
||||
name: 'Flo Maier',
|
||||
title: 'Hof-DJ & Sound-Ingenieur',
|
||||
title: 'Hof-DJ & Feierdeifl',
|
||||
portrait: '/assets/characters/flo_maier/portrait_v2.jpg',
|
||||
description: 'Mastermind an den Turntables und Behüter der heiligen 135-BPM-Frequenz.',
|
||||
famousQuote: 'Dieser Track drückt mehr als die königliche Kutsche!',
|
||||
description: 'Mastermind an den Turntables und berüchtigter Feierdeifl des Reiches.',
|
||||
famousQuote: 'Soll i ois ozindn?! A riesigs Freudenfeuer!',
|
||||
themeColor: '#0284c7'
|
||||
},
|
||||
viktor: {
|
||||
id: 'viktor',
|
||||
name: 'Viktor',
|
||||
title: 'Nachtwächter & Party-Ritter',
|
||||
title: 'Partyvampir & Nachtwächter',
|
||||
portrait: '/assets/characters/viktor/portrait_v2.jpg',
|
||||
description: 'Wacht im samtenen Vampir-Umhang über das Nachtleben und sorgt für eiskaltes Helles.',
|
||||
famousQuote: 'Die Nacht ist jung und der Gerstensaft fließt in Strömen!',
|
||||
description: 'Wacht im samtenen Vampir-Umhang über das Nachtleben und lauert auf frisches Helles.',
|
||||
famousQuote: 'Ein Biss, und du ravst für alle Ewigkeit!',
|
||||
themeColor: '#b91c1c'
|
||||
},
|
||||
saskia: {
|
||||
id: 'saskia',
|
||||
name: 'Schwester Saskia',
|
||||
title: 'Die Erstgeborene Schwester',
|
||||
portrait: '/assets/characters/saskia/portrait.jpg',
|
||||
description: 'Wurde auf den ersten Turm in den Wolken verschleppt und ruft nach König Sascha!',
|
||||
famousQuote: 'Sascha, beeil di! Der Turm wackelt im 135-BPM-Takt!',
|
||||
themeColor: '#f43f5e'
|
||||
},
|
||||
maja: {
|
||||
id: 'maja',
|
||||
name: 'Schwester Maja',
|
||||
title: 'Die Zweite Schwester',
|
||||
portrait: '/assets/characters/maja/portrait.jpg',
|
||||
description: 'Gefangen in den Wipfeln des Haumdaucher Himmelsgartens.',
|
||||
famousQuote: 'Hol mi runter, Sascha, i hob an riesen Durst auf a Helles!',
|
||||
themeColor: '#8b5cf6'
|
||||
},
|
||||
miriam: {
|
||||
id: 'miriam',
|
||||
name: 'Schwester Miriam',
|
||||
title: 'Die Jüngste Schwester',
|
||||
portrait: '/assets/characters/miriam/portrait.jpg',
|
||||
description: 'Hockt auf den höchsten Himmelszinnen und wartet auf ihren großen Bruder.',
|
||||
famousQuote: 'Bester Bruder! Alle drei Schwestern gerettet! Jetzt aber zügig zum Schankraum!',
|
||||
themeColor: '#06b6d4'
|
||||
},
|
||||
djs: {
|
||||
id: 'djs',
|
||||
name: 'Die Hof-DJs',
|
||||
@@ -145,3 +172,4 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
themeColor: '#e11d48'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface DialogueChoice {
|
||||
text: string
|
||||
nextId?: string
|
||||
action?: 'addKronkorken' | 'unlockZuluman' | 'unlockBridgerton' | 'startPlatformer' | 'startExpress' | 'startFinale'
|
||||
action?: 'addKronkorken' | 'unlockZuluman' | 'unlockBridgerton' | 'startPlatformer' | 'startExpress' | 'startFinale' | 'resetPrologue'
|
||||
reward?: number
|
||||
reply?: {
|
||||
speakerId: string
|
||||
@@ -13,6 +13,7 @@ export interface DialogueNode {
|
||||
id: string
|
||||
speakerId: string
|
||||
text: string
|
||||
isGameOver?: boolean
|
||||
choices: DialogueChoice[]
|
||||
}
|
||||
|
||||
@@ -24,12 +25,12 @@ export const PROLOGUE_DIALOGUES: Record<string, DialogueNode> = {
|
||||
choices: [
|
||||
{
|
||||
text: 'Zefix Josef! Ohne Kronkorken kein Bier, und ohne Bier kein Bass!',
|
||||
nextId: 'mop_appears',
|
||||
nextId: 'flo_maier_arrives',
|
||||
action: 'unlockBridgerton'
|
||||
},
|
||||
{
|
||||
text: 'Bridgerton?! Josef, konzentrier dich! Wo san meine 40 Kronkorken?!',
|
||||
nextId: 'mop_appears'
|
||||
nextId: 'flo_maier_arrives'
|
||||
},
|
||||
{
|
||||
text: 'Lass mich raten... Du mit deinen Girls ab und zack, Leergut weg?',
|
||||
@@ -45,18 +46,72 @@ export const PROLOGUE_DIALOGUES: Record<string, DialogueNode> = {
|
||||
text: 'Ja mei, woher weißt du das immer?! Wir zogen durch die Lande, die Musik war laut, der Gerstensaft floss in Strömen... und dann hat der Wind die Korken über die Dächer geweht!',
|
||||
choices: [
|
||||
{
|
||||
text: 'Na servus. Dann muss der König wohl selbst ran.',
|
||||
text: 'Na servus. Mal schauen wer da noch auf dem Balkon lauert...',
|
||||
nextId: 'flo_maier_arrives'
|
||||
}
|
||||
]
|
||||
},
|
||||
flo_maier_arrives: {
|
||||
id: 'flo_maier_arrives',
|
||||
speakerId: 'flo_maier',
|
||||
text: 'Habe die Ehre Sascha! Zum 40er zünd i da den ganzen Schlosshof o! A riesigs Freudenfeuer aus Hopfen und Eichenbalken! Des drückt mehr als jeder Bass! Was moanst?!',
|
||||
choices: [
|
||||
{
|
||||
text: 'Ja freilich, zünd ois o, Flo! Freudenfeuer marsch! 🔥',
|
||||
nextId: 'flo_game_over'
|
||||
},
|
||||
{
|
||||
text: 'Na, spinnst du Feierdeifl?! Mei Schloss bleibt stehn, heid wird friedlich geraved!',
|
||||
nextId: 'viktor_arrives'
|
||||
}
|
||||
]
|
||||
},
|
||||
flo_game_over: {
|
||||
id: 'flo_game_over',
|
||||
speakerId: 'flo_maier',
|
||||
text: '💥 WUUUUSCH! Alles steht lichterloh in Flammen! Das Schloss ist Asche, der DJ-Pult geschmolzen und das Bier verdampft. Niemand feiert auf verbrannter Erde!',
|
||||
isGameOver: true,
|
||||
choices: [
|
||||
{
|
||||
text: '🔄 Löschzug rufen & von vorn beginnen',
|
||||
action: 'resetPrologue'
|
||||
}
|
||||
]
|
||||
},
|
||||
viktor_arrives: {
|
||||
id: 'viktor_arrives',
|
||||
speakerId: 'viktor',
|
||||
text: 'Guten Abend, mein König... Wie wär\'s mit einem kleinen Biss in deinen königlichen Hals? Dann ravt die gesamte Sippschaft unsterblich als Partyvampire bis in alle Ewigkeit!',
|
||||
choices: [
|
||||
{
|
||||
text: 'Ja geil, mach mi unsterblich, Viktor! *Hals hinhalten* 🧛',
|
||||
nextId: 'viktor_game_over'
|
||||
},
|
||||
{
|
||||
text: 'Na danke Viktor, heid gibt\'s nur kühles Helles statt Blut!',
|
||||
nextId: 'mop_appears'
|
||||
}
|
||||
]
|
||||
},
|
||||
viktor_game_over: {
|
||||
id: 'viktor_game_over',
|
||||
speakerId: 'viktor',
|
||||
text: '🧛 *CHOMP!* Sascha ist nun ein Partyvampir! Doch ach weh: Es ist strahlender Sonnenschein über Haumdaucher... Du zerfällst zu feinem Glitzerstaub auf den Schlosszinnen!',
|
||||
isGameOver: true,
|
||||
choices: [
|
||||
{
|
||||
text: '🔄 Knoblauch essen & von vorn beginnen',
|
||||
action: 'resetPrologue'
|
||||
}
|
||||
]
|
||||
},
|
||||
mop_appears: {
|
||||
id: 'mop_appears',
|
||||
speakerId: 'mop',
|
||||
text: 'Habe die Ehre, Sascha! Keine Panik, dein Erschaffer hat schon die Sprungfedern in deine königlichen Stiefel programmiert. Sammle mindestens 15 Kronkorken auf den Hopfen-Pfaden, bevor wir in die Taverne einziehen!',
|
||||
text: 'Habe die Ehre, Sascha! Gut, dass das Schloss noch steht. Aber ein Notruf erreichte die Matrix: Deine drei Schwestern Saskia, Maja und Miriam wurden von Kronkorken-Kobolden hoch in die Wolken entführt! Du musst mit deinen Sprungstiefeln von Plattform zu Plattform klettern und alle drei befreien!',
|
||||
choices: [
|
||||
{
|
||||
text: 'Auf geht\'s Mop! Starte den Kronkorken-Kreuzzug!',
|
||||
text: 'Auf geht\'s Mop! Meine Schwestern warten – Kronkorken-Sprungfedern an!',
|
||||
action: 'startPlatformer'
|
||||
},
|
||||
{
|
||||
@@ -68,16 +123,41 @@ export const PROLOGUE_DIALOGUES: Record<string, DialogueNode> = {
|
||||
sascha_ready: {
|
||||
id: 'sascha_ready',
|
||||
speakerId: 'sascha',
|
||||
text: 'Sonnenbrille sitzt. Hermelinmantel weht im Wind. Haumdaucher, der König kommt!',
|
||||
text: 'Sonnenbrille sitzt. Hermelinmantel weht im Wind. Saskia, Maja, Miriam – euer königlicher Bruder holt euch da runter!',
|
||||
choices: [
|
||||
{
|
||||
text: 'Level 1 starten: Die Hopfen-Pfade!',
|
||||
text: 'Akt II starten: Die Schwesternrettung!',
|
||||
action: 'startPlatformer'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export const SISTER_RESCUE_DIALOGUES = {
|
||||
saskia: {
|
||||
id: 'rescue_saskia',
|
||||
speakerId: 'saskia',
|
||||
name: 'Schwester Saskia',
|
||||
text: 'Sascha! Du hast mich gerettet! Die Kobolde wollten mich zwingen, Radler zu trinken! Hier, nimm diesen königlichen Kronkorken! Aber beeil dich: Maja ist noch weiter oben im Himmelsgarten gefangen!',
|
||||
btnText: 'Weiter zu Level 2: Maja retten! ☁️'
|
||||
},
|
||||
maja: {
|
||||
id: 'rescue_maja',
|
||||
speakerId: 'maja',
|
||||
name: 'Schwester Maja',
|
||||
text: 'Königlicher Bruder! Endlich! I hab scho dacht, i verdurste hier oben in den Wolken! Nimm dieses kühle Helle und diesen Kronkorken zur Stärkung. Aber gib Gas: Miriam hockt ganz oben auf den höchsten Zinnen!',
|
||||
btnText: 'Weiter zu Level 3: Miriam retten! 🏰'
|
||||
},
|
||||
miriam: {
|
||||
id: 'rescue_miriam',
|
||||
speakerId: 'miriam',
|
||||
name: 'Schwester Miriam',
|
||||
text: 'JAAAA! Sascha, bester Bruder aller Zeiten! Alle drei Schwestern sind gerettet und in Sicherheit! Jetzt aber nix wie runter in die Taverne zur königlichen Audienz!',
|
||||
btnText: 'Zu Akt III: Der Schankraum! 🍻'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const GSTANZL_DUEL = [
|
||||
{
|
||||
insult: 'Dein Techno-Bass ist so dünn wie schales Exportbier!',
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface GameState {
|
||||
startTime: number
|
||||
elapsedSeconds: number
|
||||
soundMuted: boolean
|
||||
isPaused: boolean
|
||||
}
|
||||
|
||||
export const STORAGE_KEY = 'sasch40_game_save_v1'
|
||||
@@ -34,6 +35,7 @@ function getInitialState(): GameState {
|
||||
startTime: Date.now(),
|
||||
elapsedSeconds: 0,
|
||||
soundMuted: false,
|
||||
isPaused: false,
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined' || !window.localStorage) {
|
||||
@@ -56,7 +58,8 @@ function getInitialState(): GameState {
|
||||
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)
|
||||
soundMuted: Boolean(parsed.soundMuted),
|
||||
isPaused: false
|
||||
}
|
||||
if (state.soundMuted) {
|
||||
sound.setMuted(true)
|
||||
@@ -197,6 +200,14 @@ export const useGameStore = defineStore('game', {
|
||||
this.soundMuted = sound.toggleMute()
|
||||
},
|
||||
|
||||
pauseGame() {
|
||||
this.isPaused = true
|
||||
},
|
||||
|
||||
resumeGame() {
|
||||
this.isPaused = false
|
||||
},
|
||||
|
||||
resetGame() {
|
||||
if (typeof window !== 'undefined' && window.localStorage) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user