feat(sasch40): v4.0 full-screen mobile conversion, Hinter den Bergen room with Liars Dice, beat-only concert audio, and updated art
@@ -25,17 +25,14 @@ This document records the foundational design decisions, their rationale ("Why")
|
||||
|
||||
---
|
||||
|
||||
### 2. Dual Game Modes & Dynamic Chassis Layout (`GameBoyFrame.vue`)
|
||||
* **The Decision**: The GameBoy physical controls (D-Pad and A/B action buttons) MUST dynamically hide and show based on the active game mode.
|
||||
* **Why**: The game alternates between arcade mini-games (Doodle Jump Schwesternrettung, Mittelalter Guitar Hero) and point-and-click / visual-novel adventure scenes (Prologue, Tavern, Finale Certificate). Displaying a chunky D-Pad and large A/B buttons during dialogue or tavern exploration wastes mobile screen space and ruins immersion.
|
||||
### 2. Full-Screen Mobile-First Web App (`GameBoyFrame.vue`)
|
||||
* **The Decision**: The GameBoy bezel chassis, D-Pad, and external A/B action buttons have been completely removed across the entire application in favor of a 100% full-screen responsive mobile layout.
|
||||
* **Why**: Mobile players interact directly with high-fidelity touch controls on modern smartphone screens. Virtual GameBoy bezels and D-Pads wasted over 40% of the viewport and degraded playability in arcade and visual-novel acts.
|
||||
* **The Rule**:
|
||||
* **Arcade Acts** (`currentAct === 'platformer'` or `'party_express'`):
|
||||
* Show the GameBoy D-Pad and A/B action buttons (`arcade-layout`).
|
||||
* Display the arcade HUD with Korken, Helles, and 135 BPM counter.
|
||||
* **Adventure / Dialogue Acts** (`currentAct === 'intro'`, `'prologue'`, `'tavern'`, or `'finale'`):
|
||||
* Hide the D-Pad and A/B buttons completely (`adventure-layout`).
|
||||
* Display a sleek top-right settings button (`⚙️`) offering pause, mute/unmute, and game reset.
|
||||
* On `currentAct === 'finale'`, hide even the mini bar so the certificate occupies the full screen.
|
||||
* The app container fills 100% of viewport height and width.
|
||||
* **Header**: Line 1 displays `KÖNIG SASCHAS KRONKORKENKREUZZUG`, Line 2 displays the act badge (e.g. `AKT III`). The right side displays inventory counts (`👑`, `🍻`) and the persistent `⚙️` settings icon.
|
||||
* **Controls**: Act II uses tap/touch screen halves (`left`/`right`) or arrow keys; Act IV uses full-width on-screen highway strum buttons (`[D LINKS]`, `[G MITTE]`, `[A RECHTS]`).
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -67,6 +64,23 @@ This document records the foundational design decisions, their rationale ("Why")
|
||||
|
||||
---
|
||||
|
||||
### 2e. Akt III: Raum „Hinter den Bergen“ & Liar's Dice (`TavernView.vue`)
|
||||
* **The Decision**: Akt III features a special mountain hideaway scene *„Hinter den Bergen“* accessible via Hinterland signpost or tab bar.
|
||||
* **Why**: Introduces Saschas father Papa Markus and his Taiwanese connection with Gaoliang drinking culture into the anniversary narrative.
|
||||
* **Mechanics**:
|
||||
* Papa Markus sits at a table with 6 Taiwanese friends in an alpine valley nested with Taiwanese/Tokyo skyscrapers.
|
||||
* Minigame **Liar's Dice (Chui Niu / 吹牛)**: 5 dice each, 1s are wild, sequential bidding, bluff-calling reveals, loser drinks a 58% Kinmen Gaoliang shot (`🍶`).
|
||||
* Rewards: +10 Kronkorken, inventory item *58% Kinmen Gaoliang*, Easter Egg title `taiwan_dice_champion`.
|
||||
|
||||
---
|
||||
|
||||
### 2f. Akt IV: Beat-Only Metronom & Harmonische Lautenakkorde (`PartyExpressView.vue` & `synthAudio.ts`)
|
||||
* **The Decision**: During the Castle Stage rock concert in Act IV, the synthesizer BGM switches to `beat_only` mode (melody muted, playing solely the 135-BPM kick/hi-hat rhythm).
|
||||
* **Why**: Playing a rhythm game while a pre-recorded synthesizer melody plays in parallel creates cacophony and ruins the illusion that the player's notes compose the music.
|
||||
* **The Rule**: Successful hits on D, G, A trigger full 3-note harmonic medieval chords; misses trigger a muted plink. On act completion or exit, BGM restores to `full`.
|
||||
|
||||
---
|
||||
|
||||
### 3. Protagonist System & Extensibility (`characters.ts`)
|
||||
* **The Decision**: All character portraits are strictly 1:1 square pixel-art illustrations based directly on real-life friends and family photos, maintained in a centralized registry.
|
||||
* **Why**: Guests at the party must immediately recognize themselves and their friends. Previous attempts at generic AI portraits caused confusion because hair, beards, and styles did not match the actual people.
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 626 KiB |
|
After Width: | Height: | Size: 581 KiB |
|
After Width: | Height: | Size: 690 KiB |
|
Before Width: | Height: | Size: 824 KiB After Width: | Height: | Size: 875 KiB |
|
After Width: | Height: | Size: 875 KiB |
|
Before Width: | Height: | Size: 979 KiB After Width: | Height: | Size: 798 KiB |
|
After Width: | Height: | Size: 798 KiB |
@@ -308,6 +308,92 @@ async function runPlaytest() {
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
}
|
||||
|
||||
// Test Münchanien Signpost Hint Dialog
|
||||
console.log('🪧 Testing Münchanien Signpost Hint...');
|
||||
const signBtn = await page.$('.hotspot-sign-munchanien');
|
||||
if (signBtn) {
|
||||
await signBtn.click();
|
||||
await new Promise(r => setTimeout(r, 700));
|
||||
console.log('📸 11b: Capturing Münchanien Signpost Hint Modal...');
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11b_tavern_munchanien_hint.png') });
|
||||
const hintCloseBtn = await page.$('.intro-modal-box .action-btn');
|
||||
if (hintCloseBtn) await hintCloseBtn.click();
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
}
|
||||
|
||||
// Test Hinter den Bergen (Taiwan Valley & Papa Markus)
|
||||
console.log('🏮 Navigating to Hinter den Bergen...');
|
||||
const hdbDoor = await page.$('.hotspot-door-hinter-den-bergen');
|
||||
if (hdbDoor) {
|
||||
await hdbDoor.click();
|
||||
} else {
|
||||
const hdbTab = await page.$('.room-tab:nth-child(5)');
|
||||
if (hdbTab) await hdbTab.click();
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
console.log('📸 11c: Capturing Hinter den Bergen Scene (Taiwan Alpine Skyline & Papa Markus)...');
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11c_tavern_hinter_den_bergen.png') });
|
||||
|
||||
// Click Markus for Liar's Dice
|
||||
const markusBtn = await page.$('.hotspot-markus');
|
||||
if (markusBtn) {
|
||||
await markusBtn.click();
|
||||
await new Promise(r => setTimeout(r, 700));
|
||||
console.log('📸 11d: Capturing Papa Markus Dialog...');
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11d_tavern_markus_dialog.png') });
|
||||
|
||||
// Click "🎲 Auf geht's Papa, Liar's Dice spielen!" -> opens intro modal
|
||||
const startDiceIntroBtn = await page.$('.dialog-actions .action-btn');
|
||||
if (startDiceIntroBtn) {
|
||||
await startDiceIntroBtn.click();
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
console.log('📸 11e: Capturing Liar\'s Dice INTRO Modal...');
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11e_tavern_liars_dice_intro.png') });
|
||||
|
||||
// Start Liar's Dice Match
|
||||
const startMatchBtn = await page.$('.start-duel-action-btn');
|
||||
if (startMatchBtn) {
|
||||
await startMatchBtn.click();
|
||||
await new Promise(r => setTimeout(r, 1200));
|
||||
console.log('📸 11f: Capturing Liar\'s Dice Initial Round View...');
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11f_tavern_liars_dice_gameplay.png') });
|
||||
|
||||
// Submit player bid
|
||||
const bidBtn = await page.$('.bid-action-btn');
|
||||
if (bidBtn) {
|
||||
await bidBtn.click();
|
||||
await new Promise(r => setTimeout(r, 1500)); // wait for Markus response
|
||||
console.log('📸 11g: Capturing Liar\'s Dice Markus Counter-Bid...');
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11g_tavern_liars_dice_markus_turn.png') });
|
||||
}
|
||||
|
||||
// Call bluff
|
||||
const bluffBtn = await page.$('.call-bluff-btn');
|
||||
if (bluffBtn) {
|
||||
await bluffBtn.click();
|
||||
await new Promise(r => setTimeout(r, 1200)); // wait for shot animation
|
||||
console.log('📸 11h: Capturing Liar\'s Dice Bluff Resolution & Gaoliang Shot...');
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11h_tavern_liars_dice_bluff_call.png') });
|
||||
}
|
||||
|
||||
// Close modal
|
||||
const closeDiceBtn = await page.$('.liars-dice-box .duel-close-btn');
|
||||
if (closeDiceBtn) {
|
||||
await closeDiceBtn.click();
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return to Hinterland
|
||||
const backToHinterland = await page.$('.hotspot-door-back-hinterland');
|
||||
if (backToHinterland) {
|
||||
await backToHinterland.click();
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
}
|
||||
|
||||
// Test Gstanzl Duel Intro & Gameplay
|
||||
console.log('⚔️ Testing Gstanzl-Fechten Intro & Minigame...');
|
||||
const duelBtn = await page.$('.duel-trigger');
|
||||
|
||||
@@ -244,37 +244,119 @@ class SoundEngine {
|
||||
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
|
||||
// Missed clunk / muted buzz ('plink')
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
osc.type = 'sawtooth'
|
||||
osc.frequency.setValueAtTime(90, now)
|
||||
gain.gain.setValueAtTime(0.12, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12)
|
||||
osc.frequency.setValueAtTime(110, now)
|
||||
osc.frequency.exponentialRampToValueAtTime(45, now + 0.08)
|
||||
gain.gain.setValueAtTime(0.15, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08)
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(now)
|
||||
osc.stop(now + 0.08)
|
||||
} 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)
|
||||
// Resonant medieval lute chords (2 harmonic notes for rich sound)
|
||||
const chordRoots = [
|
||||
[293.66, 440.00], // D4 + A4 (Saite 1 - Deep resonant D power)
|
||||
[392.00, 587.33], // G4 + D5 (Saite 2 - Bright G medieval fifth)
|
||||
[440.00, 659.25] // A4 + E5 (Saite 3 - High piercing A rock lute)
|
||||
]
|
||||
const notes = chordRoots[lane % 3] || [392.00, 587.33]
|
||||
|
||||
gain.gain.setValueAtTime(0.28, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.32)
|
||||
notes.forEach((freq, idx) => {
|
||||
if (!this.ctx) return
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
osc.type = 'triangle'
|
||||
osc.frequency.setValueAtTime(freq, now + idx * 0.012)
|
||||
|
||||
gain.gain.setValueAtTime(0.24, now + idx * 0.012)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.38)
|
||||
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(now + idx * 0.012)
|
||||
osc.stop(now + 0.38)
|
||||
})
|
||||
}
|
||||
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
|
||||
osc.start(now)
|
||||
osc.stop(now + (success ? 0.32 : 0.12))
|
||||
}
|
||||
|
||||
// SFX: Liar's Dice - Shake dice in leather cup
|
||||
public playDiceShake() {
|
||||
if (this.isMuted) return
|
||||
this.initCtx()
|
||||
if (!this.ctx) return
|
||||
|
||||
const now = this.ctx.currentTime
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const t = now + i * 0.06
|
||||
const osc = this.ctx.createOscillator()
|
||||
const gain = this.ctx.createGain()
|
||||
osc.type = 'square'
|
||||
osc.frequency.setValueAtTime(180 + Math.random() * 120, t)
|
||||
gain.gain.setValueAtTime(0.12, t)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.04)
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(t)
|
||||
osc.stop(t + 0.04)
|
||||
}
|
||||
}
|
||||
|
||||
// SFX: Liar's Dice - Cup slam on tavern wood table
|
||||
public playCupSlam() {
|
||||
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(110, now)
|
||||
osc.frequency.exponentialRampToValueAtTime(30, now + 0.12)
|
||||
gain.gain.setValueAtTime(0.35, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15)
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(now)
|
||||
osc.stop(now + 0.15)
|
||||
}
|
||||
|
||||
// SFX: Gaoliang Shot Pour / Clink
|
||||
public playGaoliangShot() {
|
||||
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 = 'sine'
|
||||
osc.frequency.setValueAtTime(1760, now) // A6 glass clink
|
||||
osc.frequency.setValueAtTime(2637, now + 0.04) // E7
|
||||
gain.gain.setValueAtTime(0.2, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4)
|
||||
osc.connect(gain)
|
||||
gain.connect(this.ctx.destination)
|
||||
osc.start(now)
|
||||
osc.stop(now + 0.4)
|
||||
}
|
||||
|
||||
private bgmMode: 'full' | 'beat_only' = 'full'
|
||||
|
||||
// 135 BPM Medieval Techno Chiptune Beat
|
||||
public startBGM() {
|
||||
if (this.isMuted || this.bgmPlaying) return
|
||||
public startBGM(mode: 'full' | 'beat_only' = 'full') {
|
||||
this.bgmMode = mode
|
||||
if (this.isMuted) return
|
||||
if (this.bgmPlaying) {
|
||||
// If already playing, just update mode
|
||||
return
|
||||
}
|
||||
this.initCtx()
|
||||
this.bgmPlaying = true
|
||||
|
||||
@@ -340,47 +422,50 @@ class SoundEngine {
|
||||
hatNode.start(now)
|
||||
}
|
||||
|
||||
// Bassline (sawtooth filtered)
|
||||
const bassFreq = bassline[this.step % bassline.length]
|
||||
if (bassFreq > 0) {
|
||||
const bassOsc = this.ctx.createOscillator()
|
||||
const bassGain = this.ctx.createGain()
|
||||
const bassFilter = this.ctx.createBiquadFilter()
|
||||
// ONLY PLAY BASSLINE & MELODY IF NOT IN BEAT-ONLY MODE!
|
||||
if (this.bgmMode === 'full') {
|
||||
// Bassline (sawtooth filtered)
|
||||
const bassFreq = bassline[this.step % bassline.length]
|
||||
if (bassFreq > 0) {
|
||||
const bassOsc = this.ctx.createOscillator()
|
||||
const bassGain = this.ctx.createGain()
|
||||
const bassFilter = this.ctx.createBiquadFilter()
|
||||
|
||||
bassOsc.type = 'sawtooth'
|
||||
bassOsc.frequency.setValueAtTime(bassFreq, now)
|
||||
bassOsc.type = 'sawtooth'
|
||||
bassOsc.frequency.setValueAtTime(bassFreq, now)
|
||||
|
||||
bassFilter.type = 'lowpass'
|
||||
bassFilter.frequency.setValueAtTime(450, now)
|
||||
bassFilter.type = 'lowpass'
|
||||
bassFilter.frequency.setValueAtTime(450, now)
|
||||
|
||||
bassGain.gain.setValueAtTime(0.12, now)
|
||||
bassGain.gain.exponentialRampToValueAtTime(0.001, now + 0.2)
|
||||
bassGain.gain.setValueAtTime(0.12, now)
|
||||
bassGain.gain.exponentialRampToValueAtTime(0.001, now + 0.2)
|
||||
|
||||
bassOsc.connect(bassFilter)
|
||||
bassFilter.connect(bassGain)
|
||||
bassGain.connect(this.ctx.destination)
|
||||
bassOsc.connect(bassFilter)
|
||||
bassFilter.connect(bassGain)
|
||||
bassGain.connect(this.ctx.destination)
|
||||
|
||||
bassOsc.start(now)
|
||||
bassOsc.stop(now + 0.2)
|
||||
}
|
||||
bassOsc.start(now)
|
||||
bassOsc.stop(now + 0.2)
|
||||
}
|
||||
|
||||
// Medieval Lute Lead Melody
|
||||
const melFreq = melody[this.step % melody.length]
|
||||
if (melFreq > 0) {
|
||||
const melOsc = this.ctx.createOscillator()
|
||||
const melGain = this.ctx.createGain()
|
||||
// Medieval Lute Lead Melody
|
||||
const melFreq = melody[this.step % melody.length]
|
||||
if (melFreq > 0) {
|
||||
const melOsc = this.ctx.createOscillator()
|
||||
const melGain = this.ctx.createGain()
|
||||
|
||||
melOsc.type = 'square'
|
||||
melOsc.frequency.setValueAtTime(melFreq, now)
|
||||
melOsc.type = 'square'
|
||||
melOsc.frequency.setValueAtTime(melFreq, now)
|
||||
|
||||
melGain.gain.setValueAtTime(0.08, now)
|
||||
melGain.gain.exponentialRampToValueAtTime(0.001, now + 0.18)
|
||||
melGain.gain.setValueAtTime(0.08, now)
|
||||
melGain.gain.exponentialRampToValueAtTime(0.001, now + 0.18)
|
||||
|
||||
melOsc.connect(melGain)
|
||||
melGain.connect(this.ctx.destination)
|
||||
melOsc.connect(melGain)
|
||||
melGain.connect(this.ctx.destination)
|
||||
|
||||
melOsc.start(now)
|
||||
melOsc.stop(now + 0.18)
|
||||
melOsc.start(now)
|
||||
melOsc.stop(now + 0.18)
|
||||
}
|
||||
}
|
||||
|
||||
this.step++
|
||||
@@ -390,6 +475,13 @@ class SoundEngine {
|
||||
playLoop()
|
||||
}
|
||||
|
||||
public setBGMMode(mode: 'full' | 'beat_only') {
|
||||
this.bgmMode = mode
|
||||
if (!this.bgmPlaying && !this.isMuted) {
|
||||
this.startBGM(mode)
|
||||
}
|
||||
}
|
||||
|
||||
public stopBGM() {
|
||||
this.bgmPlaying = false
|
||||
if (this.bgmTimer) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useGameStore } from '../stores/gameStore'
|
||||
import { sound } from '../audio/synthAudio'
|
||||
import IntroView from './IntroView.vue'
|
||||
@@ -10,35 +10,17 @@ import PartyExpressView from './PartyExpressView.vue'
|
||||
import CertificateModal from './CertificateModal.vue'
|
||||
|
||||
const store = useGameStore()
|
||||
const platformerRef = ref<InstanceType<typeof PlatformerView> | null>(null)
|
||||
const concertRef = ref<InstanceType<typeof PartyExpressView> | null>(null)
|
||||
|
||||
function handleJump() {
|
||||
if (store.currentAct === 'platformer' && platformerRef.value) {
|
||||
platformerRef.value.onJump()
|
||||
} else if (store.currentAct === 'party_express' && concertRef.value) {
|
||||
concertRef.value.hitLane(2)
|
||||
const actBadge = computed(() => {
|
||||
switch (store.currentAct) {
|
||||
case 'prologue': return 'AKT I'
|
||||
case 'platformer': return 'AKT II'
|
||||
case 'tavern': return 'AKT III'
|
||||
case 'party_express': return 'AKT IV'
|
||||
case 'finale': return 'AKT V'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function toggleAudio() {
|
||||
store.toggleMute()
|
||||
@@ -68,557 +50,273 @@ function resetGameFromSettings() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gameboy-chassis" :class="{ 'arcade-layout': store.isArcadeMode, 'adventure-layout': !store.isArcadeMode }">
|
||||
<div class="app-container">
|
||||
<!-- Top Royal Header -->
|
||||
<div class="chassis-top">
|
||||
<div class="speaker-grill-left">
|
||||
<span></span><span></span><span></span>
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<span class="header-brand-title">KÖNIG SASCHAS KRONKORKENKREUZZUG</span>
|
||||
<span v-if="actBadge" class="header-act-badge">{{ actBadge }}</span>
|
||||
</div>
|
||||
<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' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
<button class="settings-toggle-btn" @click="openSettings" title="Einstellungen & Pause" aria-label="Einstellungen">
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Active Screen Viewport (Takes full height in Adventure / Role Play mode!) -->
|
||||
<div class="screen-bezel" :class="{ 'fullscreen-bezel': !store.isArcadeMode }">
|
||||
<div class="screen-glass">
|
||||
<IntroView v-if="store.currentAct === 'intro'" />
|
||||
<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'" ref="concertRef" />
|
||||
<CertificateModal v-else-if="store.currentAct === 'finale'" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ARCADE CONTROLS ONLY: Rendered during Jump&Run / Speed-Runner -->
|
||||
<template v-if="store.isArcadeMode">
|
||||
<!-- Medieval Stats Parchment Scroll -->
|
||||
<div class="parchment-scroll">
|
||||
<div class="parchment-content">
|
||||
<span class="stat-badge">👑 KORKEN: <strong>{{ store.kronkorken }}</strong></span>
|
||||
<span class="stat-divider">|</span>
|
||||
<span class="stat-badge">🍻 HELLES: <strong>{{ store.beers }}</strong></span>
|
||||
<span class="stat-divider">|</span>
|
||||
<span class="stat-badge">🔊 <strong>135 BPM</strong></span>
|
||||
<div class="header-right">
|
||||
<div v-if="store.currentAct !== 'intro' && store.currentAct !== 'finale'" class="header-stats">
|
||||
<span class="stat-pill">👑 {{ store.kronkorken }}</span>
|
||||
<span class="stat-pill">🍻 {{ store.beers }}</span>
|
||||
</div>
|
||||
<button class="settings-toggle-btn" @click="openSettings" title="Einstellungen & Pause" aria-label="Einstellungen">
|
||||
⚙️
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Handheld Controller Lower Body -->
|
||||
<div class="chassis-controls">
|
||||
<!-- D-PAD -->
|
||||
<div class="dpad-wrapper">
|
||||
<div class="dpad">
|
||||
<button class="dpad-btn dpad-up" disabled></button>
|
||||
<button
|
||||
class="dpad-btn dpad-left"
|
||||
@touchstart.prevent="handleDpad('left', true)"
|
||||
@touchend.prevent="handleDpad('left', false)"
|
||||
@mousedown="handleDpad('left', true)"
|
||||
@mouseup="handleDpad('left', false)"
|
||||
>◀</button>
|
||||
<div class="dpad-center"></div>
|
||||
<button
|
||||
class="dpad-btn dpad-right"
|
||||
@touchstart.prevent="handleDpad('right', true)"
|
||||
@touchend.prevent="handleDpad('right', false)"
|
||||
@mousedown="handleDpad('right', true)"
|
||||
@mouseup="handleDpad('right', false)"
|
||||
>▶</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>
|
||||
<!-- Main Active Viewport (100% full screen responsive) -->
|
||||
<main class="app-viewport">
|
||||
<IntroView v-if="store.currentAct === 'intro'" />
|
||||
<PrologueView v-else-if="store.currentAct === 'prologue'" />
|
||||
<PlatformerView v-else-if="store.currentAct === 'platformer'" />
|
||||
<TavernView v-else-if="store.currentAct === 'tavern'" />
|
||||
<PartyExpressView v-else-if="store.currentAct === 'party_express'" />
|
||||
<CertificateModal v-else-if="store.currentAct === 'finale'" />
|
||||
</main>
|
||||
|
||||
<!-- Center Logo & Utility Buttons -->
|
||||
<div class="center-controls">
|
||||
<div class="royal-crest">👑</div>
|
||||
<div class="pill-buttons">
|
||||
<button class="pill-btn" @click="openSettings">
|
||||
<span class="pill-key">SELECT</span>
|
||||
<span class="pill-sub">PAUSE</span>
|
||||
</button>
|
||||
<button class="pill-btn" @click="toggleAudio">
|
||||
<span class="pill-key">START</span>
|
||||
<span class="pill-sub">BASS</span>
|
||||
</button>
|
||||
<!-- Settings Modal -->
|
||||
<transition name="fade">
|
||||
<div v-if="settingsOpen" class="settings-modal-overlay" @click.self="closeSettings">
|
||||
<div class="settings-modal-card">
|
||||
<div class="settings-modal-header">
|
||||
<div class="settings-title-group">
|
||||
<span class="settings-icon">⚙️</span>
|
||||
<h3 class="settings-title">SPIELPAUSE & OPTIONEN</h3>
|
||||
</div>
|
||||
<button class="settings-close-x" @click="closeSettings" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons A & B -->
|
||||
<div class="action-buttons-wrapper">
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
class="action-round-btn btn-b"
|
||||
@click="handleButtonB"
|
||||
>
|
||||
<span class="btn-letter">B</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-round-btn btn-a"
|
||||
@click="handleJump"
|
||||
@touchstart.prevent="handleJump"
|
||||
>
|
||||
<span class="btn-letter">A</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="action-labels">
|
||||
<span>{{ store.currentAct === 'party_express' ? 'SAITE G (B)' : "O'ZAPFT (B)" }}</span>
|
||||
<span>{{ store.currentAct === 'party_express' ? 'SAITE A (A)' : 'SPRUNG (A)' }}</span>
|
||||
<div class="settings-body">
|
||||
<p class="settings-lore">
|
||||
Das Spiel ist pausiert. Trinke einen Schluck Helles oder passe deine königlichen Einstellungen an!
|
||||
</p>
|
||||
|
||||
<div class="settings-actions">
|
||||
<!-- Sound Toggle -->
|
||||
<button class="settings-btn sound-btn" @click="toggleAudio">
|
||||
<span class="btn-icon">{{ store.soundMuted ? '🔇' : '🔊' }}</span>
|
||||
<span class="btn-text">Sound: <strong>{{ store.soundMuted ? 'AUS' : 'AN (135 BPM)' }}</strong></span>
|
||||
</button>
|
||||
|
||||
<!-- Resume Game -->
|
||||
<button class="settings-btn resume-btn" @click="closeSettings">
|
||||
<span class="btn-icon">▶️</span>
|
||||
<span class="btn-text">Weiterspielen</span>
|
||||
</button>
|
||||
|
||||
<!-- Reset Game -->
|
||||
<button class="settings-btn reset-btn" @click="resetGameFromSettings">
|
||||
<span class="btn-icon">🔄</span>
|
||||
<span class="btn-text">Spielstand zurücksetzen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Mini Bottom Status Bar for Adventure/Role Play Mode (Hidden on Finale Certificate) -->
|
||||
<div v-else-if="store.currentAct !== 'finale'" class="adventure-bottom-bar">
|
||||
<div class="adv-stat">
|
||||
<span class="adv-icon">👑</span>
|
||||
<span class="adv-val">{{ store.kronkorken }} Korken</span>
|
||||
</div>
|
||||
<div class="adv-stat">
|
||||
<span class="adv-icon">🍻</span>
|
||||
<span class="adv-val">{{ store.beers }} Helles</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gameboy-chassis {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
.app-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(180deg, #3d2214 0%, #26140b 100%);
|
||||
border: 4px solid #b45309;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.9);
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Top Trim */
|
||||
.chassis-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: #2b1810;
|
||||
border-bottom: 2px solid #78350f;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chassis-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
background-color: #0c0a09;
|
||||
color: #fef3c7;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6.8px;
|
||||
color: #ffd700;
|
||||
letter-spacing: 0.2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.brand-act {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 5.5px;
|
||||
color: #f59e0b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.speaker-grill-left {
|
||||
/* Header */
|
||||
.app-header {
|
||||
height: 48px;
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
background: linear-gradient(180deg, #1c1917 0%, #141210 100%);
|
||||
border-bottom: 2px solid #78350f;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.speaker-grill-left span {
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
background: #1a0f0a;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.settings-toggle-btn {
|
||||
background: rgba(120, 53, 15, 0.7);
|
||||
color: #ffd700;
|
||||
border: 1px solid #ffd700;
|
||||
border-radius: 4px;
|
||||
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 */
|
||||
.screen-bezel {
|
||||
flex: 1;
|
||||
background: #17100b;
|
||||
margin: 6px 8px 4px 8px;
|
||||
border-radius: 8px;
|
||||
border: 3px solid #1a0f0a;
|
||||
box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.9);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fullscreen-bezel {
|
||||
margin: 4px 6px 0 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.screen-glass {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Adventure Bottom Bar */
|
||||
.adventure-bottom-bar {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
background: #2b1810;
|
||||
border-top: 2px solid #78350f;
|
||||
padding: 6px 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.adv-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.adv-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.adv-val {
|
||||
.header-brand-title {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 8px;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
/* Parchment Status Bar */
|
||||
.parchment-scroll {
|
||||
background: #fdf6e2;
|
||||
border: 2px solid #b45309;
|
||||
border-radius: 4px;
|
||||
margin: 4px 10px;
|
||||
padding: 4px 6px;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.parchment-content {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7.5px;
|
||||
color: #78350f;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
/* Controls Section (Arcade Mode Only) */
|
||||
.chassis-controls {
|
||||
height: 130px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 12px 10px 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* D-PAD */
|
||||
.dpad-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.dpad {
|
||||
position: relative;
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
.dpad-center {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: #78350f;
|
||||
}
|
||||
|
||||
.dpad-btn {
|
||||
position: absolute;
|
||||
background: #854d0e;
|
||||
border: 1.5px solid #d4af37;
|
||||
color: #fef08a;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dpad-btn:active {
|
||||
background: #b45309;
|
||||
}
|
||||
|
||||
.dpad-up {
|
||||
top: 0; left: 28px; width: 28px; height: 28px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.dpad-down {
|
||||
bottom: 0; left: 28px; width: 28px; height: 28px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.dpad-left {
|
||||
top: 28px; left: 0; width: 28px; height: 28px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.dpad-right {
|
||||
top: 28px; right: 0; width: 28px; height: 28px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6px;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
/* Center */
|
||||
.center-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.royal-crest {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.pill-buttons {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pill-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #1c1917;
|
||||
border: 1px solid #78350f;
|
||||
border-radius: 12px;
|
||||
padding: 3px 6px;
|
||||
cursor: pointer;
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
.pill-key {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 5px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.pill-sub {
|
||||
font-size: 7px;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.action-buttons-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
position: relative;
|
||||
width: 84px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.action-round-btn {
|
||||
position: absolute;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ffd700;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.action-round-btn:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.btn-b {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #eab308;
|
||||
}
|
||||
|
||||
.btn-a {
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-letter {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
color: #f59e0b;
|
||||
letter-spacing: 0.3px;
|
||||
white-space: nowrap;
|
||||
text-shadow: 1px 1px 0 #000;
|
||||
}
|
||||
|
||||
.action-labels {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 5.5px;
|
||||
color: #d4af37;
|
||||
@media (min-width: 640px) {
|
||||
.header-brand-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Settings & Pause Modal */
|
||||
.settings-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
.header-act-badge {
|
||||
font-family: monospace;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background-color: #451a03;
|
||||
color: #fde68a;
|
||||
border: 1px solid #b45309;
|
||||
white-space: nowrap;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.stat-pill {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 9px;
|
||||
color: #fef08a;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #78350f;
|
||||
}
|
||||
|
||||
.settings-toggle-btn {
|
||||
background: #292524;
|
||||
border: 1px solid #78350f;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #fbbf24;
|
||||
transition: transform 0.1s, background-color 0.15s;
|
||||
}
|
||||
|
||||
.settings-toggle-btn:hover {
|
||||
background: #44403c;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.settings-toggle-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Main Viewport */
|
||||
.app-viewport {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: calc(100% - 48px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 250;
|
||||
padding: 16px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: #2b1810;
|
||||
border: 3px solid #d4af37;
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
color: #fef3c7;
|
||||
box-shadow: 0 0 24px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
/* Settings Modal */
|
||||
.settings-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1.5px solid #78350f;
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.settings-modal-card {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
background: #1c1917;
|
||||
border: 2px solid #b45309;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.9);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #292524;
|
||||
border-bottom: 1px solid #78350f;
|
||||
}
|
||||
|
||||
.settings-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 8px;
|
||||
color: #ffd700;
|
||||
margin: 0;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 10px;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.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;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #a8a29e;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
transition: all 0.15s ease;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.settings-close-x:hover {
|
||||
background: #dc2626;
|
||||
transform: scale(1.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.settings-paused-hint {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6.5px;
|
||||
color: #38bdf8;
|
||||
.settings-body {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.settings-lore {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #d6d3d1;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
@@ -628,43 +326,49 @@ function resetGameFromSettings() {
|
||||
}
|
||||
|
||||
.settings-btn {
|
||||
background: #3b2014;
|
||||
color: #fef08a;
|
||||
border: 1.5px solid #b45309;
|
||||
border-radius: 6px;
|
||||
padding: 10px 8px;
|
||||
font-family: 'MedievalSharp', serif;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.15s ease;
|
||||
transition: transform 0.1s, opacity 0.15s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.settings-btn:hover {
|
||||
background: #78350f;
|
||||
border-color: #ffd700;
|
||||
.settings-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.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;
|
||||
.sound-btn {
|
||||
background: #3b82f6;
|
||||
color: #ffffff;
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
.resume-btn {
|
||||
background: #15803d;
|
||||
border-color: #86efac;
|
||||
color: #fff;
|
||||
color: #ffffff;
|
||||
border-color: #22c55e;
|
||||
}
|
||||
|
||||
.resume-btn:hover {
|
||||
background: #16a34a;
|
||||
.reset-btn {
|
||||
background: #7f1d1d;
|
||||
color: #fca5a5;
|
||||
border-color: #dc2626;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -136,6 +136,7 @@ function generateNoteChart() {
|
||||
|
||||
function startConcert() {
|
||||
sound.playDialogueBlip()
|
||||
sound.setBGMMode('beat_only')
|
||||
showConcertIntro.value = false
|
||||
notes = generateNoteChart()
|
||||
startTime = performance.now()
|
||||
@@ -224,6 +225,7 @@ function spawnParticles(x: number, y: number, color: string, count = 8) {
|
||||
|
||||
function finishConcert() {
|
||||
concertFinished.value = true
|
||||
sound.setBGMMode('full')
|
||||
sound.playFanfare()
|
||||
store.serveGuest()
|
||||
store.serveGuest()
|
||||
@@ -444,6 +446,7 @@ onMounted(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
sound.setBGMMode('full')
|
||||
if (animId) cancelAnimationFrame(animId)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { sound } from '../audio/synthAudio'
|
||||
|
||||
const store = useGameStore()
|
||||
|
||||
type TavernRoom = 'schankraum' | 'nebenraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer'
|
||||
type TavernRoom = 'schankraum' | 'nebenraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer' | 'hinter_den_bergen'
|
||||
const currentRoom = ref<TavernRoom>('schankraum')
|
||||
|
||||
interface ActiveDialog {
|
||||
@@ -309,9 +309,242 @@ function openCharacterDialog(charId: string) {
|
||||
activeDialog.value = null
|
||||
}
|
||||
}
|
||||
} else if (charId === 'markus') {
|
||||
activeDialog.value = {
|
||||
characterId: charId,
|
||||
name: char.name,
|
||||
title: char.title,
|
||||
portrait: char.portrait,
|
||||
text: 'Servus mein Sohn! Schau di an, 40 Jahre und herrschst über ganz Haumdaucher! Meine taiwanischen Freunde und ich trinken 58% Kinmen-Gaoliang und spielen Liar\'s Dice (Chui Niu / 吹牛)! Traust du dich gegen deinen alten Herrn anzutreten? Wer verliert, kippt einen Kurzen!',
|
||||
actionLabel: '🎲 Auf geht\'s Papa, Liar\'s Dice spielen!',
|
||||
actionFn: () => {
|
||||
activeDialog.value = null
|
||||
openLiarsDiceIntro()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Münchanien Hint Modal
|
||||
const munchanienHintActive = ref(false)
|
||||
function openMunchanienHint() {
|
||||
sound.playDialogueBlip()
|
||||
munchanienHintActive.value = true
|
||||
}
|
||||
|
||||
// Liar's Dice (Chui Niu / 吹牛) State & Logic
|
||||
const liarsDiceIntroOpen = ref(false)
|
||||
const liarsDiceActive = ref(false)
|
||||
const liarsDiceGameOver = ref(false)
|
||||
const liarsDicePlayerWon = ref(false)
|
||||
const liarsDiceRound = ref(1)
|
||||
const liarsDicePlayerWins = ref(0)
|
||||
const liarsDiceMarkusWins = ref(0)
|
||||
const playerDice = ref<number[]>([1, 2, 3, 4, 5])
|
||||
const markusDice = ref<number[]>([1, 2, 3, 4, 5])
|
||||
const markusDiceRevealed = ref(false)
|
||||
const currentBid = ref<{ count: number; value: number } | null>(null)
|
||||
const lastBidder = ref<'player' | 'markus' | null>(null)
|
||||
const liarsDiceMsg = ref('')
|
||||
const selectedBidCount = ref(2)
|
||||
const selectedBidValue = ref(2)
|
||||
const isShaking = ref(false)
|
||||
const shotAnimation = ref<'sascha' | 'markus' | null>(null)
|
||||
const isRoundEvaluating = ref(false)
|
||||
|
||||
function dieIcon(val: number): string {
|
||||
const icons = ['', '⚀', '⚁', '⚂', '⚃', '⚄', '⚅']
|
||||
return icons[val] || '🎲'
|
||||
}
|
||||
|
||||
function openLiarsDiceIntro() {
|
||||
sound.playDialogueBlip()
|
||||
liarsDiceIntroOpen.value = true
|
||||
}
|
||||
|
||||
function startLiarsDiceMatch() {
|
||||
sound.playDialogueBlip()
|
||||
liarsDiceIntroOpen.value = false
|
||||
liarsDiceActive.value = true
|
||||
liarsDiceGameOver.value = false
|
||||
liarsDicePlayerWon.value = false
|
||||
liarsDicePlayerWins.value = 0
|
||||
liarsDiceMarkusWins.value = 0
|
||||
liarsDiceRound.value = 1
|
||||
startNewDiceRound()
|
||||
}
|
||||
|
||||
function startNewDiceRound() {
|
||||
sound.playDiceShake()
|
||||
isShaking.value = true
|
||||
markusDiceRevealed.value = false
|
||||
currentBid.value = null
|
||||
lastBidder.value = null
|
||||
shotAnimation.value = null
|
||||
isRoundEvaluating.value = false
|
||||
selectedBidCount.value = 2
|
||||
selectedBidValue.value = 2
|
||||
|
||||
// Generate 5 random dice each (1 to 6)
|
||||
playerDice.value = Array.from({ length: 5 }, () => Math.floor(Math.random() * 6) + 1).sort((a, b) => a - b)
|
||||
markusDice.value = Array.from({ length: 5 }, () => Math.floor(Math.random() * 6) + 1).sort((a, b) => a - b)
|
||||
|
||||
setTimeout(() => {
|
||||
isShaking.value = false
|
||||
liarsDiceMsg.value = `Runde ${liarsDiceRound.value}: Schüttle die Becher! Gib dein Gebot ab (z.B. mindestens zwei Dreier).`
|
||||
}, 400)
|
||||
}
|
||||
|
||||
function canMakeBid(count: number, value: number): boolean {
|
||||
if (!currentBid.value) return count >= 1 && count <= 10
|
||||
if (count > currentBid.value.count) return true
|
||||
if (count === currentBid.value.count && value > currentBid.value.value) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function makePlayerBid() {
|
||||
if (isRoundEvaluating.value || isShaking.value) return
|
||||
if (!canMakeBid(selectedBidCount.value, selectedBidValue.value)) {
|
||||
alert('Dein Gebot muss höher sein als das vorherige (höhere Anzahl oder bei gleicher Anzahl höhere Augenzahl)!')
|
||||
return
|
||||
}
|
||||
|
||||
sound.playDialogueBlip()
|
||||
currentBid.value = { count: selectedBidCount.value, value: selectedBidValue.value }
|
||||
lastBidder.value = 'player'
|
||||
liarsDiceMsg.value = `Du sagst an: Mindestens ${currentBid.value.count}x die ${dieIcon(currentBid.value.value)}!`
|
||||
|
||||
// Next minimum suggestion for controls
|
||||
if (selectedBidValue.value < 6) {
|
||||
selectedBidValue.value++
|
||||
} else {
|
||||
selectedBidCount.value++
|
||||
selectedBidValue.value = 2
|
||||
}
|
||||
|
||||
// Trigger Markus AI after a pause
|
||||
setTimeout(() => {
|
||||
markusTurn()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function markusTurn() {
|
||||
if (isRoundEvaluating.value || !currentBid.value) return
|
||||
|
||||
const bid = currentBid.value
|
||||
// Count how many matching dice Markus has (1s are wild unless bid is on 1)
|
||||
const markusMatches = markusDice.value.filter(d => d === bid.value || (bid.value !== 1 && d === 1)).length
|
||||
const expectedPlayerMatches = bid.value === 1 ? 0.83 : 1.67
|
||||
const totalEstimated = markusMatches + expectedPlayerMatches
|
||||
|
||||
// Call bluff if the bid is too high or improbable
|
||||
const bluffThreshold = bid.count >= 6 ? 0.8 : 1.3
|
||||
if (bid.count > totalEstimated + bluffThreshold) {
|
||||
callBluff('markus')
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, Markus raises!
|
||||
const counts: Record<number, number> = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0 }
|
||||
markusDice.value.forEach(d => counts[d]++)
|
||||
let bestVal = 2
|
||||
let maxC = 0
|
||||
for (let v = 2; v <= 6; v++) {
|
||||
const effective = counts[v] + counts[1]
|
||||
if (effective > maxC) {
|
||||
maxC = effective
|
||||
bestVal = v
|
||||
}
|
||||
}
|
||||
|
||||
let nextCount = bid.count
|
||||
let nextVal = bid.value + 1
|
||||
if (nextVal > 6) {
|
||||
nextCount = bid.count + 1
|
||||
nextVal = Math.min(6, Math.max(2, bestVal))
|
||||
}
|
||||
|
||||
if (nextCount === bid.count && bestVal > bid.value) {
|
||||
nextVal = bestVal
|
||||
}
|
||||
|
||||
sound.playDialogueBlip()
|
||||
currentBid.value = { count: nextCount, value: nextVal }
|
||||
lastBidder.value = 'markus'
|
||||
liarsDiceMsg.value = `Papa Markus erhöht: Mindestens ${nextCount}x die ${dieIcon(nextVal)}! Was sagst du, mein Sohn?`
|
||||
|
||||
// Update player selectors to be just above Markus' bid
|
||||
if (nextVal < 6) {
|
||||
selectedBidCount.value = nextCount
|
||||
selectedBidValue.value = nextVal + 1
|
||||
} else {
|
||||
selectedBidCount.value = nextCount + 1
|
||||
selectedBidValue.value = 2
|
||||
}
|
||||
}
|
||||
|
||||
function callBluff(caller: 'player' | 'markus') {
|
||||
if (isRoundEvaluating.value || !currentBid.value) return
|
||||
isRoundEvaluating.value = true
|
||||
|
||||
sound.playCupSlam()
|
||||
markusDiceRevealed.value = true
|
||||
|
||||
const bid = currentBid.value
|
||||
const allDice = [...playerDice.value, ...markusDice.value]
|
||||
const totalMatches = allDice.filter(d => d === bid.value || (bid.value !== 1 && d === 1)).length
|
||||
|
||||
const bidWasTrue = totalMatches >= bid.count
|
||||
|
||||
setTimeout(() => {
|
||||
sound.playGaoliangShot()
|
||||
if (caller === 'player') {
|
||||
if (bidWasTrue) {
|
||||
// Player called bluff wrongly -> Player loses round!
|
||||
liarsDiceMarkusWins.value++
|
||||
shotAnimation.value = 'sascha'
|
||||
liarsDiceMsg.value = `Aufgedeckt: Es liegen tatsächlich ${totalMatches}x ${dieIcon(bid.value)}! Das Gebot stimmte. Sascha kippt einen Kurzen Gaoliang! 🍶`
|
||||
} else {
|
||||
// Player caught Markus bluffing -> Markus loses round!
|
||||
liarsDicePlayerWins.value++
|
||||
shotAnimation.value = 'markus'
|
||||
liarsDiceMsg.value = `Aufgedeckt: Nur ${totalMatches}x ${dieIcon(bid.value)}! Markus hat geblufft und muss einen Gaoliang kippen! 乾杯!`
|
||||
}
|
||||
} else {
|
||||
// Markus called bluff
|
||||
if (bidWasTrue) {
|
||||
// Markus called bluff wrongly -> Markus loses round!
|
||||
liarsDicePlayerWins.value++
|
||||
shotAnimation.value = 'markus'
|
||||
liarsDiceMsg.value = `Aufgedeckt: Es liegen ${totalMatches}x ${dieIcon(bid.value)}! Saschas Gebot stimmte! Markus kippt einen Gaoliang!`
|
||||
} else {
|
||||
// Markus caught player bluffing -> Player loses round!
|
||||
liarsDiceMarkusWins.value++
|
||||
shotAnimation.value = 'sascha'
|
||||
liarsDiceMsg.value = `Aufgedeckt: Nur ${totalMatches}x ${dieIcon(bid.value)}! Du hast geblufft, Sascha! Trink einen Gaoliang!`
|
||||
}
|
||||
}
|
||||
|
||||
// Check Match End (Best of 3)
|
||||
if (liarsDicePlayerWins.value >= 2) {
|
||||
liarsDiceGameOver.value = true
|
||||
liarsDicePlayerWon.value = true
|
||||
sound.playFanfare()
|
||||
store.addKronkorken(10)
|
||||
store.addItem('58% Kinmen Gaoliang')
|
||||
store.unlockEasterEgg('taiwan_dice_champion')
|
||||
} else if (liarsDiceMarkusWins.value >= 2) {
|
||||
liarsDiceGameOver.value = true
|
||||
liarsDicePlayerWon.value = false
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
|
||||
function proceedNextDiceRound() {
|
||||
liarsDiceRound.value++
|
||||
startNewDiceRound()
|
||||
}
|
||||
|
||||
function inspectMirror() {
|
||||
sound.playFanfare()
|
||||
zulumanMirrorActive.value = true
|
||||
@@ -356,7 +589,7 @@ function proceedToExpress() {
|
||||
|
||||
<!-- ================= 1. GASTSTUBE (SCHANKRAUM) ================= -->
|
||||
<template v-if="currentRoom === 'schankraum'">
|
||||
<img src="/assets/scenes/schankraum_v2.jpg?v=3" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
|
||||
<img src="/assets/scenes/schankraum.jpg" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
|
||||
|
||||
<!-- Verena on the wooden gallery -->
|
||||
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
|
||||
@@ -463,7 +696,7 @@ function proceedToExpress() {
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- ================= 3. HINTERLAND ================= -->
|
||||
<!-- ================= 4. HINTERLAND ================= -->
|
||||
<template v-else-if="currentRoom === 'hinterland'">
|
||||
<img src="/assets/scenes/hinterland.jpg" alt="Hinterland Haumdaucher Alm" class="tavern-bg" />
|
||||
|
||||
@@ -474,10 +707,22 @@ function proceedToExpress() {
|
||||
<span class="hotspot-badge">Mane (Wanderin) 🥾</span>
|
||||
</button>
|
||||
|
||||
<!-- Hansi enjoying the view on the bench -->
|
||||
<!-- Hansi walking towards Münchanien -->
|
||||
<button class="hotspot hotspot-hansi" @click="openCharacterDialog('hansi')">
|
||||
<img :src="CHARACTERS.hansi.portrait" class="hotspot-thumb" alt="Hansi" />
|
||||
<span class="hotspot-badge">Hansi 🧔</span>
|
||||
<span class="hotspot-badge">Hansi (Richtung Münchanien) 🧔</span>
|
||||
</button>
|
||||
|
||||
<!-- Signpost to Münchanien -->
|
||||
<button class="hotspot hotspot-sign-munchanien" @click="openMunchanienHint">
|
||||
<span class="sign-icon">🪧</span>
|
||||
<span class="hotspot-badge">Wegweiser: Münchanien 🏰</span>
|
||||
</button>
|
||||
|
||||
<!-- Trail to Hinter den Bergen (Taiwan & Papa Markus) -->
|
||||
<button class="hotspot door-hotspot hotspot-door-hinter-den-bergen" @click="switchRoom('hinter_den_bergen')">
|
||||
<span class="door-icon">🏔️</span>
|
||||
<span class="hotspot-badge glow-door">Hinter den Bergen ▶</span>
|
||||
</button>
|
||||
|
||||
<!-- Trail back to Marktplatz -->
|
||||
@@ -487,7 +732,24 @@ function proceedToExpress() {
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- ================= 4. HINTERZIMMER (SECRET VIP RAVE) ================= -->
|
||||
<!-- ================= 5. HINTER DEN BERGEN (TAIWAN & PAPA MARKUS) ================= -->
|
||||
<template v-else-if="currentRoom === 'hinter_den_bergen'">
|
||||
<img src="/assets/scenes/hinter_den_bergen.jpg" alt="Hinter den Bergen mit Papa Markus" class="tavern-bg" />
|
||||
|
||||
<!-- Markus and Taiwanese companions table -->
|
||||
<button class="hotspot hotspot-markus" @click="openCharacterDialog('markus')">
|
||||
<img :src="CHARACTERS.markus.portrait" class="hotspot-thumb" alt="Papa Markus" />
|
||||
<span class="hotspot-badge">Papa Markus 🎲🍶</span>
|
||||
</button>
|
||||
|
||||
<!-- Trail back to Hinterland -->
|
||||
<button class="hotspot door-hotspot hotspot-door-back-hinterland" @click="switchRoom('hinterland')">
|
||||
<span class="door-icon">🌲</span>
|
||||
<span class="hotspot-badge glow-door">◀ Zum Hinterland</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<!-- ================= 6. HINTERZIMMER (SECRET VIP RAVE) ================= -->
|
||||
<template v-else-if="currentRoom === 'hinterzimmer'">
|
||||
<img src="/assets/scenes/hinterzimmer.jpg" alt="Geheimes VIP Hinterzimmer" class="tavern-bg" />
|
||||
<div class="strobe-overlay"></div>
|
||||
@@ -527,6 +789,7 @@ function proceedToExpress() {
|
||||
currentRoom === 'nebenraum' ? '🎨 AKT III: KÖNIGLICHER NEBENRAUM & ATELIER 🎨' :
|
||||
currentRoom === 'marktplatz' ? '🎪 AKT III: HAUMDAUCHER MARKTPLATZ 🎪' :
|
||||
currentRoom === 'hinterland' ? '🌲 AKT III: ALPINES HINTERLAND 🌲' :
|
||||
currentRoom === 'hinter_den_bergen' ? '🏮 AKT III: HINTER DEN BERGEN (TAIWAN) 🏮' :
|
||||
'🪩 AKT III: DER GEHEIME PARTY BUNKER 🪩'
|
||||
}}
|
||||
</div>
|
||||
@@ -561,6 +824,13 @@ function proceedToExpress() {
|
||||
>
|
||||
🌲 Hinterland
|
||||
</button>
|
||||
<button
|
||||
class="room-tab"
|
||||
:class="{ active: currentRoom === 'hinter_den_bergen' }"
|
||||
@click="switchRoom('hinter_den_bergen')"
|
||||
>
|
||||
🏮 Hinter den Bergen
|
||||
</button>
|
||||
<button
|
||||
v-if="store.unlockedEasterEggs.includes('hinterzimmer') || currentRoom === 'hinterzimmer'"
|
||||
class="room-tab vip-tab"
|
||||
@@ -775,6 +1045,178 @@ function proceedToExpress() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Münchanien Hint Modal -->
|
||||
<div v-if="munchanienHintActive" class="dialog-overlay" @click="munchanienHintActive = false">
|
||||
<div class="dialog-box intro-modal-box" @click.stop>
|
||||
<div class="intro-modal-header">
|
||||
<span class="intro-tag">WEGWEISER NACH MÜNCHANIE</span>
|
||||
<button class="duel-close-btn" @click="munchanienHintActive = false" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
<h2 class="intro-modal-title">🏰 WEGWEISER: NACH MÜNCHANIE</h2>
|
||||
<p class="intro-modal-story">
|
||||
Auf dem geschnitzten Wegweiser steht mit Kreide geschrieben:<br><br>
|
||||
<em>„Hansi ist schon vorausgegangen, um die mächtige Sound-Anlage für das Abschiedskonzert in der Residenzstadt Münchanie aufzubauen! Bereite dich gut vor, sammle Kronkorken und Helles in allen Räumen und besuche alle Freunde, bevor du nach Akt IV aufbrichst!“</em>
|
||||
</p>
|
||||
<button class="action-btn" @click="munchanienHintActive = false">
|
||||
Verstanden! Weiter erkunden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liar's Dice Intro Modal -->
|
||||
<div v-if="liarsDiceIntroOpen" class="dialog-overlay" @click="liarsDiceIntroOpen = 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="liarsDiceIntroOpen = false" aria-label="Schließen">✕</button>
|
||||
</div>
|
||||
<h2 class="intro-modal-title">🎲 LIAR'S DICE (CHUI NIU / 吹牛)</h2>
|
||||
<p class="intro-modal-story">
|
||||
Papa Markus fordert dich zu einer Partie Liar's Dice heraus, wie man es in den Gassen von Taipeh und Kinmen spielt! Wer blufft besser und wer muss den 58% Gaoliang-Schnaps kippen?
|
||||
</p>
|
||||
<div class="intro-modal-rules">
|
||||
<div class="rule-line">🎲 <strong>5 Würfel pro Spieler:</strong> Jeder sieht nur seine eigenen Würfel!</div>
|
||||
<div class="rule-line">✨ <strong>1er sind Joker:</strong> Zählen für alle Augenzahlen (außer wenn auf 1er geboten wird).</div>
|
||||
<div class="rule-line">🗣️ <strong>Gebote steigern:</strong> Schätze die Gesamtzahl aller Würfel (deine + Markus). Nächstes Gebot muss höhere Anzahl oder bei gleicher Anzahl höhere Augenzahl sein.</div>
|
||||
<div class="rule-line">🍶 <strong>Bluff anzweifeln:</strong> Glaubst du Markus lügt? Sag „Bluff!“ – wer unrecht hat, kippt einen Schnaps!</div>
|
||||
<div class="rule-line">🏆 <strong>Modus:</strong> Best of 3! Belohnung: +10 Kronkorken & 58% Kinmen Gaoliang!</div>
|
||||
</div>
|
||||
<button class="action-btn start-duel-action-btn" @click="startLiarsDiceMatch">
|
||||
🎲 Würfelbecher schütteln!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Liar's Dice Game Modal -->
|
||||
<div v-if="liarsDiceActive" class="dialog-overlay">
|
||||
<div class="dialog-box liars-dice-box">
|
||||
<div class="liars-dice-header">
|
||||
<div class="dice-title-group">
|
||||
<h3>🎲 LIAR'S DICE GEGEN PAPA MARKUS</h3>
|
||||
<span class="dice-score">Stand: Sascha {{ liarsDicePlayerWins }} : {{ liarsDiceMarkusWins }} Markus (Best of 3)</span>
|
||||
</div>
|
||||
<button class="duel-close-btn" @click="liarsDiceActive = false" aria-label="Spiel verlassen">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Status Message -->
|
||||
<div class="dice-status-msg">
|
||||
{{ liarsDiceMsg }}
|
||||
</div>
|
||||
|
||||
<!-- Cups and Dice Arena -->
|
||||
<div class="dice-arena">
|
||||
<!-- Markus Side -->
|
||||
<div class="dice-player-row markus-row">
|
||||
<div class="dice-player-label">
|
||||
<img :src="CHARACTERS.markus.portrait" class="dice-avatar" alt="Papa Markus" />
|
||||
<span>Papa Markus</span>
|
||||
</div>
|
||||
<div class="dice-cup-area">
|
||||
<div v-if="!markusDiceRevealed" class="cup-covered">
|
||||
<span class="cup-icon">🥤</span>
|
||||
<span class="cup-text">[ 5 verdeckte Würfel ]</span>
|
||||
</div>
|
||||
<div v-else class="dice-revealed-row">
|
||||
<span v-for="(die, idx) in markusDice" :key="idx" class="dice-die markus-die">
|
||||
{{ dieIcon(die) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Bid Banner -->
|
||||
<div class="current-bid-banner" v-if="currentBid">
|
||||
<span class="bid-label">Aktuelles Gebot:</span>
|
||||
<span class="bid-value">Mindestens {{ currentBid.count }}x {{ dieIcon(currentBid.value) }}</span>
|
||||
<span class="bidder-tag">({{ lastBidder === 'player' ? 'von Sascha' : 'von Papa Markus' }})</span>
|
||||
</div>
|
||||
|
||||
<!-- Player Side -->
|
||||
<div class="dice-player-row player-row">
|
||||
<div class="dice-player-label">
|
||||
<span class="player-icon">👑</span>
|
||||
<span>König Sascha (Du)</span>
|
||||
</div>
|
||||
<div class="dice-revealed-row">
|
||||
<span v-for="(die, idx) in playerDice" :key="idx" class="dice-die player-die">
|
||||
{{ dieIcon(die) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shot Drink Penalty Animation Banner -->
|
||||
<div v-if="shotAnimation" class="shot-animation-banner">
|
||||
🍶 {{ shotAnimation === 'sascha' ? 'Sascha trinkt einen 58% Gaoliang Shot! 🥴' : 'Markus kippt den Gaoliang Shot! 乾杯! 🎉' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls / Actions -->
|
||||
<div v-if="!liarsDiceGameOver && !isRoundEvaluating" class="dice-controls">
|
||||
<div class="bid-selectors">
|
||||
<div class="selector-col">
|
||||
<label>Anzahl:</label>
|
||||
<div class="counter-btns">
|
||||
<button class="mini-btn" @click="selectedBidCount = Math.max(1, selectedBidCount - 1)">-</button>
|
||||
<span class="counter-val">{{ selectedBidCount }}</span>
|
||||
<button class="mini-btn" @click="selectedBidCount = Math.min(10, selectedBidCount + 1)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selector-col">
|
||||
<label>Augenzahl:</label>
|
||||
<div class="die-val-btns">
|
||||
<button
|
||||
v-for="val in [2, 3, 4, 5, 6, 1]"
|
||||
:key="val"
|
||||
class="die-select-btn"
|
||||
:class="{ active: selectedBidValue === val }"
|
||||
@click="selectedBidValue = val"
|
||||
>
|
||||
{{ dieIcon(val) }} ({{ val }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dice-action-buttons">
|
||||
<button class="action-btn bid-action-btn" @click="makePlayerBid">
|
||||
🗣️ Gebot abgeben ({{ selectedBidCount }}x {{ dieIcon(selectedBidValue) }})
|
||||
</button>
|
||||
<button
|
||||
v-if="currentBid && lastBidder === 'markus'"
|
||||
class="action-btn call-bluff-btn"
|
||||
@click="callBluff('player')"
|
||||
>
|
||||
🤥 Lüge! (Bluff aufdecken)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Next Round or Game Over -->
|
||||
<div v-else-if="isRoundEvaluating && !liarsDiceGameOver" class="dice-round-eval">
|
||||
<button class="action-btn next-round-btn" @click="proceedNextDiceRound">
|
||||
🎲 Nächste Runde starten ▶
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="liarsDiceGameOver" class="dice-game-over">
|
||||
<template v-if="liarsDicePlayerWon">
|
||||
<h2 class="victory-title">👑 SASCHA GEWINNT DAS LIAR'S DICE MATCH!</h2>
|
||||
<p class="dice-over-desc">Papa Markus klopft dir auf die Schulter: „Respekt, Sascha! Du hast das Zeug zum König von Taiwan!“</p>
|
||||
<p class="reward-text">+10 Kronkorken, 58% Kinmen Gaoliang Flasche & Titel freigeschaltet!</p>
|
||||
<button class="action-btn" @click="liarsDiceActive = false">乾杯 Papa! Zurücklehnen</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h2 class="defeat-title">🍶 PAPA MARKUS TRIUMPHIERT!</h2>
|
||||
<p class="dice-over-desc">Papa Markus lacht schallend: „Gegen jahrzehntelange Gaoliang-Erfahrung kommst du noch nicht an, Sascha!“</p>
|
||||
<div class="boar-retry-actions">
|
||||
<button class="action-btn retry-btn" @click="startLiarsDiceMatch">Revanche spielen!</button>
|
||||
<button class="close-btn" @click="liarsDiceActive = false">Vielleicht später</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -901,11 +1343,11 @@ function proceedToExpress() {
|
||||
}
|
||||
|
||||
/* 1. Schankraum Hotspots */
|
||||
.hotspot-verena { top: 17%; left: 28%; }
|
||||
.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%; }
|
||||
.hotspot-verena { top: 21%; left: 28%; }
|
||||
.hotspot-door-nebenraum { top: 45%; left: 34%; }
|
||||
.hotspot-armend { top: 58%; left: 74%; }
|
||||
.hotspot-lutz { top: 56%; left: 52%; }
|
||||
.hotspot-door-marktplatz { top: 48%; left: 86%; }
|
||||
|
||||
/* 2. Nebenraum Hotspots */
|
||||
.hotspot-mirror-nebenraum { top: 36%; left: 24%; }
|
||||
@@ -974,8 +1416,8 @@ function proceedToExpress() {
|
||||
.hotspot-door-back-gaststube { top: 74%; left: 20%; }
|
||||
.hotspot-door-hinterland { top: 34%; left: 76%; }
|
||||
|
||||
/* 3. Hinterland Hotspots */
|
||||
.hotspot-mane { top: 54%; left: 36%; }
|
||||
/* 4. Hinterland Hotspots */
|
||||
.hotspot-mane { top: 52%; left: 82%; }
|
||||
.hiking-anim {
|
||||
animation: hikeBob 1.2s infinite ease-in-out;
|
||||
}
|
||||
@@ -990,10 +1432,16 @@ function proceedToExpress() {
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
.hotspot-hansi { top: 56%; left: 74%; }
|
||||
.hotspot-door-back-marktplatz { top: 72%; left: 22%; }
|
||||
.hotspot-hansi { top: 44%; left: 63%; }
|
||||
.hotspot-sign-munchanien { top: 53%; left: 24%; }
|
||||
.hotspot-door-hinter-den-bergen { top: 42%; left: 24%; }
|
||||
.hotspot-door-back-marktplatz { top: 74%; left: 16%; }
|
||||
|
||||
/* 4. Hinterzimmer Hotspots & Strobe */
|
||||
/* 5. Hinter den Bergen Hotspots */
|
||||
.hotspot-markus { top: 56%; left: 50%; }
|
||||
.hotspot-door-back-hinterland { top: 74%; left: 20%; }
|
||||
|
||||
/* 6. Hinterzimmer Hotspots & Strobe */
|
||||
.strobe-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -1596,4 +2044,291 @@ function proceedToExpress() {
|
||||
.start-duel-action-btn:hover {
|
||||
background: #16a34a !important;
|
||||
}
|
||||
|
||||
/* Liar's Dice (Chui Niu) Modal & UI */
|
||||
.liars-dice-box {
|
||||
max-width: 360px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.liars-dice-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid #78350f;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.dice-title-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dice-title-group h3 {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7.5px;
|
||||
color: #ffd700;
|
||||
margin: 0 0 4px 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.dice-score {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6.5px;
|
||||
color: #fef08a;
|
||||
}
|
||||
|
||||
.dice-status-msg {
|
||||
font-family: 'MedievalSharp', serif;
|
||||
font-size: 13.5px;
|
||||
color: #fef3c7;
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dice-arena {
|
||||
background: #1a0f0a;
|
||||
border: 1.5px solid #d97706;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dice-player-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.dice-player-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6.5px;
|
||||
color: #fef08a;
|
||||
}
|
||||
|
||||
.dice-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ffd700;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.player-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dice-cup-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cup-covered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #94a3b8;
|
||||
font-family: 'MedievalSharp', serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cup-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.dice-revealed-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dice-die {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.markus-die {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.player-die {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.current-bid-banner {
|
||||
background: #451a03;
|
||||
border: 1px dashed #ffd700;
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: #fef08a;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bid-label {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6px;
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.bid-value {
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.bidder-tag {
|
||||
font-size: 9.5px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.shot-animation-banner {
|
||||
background: #7f1d1d;
|
||||
border: 1.5px solid #ef4444;
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
font-family: 'MedievalSharp', serif;
|
||||
font-size: 13px;
|
||||
color: #fef08a;
|
||||
animation: pulseAnim 0.8s infinite alternate;
|
||||
}
|
||||
|
||||
.dice-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bid-selectors {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selector-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.selector-col label {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6px;
|
||||
color: #fed7aa;
|
||||
}
|
||||
|
||||
.counter-btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #1a0f0a;
|
||||
border: 1px solid #78350f;
|
||||
border-radius: 4px;
|
||||
justify-content: space-between;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.mini-btn {
|
||||
background: #b45309;
|
||||
border: 1px solid #f59e0b;
|
||||
color: #fff;
|
||||
border-radius: 3px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.counter-val {
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 10px;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.die-val-btns {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.die-select-btn {
|
||||
background: #231209;
|
||||
border: 1px solid #78350f;
|
||||
border-radius: 3px;
|
||||
color: #fef3c7;
|
||||
font-size: 10px;
|
||||
padding: 4px 3px;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.die-select-btn.active {
|
||||
background: #d97706;
|
||||
border-color: #ffd700;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dice-action-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bid-action-btn {
|
||||
flex: 1.5;
|
||||
background: #15803d !important;
|
||||
border-color: #86efac !important;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6.5px;
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
.call-bluff-btn {
|
||||
flex: 1;
|
||||
background: #dc2626 !important;
|
||||
border-color: #fca5a5 !important;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 6.5px;
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
.dice-round-eval {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.next-round-btn {
|
||||
width: 100%;
|
||||
background: #2563eb !important;
|
||||
border-color: #93c5fd !important;
|
||||
font-family: 'Press Start 2P', monospace;
|
||||
font-size: 7.5px;
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.dice-game-over {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dice-over-desc {
|
||||
font-family: 'MedievalSharp', serif;
|
||||
font-size: 13.5px;
|
||||
margin: 6px 0;
|
||||
color: #fef3c7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -76,8 +76,8 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
id: 'amy',
|
||||
name: 'Amy (Amelie)',
|
||||
title: 'Die Hof-Künstlerin',
|
||||
portrait: '/assets/characters/amy/portrait_v2.jpg',
|
||||
description: 'Kreatives Genie in Latzhose mit Dutt und feinen Kräuter-Zigarren.',
|
||||
portrait: '/assets/characters/amy/portrait.jpg',
|
||||
description: 'Kreatives Genie in Latzhose mit Brille, Dutt und feinen Kräuter-Zigarren.',
|
||||
famousQuote: 'Rave the Kingdom! Die Farben brauchen mehr Bass!',
|
||||
themeColor: '#a855f7'
|
||||
},
|
||||
@@ -153,6 +153,15 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
||||
famousQuote: 'Bester Bruder! Alle drei Schwestern gerettet! Jetzt aber zügig zum Schankraum!',
|
||||
themeColor: '#06b6d4'
|
||||
},
|
||||
markus: {
|
||||
id: 'markus',
|
||||
name: 'Markus (Saschas Vater)',
|
||||
title: 'Herrscher Hinter den Bergen & Gaoliang-Meister',
|
||||
portrait: '/assets/characters/markus/portrait.jpg',
|
||||
description: 'Saschas gutmütiger, lebensfroher Vater. Sitzt mit seinen taiwanischen Freunden an einem rustikalen Tisch und spielt Liar\'s Dice.',
|
||||
famousQuote: 'Servus mein Sohn! Trink erst mal an Schluck Gaoliang mit uns, bevor du weiterziehst!',
|
||||
themeColor: '#f59e0b'
|
||||
},
|
||||
djs: {
|
||||
id: 'djs',
|
||||
name: 'Die Hof-DJs',
|
||||
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 77 KiB |