Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a0b65c3fa | ||
|
|
6374ba9e8a |
@@ -27,18 +27,46 @@ This document records the foundational design decisions, their rationale ("Why")
|
|||||||
|
|
||||||
### 2. Dual Game Modes & Dynamic Chassis Layout (`GameBoyFrame.vue`)
|
### 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.
|
* **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 (Jump & Run, Party Express) 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.
|
* **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.
|
||||||
* **The Rule**:
|
* **The Rule**:
|
||||||
* **Arcade Acts** (`currentAct === 'platformer'` or `'party_express'`):
|
* **Arcade Acts** (`currentAct === 'platformer'` or `'party_express'`):
|
||||||
* Show the GameBoy D-Pad and A/B action buttons (`arcade-layout`).
|
* Show the GameBoy D-Pad and A/B action buttons (`arcade-layout`).
|
||||||
* Display the arcade HUD with Korken, Helles, and 135 BPM counter.
|
* Display the arcade HUD with Korken, Helles, and 135 BPM counter.
|
||||||
* **Adventure / Dialogue Acts** (`currentAct === 'intro'`, `'prologue'`, `'tavern'`, or `'finale'`):
|
* **Adventure / Dialogue Acts** (`currentAct === 'intro'`, `'prologue'`, `'tavern'`, or `'finale'`):
|
||||||
* Hide the D-Pad and A/B buttons completely (`adventure-layout`).
|
* Hide the D-Pad and A/B buttons completely (`adventure-layout`).
|
||||||
* Display a sleek mini bottom bar with current Korken, Helles, and an inventory drawer button (*„🎒 Beutel“*).
|
* 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.
|
* On `currentAct === 'finale'`, hide even the mini bar so the certificate occupies the full screen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 2b. Global Settings & Audio Control Modal (`GameBoyFrame.vue`)
|
||||||
|
* **The Decision**: A persistent `⚙️` settings icon is available at the top-right of the GameBoy frame.
|
||||||
|
* **Why**: Mobile players need a straightforward way to mute sound in public, pause during conversations, or cleanly restart the adventure.
|
||||||
|
* **The Rule**: Clicking `⚙️` opens a modal that pauses game loops, provides toggleable audio (`🔊 Sound: AN / 🔇 Sound: AUS`), and offers a `🔄 Spielstand zurücksetzen` option with safety confirmation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2c. Prologue Branching & Themed Game Overs (`PrologueView.vue`)
|
||||||
|
* **The Decision**: Prologue choices can trigger humorous non-fatal Game Over screens with themed reset actions:
|
||||||
|
* 🔥 **Flo Maier (*Feierdeifl*)**: Choosing to burn the castle leads to a fiery castle inferno and a `🚒 MIT DEM LÖSCHZUG NEU STARTEN` button.
|
||||||
|
* 🧛 **Viktor (*Partyvampir*)**: Choosing the vampire bite turns Sascha into dust at sunrise and provides a `🧄 MIT KNOBLAUCH-KUR NEU STARTEN` button.
|
||||||
|
* **Why**: Gives playful branching flavor without permanently penalizing the player.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2d. Akt II: Doodle Jump Schwesternrettung (`PlatformerView.vue`)
|
||||||
|
* **The Decision**: Act II is a bottom-to-top vertical bouncing climber across 3 progressive levels to rescue King Sascha's three sisters:
|
||||||
|
1. **Level 1**: Rescue **Saskia** (+3 Kronkorken, 1 Helles)
|
||||||
|
2. **Level 2**: Rescue **Maja** (+3 Kronkorken, 1 Helles)
|
||||||
|
3. **Level 3**: Rescue **Miriam** (+4 Kronkorken, 1 Helles)
|
||||||
|
* **Mechanics**:
|
||||||
|
* Continuous automatic upward bouncing upon landing on platforms.
|
||||||
|
* Spring platforms (`🌀`) for high-velocity boost with custom synthesizer audio.
|
||||||
|
* Seamless screen wrap-around (moving off the left edge wraps to the right and vice versa).
|
||||||
|
* Gentle fall protection: falling below the visible screen softly places Sascha onto a safe platform near his lower altitude rather than triggering a punitive death.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 3. Protagonist System & Extensibility (`characters.ts`)
|
### 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.
|
* **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.
|
* **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.
|
||||||
@@ -67,9 +95,9 @@ This document records the foundational design decisions, their rationale ("Why")
|
|||||||
|
|
||||||
### 4. Background Art Integrity & Cleanliness
|
### 4. Background Art Integrity & Cleanliness
|
||||||
* **The Decision**: Background scenes must remain clean of any AI-hallucinated pseudo-English text or stray UI elements.
|
* **The Decision**: Background scenes must remain clean of any AI-hallucinated pseudo-English text or stray UI elements.
|
||||||
* **Why**: AI image generators often bake random adventure-game dialogue boxes or text (e.g. `[O] I have to tell you, the glow-wurst is excellent!`) into the scene.
|
* **Why**: AI image generators often bake random adventure-game dialogue boxes or text into the scene.
|
||||||
* **The Rule**:
|
* **The Rule**:
|
||||||
* `public/assets/scenes/tavern.jpg`: The bottom area is cleaned with a seamless dark oak wood beam and floor texture.
|
* `public/assets/scenes/schankraum_v3.jpg`: Seamless warm dark oak wood floorboards with natural planks.
|
||||||
* Any scene footer navigation (like `.scene-bottom-nav`) must be styled as natural wooden wainscoting with clear German labels.
|
* Any scene footer navigation (like `.scene-bottom-nav`) must be styled as natural wooden wainscoting with clear German labels.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -78,8 +106,8 @@ This document records the foundational design decisions, their rationale ("Why")
|
|||||||
* **The Decision**: Critical game HUD elements (score, act title, instructions) must either be drawn as HTML/CSS overlays or placed within a minimum safe margin of 24px on canvas.
|
* **The Decision**: Critical game HUD elements (score, act title, instructions) must either be drawn as HTML/CSS overlays or placed within a minimum safe margin of 24px on canvas.
|
||||||
* **Why**: Smartphones vary widely in screen aspect ratio (16:9, 19.5:9, 20:9). When a 360x480 canvas is rendered with `object-fit: cover`, the left and right (or top and bottom) edges can be cropped by 10–25px on narrow screens.
|
* **Why**: Smartphones vary widely in screen aspect ratio (16:9, 19.5:9, 20:9). When a 360x480 canvas is rendered with `object-fit: cover`, the left and right (or top and bottom) edges can be cropped by 10–25px on narrow screens.
|
||||||
* **The Rule**:
|
* **The Rule**:
|
||||||
* In `PlatformerView.vue`: The Korken counter and level title are rendered via an absolute HTML overlay (`.platformer-hud-top`) so they are 100% immune to canvas cropping.
|
* In `PlatformerView.vue`: The Korken counter, sister progress, and level title are rendered via an absolute HTML overlay (`.platformer-hud-top`) so they are 100% immune to canvas cropping.
|
||||||
* In `PartyExpressView.vue`: The banner title is centered (`ctx.textAlign = 'center'`), and the progress bar has symmetrical 24px padding.
|
* In `PartyExpressView.vue`: The 3-lane strings, score counter, combo meter, and crowd meter use responsive percentages.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -89,9 +117,9 @@ This document records the foundational design decisions, their rationale ("Why")
|
|||||||
* **Score Breakdown**:
|
* **Score Breakdown**:
|
||||||
* Kronkorken collected: `100 pts` each.
|
* Kronkorken collected: `100 pts` each.
|
||||||
* Helles tapped: `250 pts` each.
|
* Helles tapped: `250 pts` each.
|
||||||
* Guests served in Party Express: `300 pts` each.
|
* Guests served / Concert performance: up to `3,000 pts`.
|
||||||
* Gstanzl duel victory: `1,500 pts`.
|
* Gstanzl duel victory: `1,500 pts`.
|
||||||
* Discovered secrets (Bridgerton dialogue, Zuluman mirror): `500 pts` each.
|
* Discovered secrets (Bridgerton dialogue, Party Bunker, Zuluman mirror): `500 pts` each.
|
||||||
* **Ranks**:
|
* **Ranks**:
|
||||||
* `S-Rang (Legendär)`: >= 11,000 points (*„Unsterblicher Großfürst des Basses“*).
|
* `S-Rang (Legendär)`: >= 11,000 points (*„Unsterblicher Großfürst des Basses“*).
|
||||||
* `A-Rang (Erhaben)`: >= 9,500 points (*„Meister-Brauer der königlichen Garde“*).
|
* `A-Rang (Erhaben)`: >= 9,500 points (*„Meister-Brauer der königlichen Garde“*).
|
||||||
@@ -101,10 +129,10 @@ This document records the foundational design decisions, their rationale ("Why")
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 7. Zero-Dependency Web Audio Synthesizer (`sound.ts`)
|
### 7. Zero-Dependency Web Audio Synthesizer (`synthAudio.ts`)
|
||||||
* **The Decision**: All sound effects, fanfares, and the 135 BPM techno bassline are synthesized in pure code using the Web Audio API.
|
* **The Decision**: All sound effects, fanfares, lute chords, and the 135 BPM techno bassline are synthesized in pure code using the Web Audio API.
|
||||||
* **Why**: External audio files (.mp3/.wav) take megabytes of bandwidth, stutter on mobile cellular networks, and trigger browser autoplay blocks.
|
* **Why**: External audio files (.mp3/.wav) take megabytes of bandwidth, stutter on mobile cellular networks, and trigger browser autoplay blocks.
|
||||||
* **The Rule**: The `SoundService` initializes on the first user interaction (touch/click). Sound effects use oscillators and gain envelopes; 135 BPM kick and sub-bass use recurring periodic intervals.
|
* **The Rule**: The audio context initializes on the first user interaction (touch/click). Sound effects use oscillators and gain envelopes; 135 BPM kick and sub-bass use recurring periodic intervals.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -115,16 +143,42 @@ This document records the foundational design decisions, their rationale ("Why")
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 9. Easter Egg Invisibility Pattern (`TavernView.vue`)
|
### 9. Party Bunker Trapdoor & Double-Tap Pattern (`TavernView.vue`)
|
||||||
* **The Decision**: Secret hotspots (e.g. `secret-tap-hotspot` in the Schankraum) must be 100% invisible initially.
|
* **The Decision**: Secret trapdoor (`.trapdoor-hotspot` in Schankraum leading to the VIP Party Bunker) must be 100% invisible initially and requires a deliberate double-click or double-tap to unlock.
|
||||||
* **Why**: An Easter egg marked with a glowing button or text badge is not a secret; it invites immediate trivial clicking.
|
* **Why**: A single accidental click on the floor should not whisk the player away into an Easter Egg bunker before they explore the tavern.
|
||||||
* **The Rule**:
|
* **The Rule**:
|
||||||
* The button must render with transparent background, no border, no outline, no box shadow, no hover change, and no tooltip (`:title="undefined"`).
|
* Rendered with transparent background, no border, no outline, no box shadow, no hover change, and no tooltip (`:title="undefined"`).
|
||||||
* Only upon the user's first click does it reveal the gold key icon, play a celebratory fanfare, unlock the Easter egg in the game store (+500 points), and transition to the secret room.
|
* Double-click/double-tap detection combines `@dblclick` and timestamp delta (`now - lastTrapTap < 450ms`) to work flawlessly across desktop and mobile touchscreens.
|
||||||
|
* Upon double-tap, it reveals the wood trapdoor sprite, labels itself *„Luke zum Party Bunker“*, plays a celebratory fanfare, unlocks the Easter egg in the game store (+500 points), and transitions to the Party Bunker.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10. LocalStorage Persistence & Clean Game Reset (`gameStore.ts`)
|
### 10. Multi-Room World Architecture & Tavern Minigames (`TavernView.vue`)
|
||||||
|
* **The Decision**: Act III is structured as a spatial, interconnected five-room village hub rather than a cramped single screen:
|
||||||
|
1. **Gaststube (Schankraum)**: King Sascha's table, Queen Verena on the balcony, Armend at the forge, Lutz (ZTUL) at the fireplace, door to Nebenraum, door to Marktplatz, and the secret *„Luke zum Party Bunker“* in the floor.
|
||||||
|
2. **Königlicher Nebenraum (Kemenate & Atelier)**: Amy painting the royal *„Rave the Kingdom!“* banner at her easel in overalls, and the golden Zuluman magic mirror on the wall.
|
||||||
|
3. **Haumdaucher Marktplatz**: Mario acrobatically juggling chainsaws and hops on the cobblestones; NAT (Josef) on the wooden festival stage with wild boar banners telling stories of his girls and heroic deeds.
|
||||||
|
4. **Alpines Hinterland**: Mane (Ramona) hiking the alpine trail with walking stick; Hansi on his departure journey towards Münchanien.
|
||||||
|
5. **Geheimes VIP-Party-Bunker**: Secret rave with disco chandelier, stroboscope, Pü's crystal staff, DJ Flo Maier, and Viktor the night-guard.
|
||||||
|
* **Minigames & Exit Safety**:
|
||||||
|
* **Bayerisches Gstanzl-Fechten**: Multi-round insult rhyming duel. Preceded by an explanatory rules modal. Must always include a visible `[✕]` close button so players are never trapped.
|
||||||
|
* **Wildschwein-Schnick-Schnack-Schnuck**: Best-of-3 tactical duel against NAT. Preceded by an explanatory rules modal (🐗 Keiler bricht 🌿 Dickicht, 🌿 Dickicht fängt 🏹 Lanze, 🏹 Lanze erlegt 🐗 Keiler). Includes `[✕]` close button and active button blur to prevent mobile sticky hover states.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10b. Akt IV: Mittelalter Guitar Hero — Hansis Abschiedskonzert (`PartyExpressView.vue`)
|
||||||
|
* **The Decision**: Act IV is a 3-lane medieval lute rhythm game set on an epic castle festival stage in front of 12,000 cheering fans.
|
||||||
|
* **Why**: Perfectly bridges the story from Hansi's departure in the Hinterland to a grand musical send-off before the royal coronation.
|
||||||
|
* **The Rule**:
|
||||||
|
* 3 color-coded lanes (`🔴 Saite 1`, `🟡 Saite 2`, `🔵 Saite 3`).
|
||||||
|
* Notes spawn synchronized to 135 BPM audio clicks and melodic riffs.
|
||||||
|
* Three large, finger-friendly touch buttons positioned comfortably at the lower screen for thumb play.
|
||||||
|
* Combo multiplier increases up to 4x with unbroken streaks.
|
||||||
|
* Generous hit window for fun, non-punishing festival gameplay.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. LocalStorage Persistence & Clean Game Reset (`gameStore.ts`)
|
||||||
* **The Decision**: Game state automatically synchronizes to `localStorage` (`sasch40_game_save_v1`) on every mutation and is cleanly wiped when clicking *„🔄 NOCHMAL SPIELEN“*.
|
* **The Decision**: Game state automatically synchronizes to `localStorage` (`sasch40_game_save_v1`) on every mutation and is cleanly wiped when clicking *„🔄 NOCHMAL SPIELEN“*.
|
||||||
* **Why**: Mobile users frequently refresh or switch apps to snap photos. Without persistence, a browser reload resets their progress back to Act I.
|
* **Why**: Mobile users frequently refresh or switch apps to snap photos. Without persistence, a browser reload resets their progress back to Act I.
|
||||||
* **The Rule**:
|
* **The Rule**:
|
||||||
@@ -151,7 +205,7 @@ make all
|
|||||||
|
|
||||||
The `Makefile` targets:
|
The `Makefile` targets:
|
||||||
* `compile`: Runs `npm run build` (`vue-tsc -b && vite build`).
|
* `compile`: Runs `npm run build` (`vue-tsc -b && vite build`).
|
||||||
* `build`: Runs `docker build --platform linux/amd64 -t sasch40:latest .` (copies `dist/` directly into `nginx:alpine`).
|
* `build`: Runs `docker build --no-cache --platform linux/amd64 -t sasch40:latest .` (copies `dist/` directly into `nginx:alpine`).
|
||||||
* `tag`: Tags as `registry.haumdaucher.de/sasch40:latest`.
|
* `tag`: Tags as `registry.haumdaucher.de/sasch40:latest`.
|
||||||
* `push`: Pushes to the Haumdaucher registry.
|
* `push`: Pushes to the Haumdaucher registry.
|
||||||
* `deploy`: Executes `kubectl rollout restart deployment/sasch40 -n web` and monitors rollout status.
|
* `deploy`: Executes `kubectl rollout restart deployment/sasch40 -n web` and monitors rollout status.
|
||||||
@@ -171,7 +225,9 @@ This script:
|
|||||||
2. Verifies the Intro Screen and clicks Start.
|
2. Verifies the Intro Screen and clicks Start.
|
||||||
3. Simulates dialogue options on the Castle Balcony (Prologue).
|
3. Simulates dialogue options on the Castle Balcony (Prologue).
|
||||||
4. Tests movement and jump controls in Act II (Hopfen-Pfade).
|
4. Tests movement and jump controls in Act II (Hopfen-Pfade).
|
||||||
5. Interacts with hotspots, dialogue modals, and the Gstanzl duel in Act III (Taverne).
|
5. Interacts with all 5 rooms, hotspots, dialogue modals, Zuluman mirror, Luke zum Hinterraum, Gstanzl duel, and the Wildschwein duel in Act III (Taverne).
|
||||||
6. Advances through the Party Express and reaches the Royal Certificate.
|
6. Tests reload persistence in `localStorage`.
|
||||||
7. Captures 10 screenshots to verify zero visual regressions.
|
7. Advances through the Party Express (Act IV) and reaches the Royal Certificate (Act V).
|
||||||
8. Asserts zero console errors, zero failed HTTP requests, and zero broken images.
|
8. Tests the `🔄 NOCHMAL SPIELEN` button and verifies return to Intro.
|
||||||
|
9. Captures 25 screenshots to verify zero visual regressions.
|
||||||
|
10. Asserts zero console errors, zero failed HTTP requests, and zero broken images.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 740 KiB |
|
After Width: | Height: | Size: 719 KiB |
|
After Width: | Height: | Size: 638 KiB |
|
After Width: | Height: | Size: 860 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 824 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 990 KiB |
|
After Width: | Height: | Size: 990 KiB |
|
Before Width: | Height: | Size: 972 KiB After Width: | Height: | Size: 979 KiB |
|
Before Width: | Height: | Size: 972 KiB After Width: | Height: | Size: 979 KiB |
|
After Width: | Height: | Size: 979 KiB |
@@ -72,158 +72,130 @@ async function runPlaytest() {
|
|||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
|
||||||
// 2. PROLOGUE SCREEN (Balcony Scene)
|
// 2. PROLOGUE SCREEN (Balcony Scene)
|
||||||
console.log('📸 02: Capturing Prologue Screen (Verifying banner/portrait alignment)...');
|
console.log('📸 02a: Capturing Prologue Screen (NAT opening)...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02_prologue_screen.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02a_prologue_nat.png') });
|
||||||
|
|
||||||
// Advance Prologue Dialogues
|
// Click first choice -> Flo Maier arrives
|
||||||
await page.waitForSelector('.choice-btn', { timeout: 5000 });
|
const natChoice = await page.$('.choice-btn');
|
||||||
const firstChoice = await page.$('.choice-btn');
|
if (natChoice) await natChoice.click();
|
||||||
if (firstChoice) {
|
await new Promise(r => setTimeout(r, 800));
|
||||||
await firstChoice.click();
|
|
||||||
await new Promise(r => setTimeout(r, 800));
|
|
||||||
console.log('📸 02b: Capturing Prologue Mop Dialog...');
|
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02b_prologue_mop.png') });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance to Platformer
|
console.log('📸 02b: Capturing Flo Maier Feierdeifl arrives...');
|
||||||
const mopChoice = await page.$('.choice-btn');
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02b_prologue_flo_feierdeifl.png') });
|
||||||
if (mopChoice) {
|
|
||||||
await mopChoice.click();
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. PLATFORMER PRE-LEVEL START SCREEN (Act II)
|
// Test Game Over branch: "Ja freilich, zünd ois o, Flo!"
|
||||||
console.log('📸 03: Capturing Platformer Start Screen Modal...');
|
const fireChoice = await page.$('.choice-btn:nth-child(1)');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03_platformer_start_modal.png') });
|
if (fireChoice) await fireChoice.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
|
||||||
// Click Start Act Button on the modal
|
console.log('📸 02c: Capturing Flo Maier Game Over (fiery castle)...');
|
||||||
const startPlatformerBtn = await page.$('.start-act-btn');
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02c_prologue_flo_game_over.png') });
|
||||||
if (startPlatformerBtn) {
|
|
||||||
await startPlatformerBtn.click();
|
// Reset via Löschzug
|
||||||
|
const resetFireBtn = await page.$('.game-over-btn');
|
||||||
|
if (resetFireBtn) await resetFireBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
|
||||||
|
// Skip NAT to Flo again
|
||||||
|
const natChoice2 = await page.$('.choice-btn');
|
||||||
|
if (natChoice2) await natChoice2.click();
|
||||||
|
await new Promise(r => setTimeout(r, 600));
|
||||||
|
|
||||||
|
// Decline Flo Maier -> Viktor arrives
|
||||||
|
const declineFloBtn = await page.$('.choice-btn:nth-child(2)');
|
||||||
|
if (declineFloBtn) await declineFloBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
|
||||||
|
console.log('📸 02d: Capturing Viktor Partyvampir arrives...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02d_prologue_viktor_vampire.png') });
|
||||||
|
|
||||||
|
// Test Game Over branch: "Ja geil, mach mi unsterblich!"
|
||||||
|
const biteChoice = await page.$('.choice-btn:nth-child(1)');
|
||||||
|
if (biteChoice) await biteChoice.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
|
||||||
|
console.log('📸 02e: Capturing Viktor Game Over (glitter dust in sun)...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02e_prologue_viktor_game_over.png') });
|
||||||
|
|
||||||
|
// Reset via Knoblauch
|
||||||
|
const resetVampBtn = await page.$('.game-over-btn');
|
||||||
|
if (resetVampBtn) await resetVampBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
|
||||||
|
// Advance to Mop safely: NAT -> Flo (decline) -> Viktor (decline) -> Mop
|
||||||
|
const natC = await page.$('.choice-btn');
|
||||||
|
if (natC) await natC.click();
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
const floC = await page.$('.choice-btn:nth-child(2)');
|
||||||
|
if (floC) await floC.click();
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
const vikC = await page.$('.choice-btn:nth-child(2)');
|
||||||
|
if (vikC) await vikC.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
|
||||||
|
console.log('📸 02f: Capturing Mop Dialogue (Sister Rescue Quest)...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02f_prologue_mop.png') });
|
||||||
|
|
||||||
|
// Advance to Akt II (Doodle Jump Schwesternrettung)
|
||||||
|
const startAkt2Btn = await page.$('.choice-btn');
|
||||||
|
if (startAkt2Btn) await startAkt2Btn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
// 3. AKT II: DOODLE JUMP SCHWESTERNRETTUNG
|
||||||
|
console.log('📸 03a: Capturing Akt II Doodle Jump Start Modal...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03a_doodle_jump_intro_modal.png') });
|
||||||
|
|
||||||
|
// Click Start Act Button on modal
|
||||||
|
const startDoodleBtn = await page.$('.start-act-btn');
|
||||||
|
if (startDoodleBtn) {
|
||||||
|
await startDoodleBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 800));
|
await new Promise(r => setTimeout(r, 800));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. PLATFORMER GAMEPLAY
|
// Capture Gameplay (Bouncing up platforms)
|
||||||
console.log('📸 04: Capturing Platformer Gameplay...');
|
console.log('📸 03b: Capturing Doodle Jump Gameplay...');
|
||||||
const btnA = await page.$('.btn-a');
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03b_doodle_jump_gameplay.png') });
|
||||||
if (btnA) {
|
|
||||||
await btnA.click();
|
|
||||||
await new Promise(r => setTimeout(r, 300));
|
|
||||||
}
|
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '04_platformer_gameplay.png') });
|
|
||||||
|
|
||||||
// 5. TAVERN: SCHANKRAUM (Gaststube)
|
// Trigger Sister 1 Rescue (Saskia)
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const app = document.querySelector('#app')?.__vue_app__;
|
||||||
|
const comp = app?._instance?.subTree?.children?.[1]?.children?.[0]?.component?.subTree?.children?.[0]?.component?.ctx;
|
||||||
|
// Alternatively trigger rescue directly via store or component
|
||||||
|
const pinia = app?.config?.globalProperties?.$pinia;
|
||||||
|
const store = pinia?._s.get('game');
|
||||||
|
if (store) {
|
||||||
|
store.addKronkorken(3);
|
||||||
|
store.addBeer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. TAVERN: SCHANKRAUM (Gaststube with Seamless Oak Floor)
|
||||||
console.log('🍺 Advancing to Tavern Act (Schankraum)...');
|
console.log('🍺 Advancing to Tavern Act (Schankraum)...');
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const app = document.querySelector('#app')?.__vue_app__;
|
const app = document.querySelector('#app')?.__vue_app__;
|
||||||
if (app && app.config?.globalProperties?.$pinia) {
|
const pinia = app?.config?.globalProperties?.$pinia;
|
||||||
const pinia = app.config.globalProperties.$pinia;
|
const store = pinia?._s.get('game');
|
||||||
for (const [id, store] of pinia._s.entries()) {
|
if (store) store.setAct('tavern');
|
||||||
if (id === 'game') store.setAct('tavern');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
console.log('📸 05: Capturing Tavern Gaststube (Schankraum)...');
|
|
||||||
|
console.log('📸 05: Capturing Tavern Schankraum (seamless oak floor)...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05_tavern_schankraum.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05_tavern_schankraum.png') });
|
||||||
|
|
||||||
// Test Verena Hotspot
|
// Test Double-Click / Double-Tap on Trapdoor to unlock Party Bunker
|
||||||
console.log('👥 Testing Verena Dialog...');
|
console.log('🚪 Testing double-click / double-tap on oak floor trapdoor...');
|
||||||
const verenaBtn = await page.$('.hotspot-verena');
|
const trapdoor = await page.$('.trapdoor-hotspot');
|
||||||
if (verenaBtn) {
|
if (trapdoor) {
|
||||||
await verenaBtn.click();
|
// First click: triggers subtle sound, still closed
|
||||||
await new Promise(r => setTimeout(r, 700));
|
await trapdoor.click();
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '06_tavern_verena_dialog.png') });
|
await new Promise(r => setTimeout(r, 150));
|
||||||
const actionBtn = await page.$('.dialog-actions .action-btn');
|
// Second click within 450ms: unlocks Party Bunker!
|
||||||
if (actionBtn) await actionBtn.click();
|
await trapdoor.click();
|
||||||
await new Promise(r => setTimeout(r, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test Armend Hotspot
|
|
||||||
console.log('🔨 Testing Armend Dialog...');
|
|
||||||
const armendBtn = await page.$('.hotspot-armend');
|
|
||||||
if (armendBtn) {
|
|
||||||
await armendBtn.click();
|
|
||||||
await new Promise(r => setTimeout(r, 700));
|
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '07_tavern_armend_dialog.png') });
|
|
||||||
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
||||||
if (actionBtn) await actionBtn.click();
|
|
||||||
await new Promise(r => setTimeout(r, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. TAVERN: MARKTPLATZ (Mario with spinning chainsaws)
|
|
||||||
console.log('🎪 Navigating to Marktplatz...');
|
|
||||||
const marktplatzTab = await page.$('.room-tab:nth-child(2)');
|
|
||||||
if (marktplatzTab) {
|
|
||||||
await marktplatzTab.click();
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
console.log('📸 08: Capturing Marktplatz (Mario chainsaws)...');
|
console.log('📸 12b: Capturing Secret VIP Party Bunker...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_marktplatz.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '12b_tavern_party_bunker_vip.png') });
|
||||||
|
|
||||||
// Click Mario
|
// Click Pü in Party Bunker
|
||||||
const marioBtn = await page.$('.hotspot-mario');
|
|
||||||
if (marioBtn) {
|
|
||||||
await marioBtn.click();
|
|
||||||
await new Promise(r => setTimeout(r, 700));
|
|
||||||
console.log('📸 09: Capturing Mario Dialog...');
|
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09_tavern_mario_dialog.png') });
|
|
||||||
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
||||||
if (actionBtn) await actionBtn.click();
|
|
||||||
await new Promise(r => setTimeout(r, 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click NAT (Josef)
|
|
||||||
const natBtn = await page.$('.hotspot-nat');
|
|
||||||
if (natBtn) {
|
|
||||||
await natBtn.click();
|
|
||||||
await new Promise(r => setTimeout(r, 700));
|
|
||||||
console.log('📸 09b: Capturing NAT (Josef) Dialog...');
|
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09b_tavern_nat_dialog.png') });
|
|
||||||
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
||||||
if (actionBtn) await actionBtn.click();
|
|
||||||
await new Promise(r => setTimeout(r, 400));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. TAVERN: HINTERLAND (Mane hiking)
|
|
||||||
console.log('🌲 Navigating to Hinterland...');
|
|
||||||
const hinterlandTab = await page.$('.room-tab:nth-child(3)');
|
|
||||||
if (hinterlandTab) {
|
|
||||||
await hinterlandTab.click();
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
|
||||||
console.log('📸 10: Capturing Hinterland (Mane hiking)...');
|
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '10_tavern_hinterland.png') });
|
|
||||||
|
|
||||||
// Click Mane
|
|
||||||
const maneBtn = await page.$('.hotspot-mane');
|
|
||||||
if (maneBtn) {
|
|
||||||
await maneBtn.click();
|
|
||||||
await new Promise(r => setTimeout(r, 700));
|
|
||||||
console.log('📸 11: Capturing Mane Dialog...');
|
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11_tavern_mane_dialog.png') });
|
|
||||||
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
||||||
if (actionBtn) await actionBtn.click();
|
|
||||||
await new Promise(r => setTimeout(r, 400));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. TAVERN: SECRET HINTERZIMMER (VIP Rave via Secret Tap)
|
|
||||||
console.log('🗝️ Returning to Schankraum to check Secret Tap (initially invisible)...');
|
|
||||||
const schankraumTab = await page.$('.room-tab:nth-child(1)');
|
|
||||||
if (schankraumTab) {
|
|
||||||
await schankraumTab.click();
|
|
||||||
await new Promise(r => setTimeout(r, 600));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🪩 Triggering Secret Tap Easter Egg...');
|
|
||||||
const secretTap = await page.$('.secret-tap-hotspot');
|
|
||||||
if (secretTap) {
|
|
||||||
await secretTap.click();
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
|
||||||
console.log('📸 12: Capturing Secret VIP Hinterzimmer...');
|
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '12_tavern_hinterzimmer_vip.png') });
|
|
||||||
|
|
||||||
// Click Pü in Hinterzimmer
|
|
||||||
const pueBtn = await page.$('.hotspot-pue');
|
const pueBtn = await page.$('.hotspot-pue');
|
||||||
if (pueBtn) {
|
if (pueBtn) {
|
||||||
await pueBtn.click();
|
await pueBtn.click();
|
||||||
@@ -234,16 +206,28 @@ async function runPlaytest() {
|
|||||||
if (actionBtn) await actionBtn.click();
|
if (actionBtn) await actionBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 400));
|
await new Promise(r => setTimeout(r, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return to Schankraum
|
||||||
|
const backToSchankraum = await page.$('.hotspot-door-back-vip');
|
||||||
|
if (backToSchankraum) {
|
||||||
|
await backToSchankraum.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
console.log('📸 12: Capturing Schankraum with revealed Luke zum Party Bunker...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '12_tavern_party_bunker_trapdoor.png') });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. ZULUMAN MAGIC MIRROR & GSTANZL DUEL
|
// Test Nebenraum (Atelier with Amy & Zuluman Mirror)
|
||||||
console.log('🪞 Testing Zuluman Magic Mirror...');
|
console.log('🎨 Navigating to Nebenraum (Kemenate / Atelier)...');
|
||||||
const backToGaststube = await page.$('.hotspot-door-back-vip');
|
const nrTab = await page.$('.room-tab:nth-child(2)');
|
||||||
if (backToGaststube) {
|
if (nrTab) await nrTab.click();
|
||||||
await backToGaststube.click();
|
await new Promise(r => setTimeout(r, 800));
|
||||||
await new Promise(r => setTimeout(r, 600));
|
|
||||||
}
|
console.log('📸 05b: Capturing Nebenraum (Atelier & Mirror)...');
|
||||||
const mirrorBtn = await page.$('.hotspot-mirror');
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05b_tavern_nebenraum.png') });
|
||||||
|
|
||||||
|
// Test Zuluman Mirror in Nebenraum
|
||||||
|
const mirrorBtn = await page.$('.hotspot-mirror-nebenraum');
|
||||||
if (mirrorBtn) {
|
if (mirrorBtn) {
|
||||||
await mirrorBtn.click();
|
await mirrorBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 700));
|
await new Promise(r => setTimeout(r, 700));
|
||||||
@@ -254,105 +238,179 @@ async function runPlaytest() {
|
|||||||
await new Promise(r => setTimeout(r, 400));
|
await new Promise(r => setTimeout(r, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('⚔️ Testing Gstanzl-Duel and [✕] Exit Button...');
|
// Test Marktplatz & Wildschwein Duel
|
||||||
|
console.log('🎪 Navigating to Marktplatz...');
|
||||||
|
const marktplatzTab = await page.$('.room-tab:nth-child(3)');
|
||||||
|
if (marktplatzTab) await marktplatzTab.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
|
||||||
|
console.log('📸 08: Capturing Marktplatz...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_marktplatz.png') });
|
||||||
|
|
||||||
|
const natBtn = await page.$('.hotspot-nat');
|
||||||
|
if (natBtn) {
|
||||||
|
await natBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 700));
|
||||||
|
console.log('📸 09b: Capturing NAT Dialog...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09b_tavern_nat_dialog.png') });
|
||||||
|
|
||||||
|
// Click "🐗 Zum Wildschwein-Duell!" -> opens INTRO MODAL
|
||||||
|
const startBoarIntroBtn = await page.$('.dialog-actions .action-btn');
|
||||||
|
if (startBoarIntroBtn) {
|
||||||
|
await startBoarIntroBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
console.log('📸 09c: Capturing Wildschwein Duel INTRO modal...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09c_tavern_boar_duel_intro.png') });
|
||||||
|
|
||||||
|
// Start Duel from Intro
|
||||||
|
const startBoarDuelBtn = await page.$('.start-duel-action-btn');
|
||||||
|
if (startBoarDuelBtn) {
|
||||||
|
await startBoarDuelBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 800));
|
||||||
|
|
||||||
|
// Play Keiler
|
||||||
|
const keilerBtn = await page.$('.boar-choice-btn:nth-child(1)');
|
||||||
|
if (keilerBtn) {
|
||||||
|
await keilerBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 600));
|
||||||
|
console.log('📸 09d: Capturing Boar Duel Round Clash (verifying unhighlighted buttons)...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09d_tavern_boar_duel_round.png') });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal via ✕
|
||||||
|
const closeBoarBtn = await page.$('.boar-duel-box .duel-close-btn');
|
||||||
|
if (closeBoarBtn) {
|
||||||
|
await closeBoarBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 400));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Hinterland (Mane & Hansi departing toward Münchanien)
|
||||||
|
console.log('🌲 Navigating to Hinterland...');
|
||||||
|
const hinterlandTab = await page.$('.room-tab:nth-child(4)');
|
||||||
|
if (hinterlandTab) await hinterlandTab.click();
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
console.log('📸 10: Capturing Hinterland (Mane & Hansi departing toward Münchanien)...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '10_tavern_hinterland.png') });
|
||||||
|
|
||||||
|
// Click Hansi to hear about departure and evening concert
|
||||||
|
const hansiBtn = await page.$('.hotspot-hansi');
|
||||||
|
if (hansiBtn) {
|
||||||
|
await hansiBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 700));
|
||||||
|
console.log('📸 11: Capturing Hansi Departure Dialog...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11_tavern_hansi_dialog.png') });
|
||||||
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
||||||
|
if (actionBtn) await actionBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test Gstanzl Duel Intro & Gameplay
|
||||||
|
console.log('⚔️ Testing Gstanzl-Fechten Intro & Minigame...');
|
||||||
const duelBtn = await page.$('.duel-trigger');
|
const duelBtn = await page.$('.duel-trigger');
|
||||||
if (duelBtn) {
|
if (duelBtn) {
|
||||||
await duelBtn.click();
|
await duelBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 700));
|
await new Promise(r => setTimeout(r, 700));
|
||||||
console.log('📸 15: Capturing Gstanzl Duel with [✕] button...');
|
console.log('📸 15a: Capturing Gstanzl Duel INTRO modal...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '15_tavern_gstanzl_duel.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '15a_tavern_gstanzl_intro.png') });
|
||||||
|
|
||||||
// Test the [✕] close button
|
const startGstanzlBtn = await page.$('.start-duel-action-btn');
|
||||||
const closeDuelBtn = await page.$('.duel-close-btn');
|
if (startGstanzlBtn) {
|
||||||
if (closeDuelBtn) {
|
await startGstanzlBtn.click();
|
||||||
console.log('Testing duel exit via [✕] button...');
|
await new Promise(r => setTimeout(r, 700));
|
||||||
await closeDuelBtn.click();
|
console.log('📸 15b: Capturing Gstanzl Duel Round...');
|
||||||
await new Promise(r => setTimeout(r, 600));
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '15b_tavern_gstanzl_duel.png') });
|
||||||
const duelClosed = await page.$('.duel-box') === null;
|
|
||||||
console.log('Duel modal closed successfully by [✕]:', duelClosed);
|
// Close via ✕
|
||||||
|
const closeDuelBtn = await page.$('.duel-box .duel-close-btn');
|
||||||
|
if (closeDuelBtn) {
|
||||||
|
await closeDuelBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 400));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test reload persistence in Tavern Act
|
// 5. AKT IV: MITTELALTER GUITAR HERO (HANSIS ABSCHIEDSKONZERT)
|
||||||
console.log('💾 Testing game state reload persistence...');
|
console.log('🎸 Advancing to Akt IV (Mittelalter Guitar Hero)...');
|
||||||
const stateBeforeReload = await page.evaluate(() => {
|
|
||||||
const app = document.querySelector('#app')?.__vue_app__;
|
|
||||||
const pinia = app?.config?.globalProperties?.$pinia;
|
|
||||||
const s = pinia?._s.get('game');
|
|
||||||
return s ? { currentAct: s.currentAct, kronkorken: s.kronkorken, beers: s.beers } : null;
|
|
||||||
});
|
|
||||||
console.log('State before reload:', stateBeforeReload);
|
|
||||||
|
|
||||||
await page.reload({ waitUntil: 'networkidle0' });
|
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
|
||||||
|
|
||||||
const stateAfterReload = await page.evaluate(() => {
|
|
||||||
const app = document.querySelector('#app')?.__vue_app__;
|
|
||||||
const pinia = app?.config?.globalProperties?.$pinia;
|
|
||||||
const s = pinia?._s.get('game');
|
|
||||||
return s ? { currentAct: s.currentAct, kronkorken: s.kronkorken, beers: s.beers } : null;
|
|
||||||
});
|
|
||||||
console.log('State after reload:', stateAfterReload);
|
|
||||||
|
|
||||||
// 10. PARTY EXPRESS (Act IV)
|
|
||||||
console.log('🚂 Advancing to Party Express (Act IV)...');
|
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const app = document.querySelector('#app')?.__vue_app__;
|
const app = document.querySelector('#app')?.__vue_app__;
|
||||||
if (app && app.config?.globalProperties?.$pinia) {
|
const pinia = app?.config?.globalProperties?.$pinia;
|
||||||
const pinia = app.config.globalProperties.$pinia;
|
const store = pinia?._s.get('game');
|
||||||
for (const [id, store] of pinia._s.entries()) {
|
if (store) store.setAct('party_express');
|
||||||
if (id === 'game') store.setAct('party_express');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
console.log('📸 16: Capturing Party Express Start Screen Modal...');
|
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '16_party_express_start_modal.png') });
|
|
||||||
|
|
||||||
// Start Party Express
|
console.log('📸 16: Capturing Lute Hero Start Screen Modal...');
|
||||||
const startExpressBtn = await page.$('.start-act-btn');
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '16_lute_hero_start_modal.png') });
|
||||||
if (startExpressBtn) {
|
|
||||||
await startExpressBtn.click();
|
// Start Concert
|
||||||
|
const startConcertBtn = await page.$('.start-act-btn');
|
||||||
|
if (startConcertBtn) {
|
||||||
|
await startConcertBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 800));
|
await new Promise(r => setTimeout(r, 800));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('📸 17: Capturing Party Express Gameplay (Redesigned Bollerwagen)...');
|
// Hit string buttons
|
||||||
// Serve beer
|
console.log('🎸 Hitting lute strings...');
|
||||||
const throwBeerBtn = await page.$('.throw-beer-btn');
|
const btnLane0 = await page.$('.btn-lane-0');
|
||||||
if (throwBeerBtn) {
|
const btnLane1 = await page.$('.btn-lane-1');
|
||||||
await throwBeerBtn.click();
|
const btnLane2 = await page.$('.btn-lane-2');
|
||||||
await new Promise(r => setTimeout(r, 400));
|
if (btnLane0) await btnLane0.click();
|
||||||
}
|
await new Promise(r => setTimeout(r, 200));
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '17_party_express_gameplay.png') });
|
if (btnLane1) await btnLane1.click();
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
if (btnLane2) await btnLane2.click();
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
|
||||||
// 11. FINALE / CERTIFICATE (Act V)
|
console.log('📸 17: Capturing Castle Stage Rock Concert Gameplay...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '17_lute_hero_gameplay.png') });
|
||||||
|
|
||||||
|
// 6. AKT V: FINALE & CERTIFICATE
|
||||||
console.log('👑 Advancing to Finale Certificate (Act V)...');
|
console.log('👑 Advancing to Finale Certificate (Act V)...');
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
const app = document.querySelector('#app')?.__vue_app__;
|
const app = document.querySelector('#app')?.__vue_app__;
|
||||||
if (app && app.config?.globalProperties?.$pinia) {
|
const pinia = app?.config?.globalProperties?.$pinia;
|
||||||
const pinia = app.config.globalProperties.$pinia;
|
const store = pinia?._s.get('game');
|
||||||
for (const [id, store] of pinia._s.entries()) {
|
if (store) {
|
||||||
if (id === 'game') {
|
store.addKronkorken(35);
|
||||||
store.addKronkorken(35);
|
store.serveGuest();
|
||||||
store.serveGuest();
|
store.serveGuest();
|
||||||
store.serveGuest();
|
store.setAct('finale');
|
||||||
store.setAct('finale');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await new Promise(r => setTimeout(r, 1500));
|
await new Promise(r => setTimeout(r, 1500));
|
||||||
console.log('📸 18: Capturing Certificate Modal...');
|
|
||||||
|
console.log('📸 18: Capturing Certificate Screen...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '18_certificate_screen.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '18_certificate_screen.png') });
|
||||||
|
|
||||||
// Test NOCHMAL SPIELEN button
|
// Test Settings Modal (Gear icon ⚙️)
|
||||||
|
console.log('⚙️ Testing Settings Modal (Pause, Mute, Reset)...');
|
||||||
|
const settingsBtn = await page.$('.settings-toggle-btn');
|
||||||
|
if (settingsBtn) {
|
||||||
|
await settingsBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 700));
|
||||||
|
console.log('📸 19: Capturing Settings & Pause Modal...');
|
||||||
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '19_settings_pause_modal.png') });
|
||||||
|
|
||||||
|
// Click Resume
|
||||||
|
const resumeBtn = await page.$('.resume-btn');
|
||||||
|
if (resumeBtn) {
|
||||||
|
await resumeBtn.click();
|
||||||
|
await new Promise(r => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test NOCHMAL SPIELEN button on certificate
|
||||||
console.log('🔄 Testing NOCHMAL SPIELEN button...');
|
console.log('🔄 Testing NOCHMAL SPIELEN button...');
|
||||||
const restartBtn = await page.$('.restart-btn');
|
const restartBtn = await page.$('.restart-btn');
|
||||||
if (restartBtn) {
|
if (restartBtn) {
|
||||||
await restartBtn.click();
|
await restartBtn.click();
|
||||||
await new Promise(r => setTimeout(r, 1000));
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
console.log('📸 19: Capturing post-restart Intro screen...');
|
console.log('📸 20: Capturing post-restart Intro screen...');
|
||||||
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '19_post_restart_intro.png') });
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '20_post_restart_intro.png') });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify all images loaded properly
|
// Verify all images loaded properly
|
||||||
|
|||||||
@@ -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
|
// 135 BPM Medieval Techno Chiptune Beat
|
||||||
public startBGM() {
|
public startBGM() {
|
||||||
if (this.isMuted || this.bgmPlaying) return
|
if (this.isMuted || this.bgmPlaying) return
|
||||||
|
|||||||
@@ -11,18 +11,32 @@ import CertificateModal from './CertificateModal.vue'
|
|||||||
|
|
||||||
const store = useGameStore()
|
const store = useGameStore()
|
||||||
const platformerRef = ref<InstanceType<typeof PlatformerView> | null>(null)
|
const platformerRef = ref<InstanceType<typeof PlatformerView> | null>(null)
|
||||||
const inventoryOpen = ref(false)
|
const concertRef = ref<InstanceType<typeof PartyExpressView> | null>(null)
|
||||||
|
|
||||||
function handleJump() {
|
function handleJump() {
|
||||||
if (platformerRef.value) {
|
if (store.currentAct === 'platformer' && platformerRef.value) {
|
||||||
platformerRef.value.onJump()
|
platformerRef.value.onJump()
|
||||||
|
} else if (store.currentAct === 'party_express' && concertRef.value) {
|
||||||
|
concertRef.value.hitLane(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDpad(dir: 'left' | 'right', pressed: boolean) {
|
function handleButtonB() {
|
||||||
if (platformerRef.value) {
|
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 === 'left') platformerRef.value.onLeft(pressed)
|
||||||
if (dir === 'right') platformerRef.value.onRight(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()
|
store.toggleMute()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleInventory() {
|
const settingsOpen = ref(false)
|
||||||
inventoryOpen.value = !inventoryOpen.value
|
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -45,11 +77,11 @@ function toggleInventory() {
|
|||||||
<div class="chassis-brand">
|
<div class="chassis-brand">
|
||||||
<span class="brand-text">KÖNIG SASCHAS KRONKORKENKREUZZUG</span>
|
<span class="brand-text">KÖNIG SASCHAS KRONKORKENKREUZZUG</span>
|
||||||
<span v-if="store.currentAct !== 'intro'" class="brand-act">
|
<span v-if="store.currentAct !== 'intro'" class="brand-act">
|
||||||
{{ store.currentAct === 'prologue' ? '• AKT I' : store.currentAct === 'platformer' ? '• AKT II' : store.currentAct === 'tavern' ? '• AKT III' : store.currentAct === 'party_express' ? '• AKT IV' : store.currentAct === 'finale' ? '• AKT V' : '' }}
|
{{ store.currentAct === 'prologue' ? 'AKT I' : store.currentAct === 'platformer' ? 'AKT II' : store.currentAct === 'tavern' ? 'AKT III' : store.currentAct === 'party_express' ? 'AKT IV' : store.currentAct === 'finale' ? 'AKT V' : '' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="sound-toggle" @click="toggleAudio">
|
<button class="settings-toggle-btn" @click="openSettings" title="Einstellungen & Pause" aria-label="Einstellungen">
|
||||||
{{ store.soundMuted ? '🔇 TON' : '🔊 135 BPM' }}
|
⚙️
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -60,7 +92,7 @@ function toggleInventory() {
|
|||||||
<PrologueView v-else-if="store.currentAct === 'prologue'" />
|
<PrologueView v-else-if="store.currentAct === 'prologue'" />
|
||||||
<PlatformerView v-else-if="store.currentAct === 'platformer'" ref="platformerRef" />
|
<PlatformerView v-else-if="store.currentAct === 'platformer'" ref="platformerRef" />
|
||||||
<TavernView v-else-if="store.currentAct === 'tavern'" />
|
<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'" />
|
<CertificateModal v-else-if="store.currentAct === 'finale'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,7 +131,14 @@ function toggleInventory() {
|
|||||||
@mousedown="handleDpad('right', true)"
|
@mousedown="handleDpad('right', true)"
|
||||||
@mouseup="handleDpad('right', false)"
|
@mouseup="handleDpad('right', false)"
|
||||||
>▶</button>
|
>▶</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>
|
</div>
|
||||||
<span class="control-label">D-PAD</span>
|
<span class="control-label">D-PAD</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,9 +147,9 @@ function toggleInventory() {
|
|||||||
<div class="center-controls">
|
<div class="center-controls">
|
||||||
<div class="royal-crest">👑</div>
|
<div class="royal-crest">👑</div>
|
||||||
<div class="pill-buttons">
|
<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-key">SELECT</span>
|
||||||
<span class="pill-sub">BEUTEL</span>
|
<span class="pill-sub">PAUSE</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="pill-btn" @click="toggleAudio">
|
<button class="pill-btn" @click="toggleAudio">
|
||||||
<span class="pill-key">START</span>
|
<span class="pill-key">START</span>
|
||||||
@@ -124,7 +163,7 @@ function toggleInventory() {
|
|||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button
|
<button
|
||||||
class="action-round-btn btn-b"
|
class="action-round-btn btn-b"
|
||||||
@click="store.addBeer"
|
@click="handleButtonB"
|
||||||
>
|
>
|
||||||
<span class="btn-letter">B</span>
|
<span class="btn-letter">B</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -137,8 +176,8 @@ function toggleInventory() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="action-labels">
|
<div class="action-labels">
|
||||||
<span>O'ZAPFT (B)</span>
|
<span>{{ store.currentAct === 'party_express' ? 'SAITE G (B)' : "O'ZAPFT (B)" }}</span>
|
||||||
<span>SPRUNG (A)</span>
|
<span>{{ store.currentAct === 'party_express' ? 'SAITE A (A)' : 'SPRUNG (A)' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,21 +193,27 @@ function toggleInventory() {
|
|||||||
<span class="adv-icon">🍻</span>
|
<span class="adv-icon">🍻</span>
|
||||||
<span class="adv-val">{{ store.beers }} Helles</span>
|
<span class="adv-val">{{ store.beers }} Helles</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="inv-mini-btn" @click="toggleInventory">
|
|
||||||
🎒 Beutel
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Inventory Drawer Modal -->
|
<!-- Settings & Pause Modal -->
|
||||||
<div v-if="inventoryOpen" class="inventory-drawer" @click="inventoryOpen = false">
|
<div v-if="settingsOpen" class="settings-overlay" @click="closeSettings">
|
||||||
<div class="inventory-card" @click.stop>
|
<div class="settings-card" @click.stop>
|
||||||
<h3>👑 KÖNIGLICHER LEERGUTSACK</h3>
|
<div class="settings-header">
|
||||||
<ul class="inventory-items">
|
<h3 class="settings-title">⚙️ EINSTELLUNGEN & PAUSE</h3>
|
||||||
<li v-for="item in store.inventory" :key="item" class="inv-item">
|
<button class="settings-close-x" @click="closeSettings">✕</button>
|
||||||
✨ {{ item }}
|
</div>
|
||||||
</li>
|
<p class="settings-paused-hint">⏸️ Spiel und Timer pausiert</p>
|
||||||
</ul>
|
<div class="settings-actions">
|
||||||
<button class="close-inv-btn" @click="inventoryOpen = false">Schließen</button>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,8 +248,10 @@ function toggleInventory() {
|
|||||||
|
|
||||||
.chassis-brand {
|
.chassis-brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +265,7 @@ function toggleInventory() {
|
|||||||
|
|
||||||
.brand-act {
|
.brand-act {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 6px;
|
font-size: 5.5px;
|
||||||
color: #f59e0b;
|
color: #f59e0b;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -235,15 +282,23 @@ function toggleInventory() {
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sound-toggle {
|
.settings-toggle-btn {
|
||||||
background: #78350f;
|
background: rgba(120, 53, 15, 0.7);
|
||||||
color: #fef08a;
|
color: #ffd700;
|
||||||
border: 1px solid #ffd700;
|
border: 1px solid #ffd700;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-size: 13px;
|
||||||
font-size: 7px;
|
padding: 2px 6px;
|
||||||
padding: 4px 6px;
|
|
||||||
cursor: pointer;
|
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 */
|
||||||
@@ -301,17 +356,6 @@ function toggleInventory() {
|
|||||||
color: #ffd700;
|
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 Status Bar */
|
||||||
.parchment-scroll {
|
.parchment-scroll {
|
||||||
background: #fdf6e2;
|
background: #fdf6e2;
|
||||||
@@ -508,54 +552,119 @@ function toggleInventory() {
|
|||||||
color: #d4af37;
|
color: #d4af37;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inventory Modal */
|
/* Settings & Pause Modal */
|
||||||
.inventory-drawer {
|
.settings-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.75);
|
background: rgba(0, 0, 0, 0.85);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 200;
|
z-index: 250;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-card {
|
.settings-card {
|
||||||
background: #2b1810;
|
background: #2b1810;
|
||||||
border: 3px solid #d4af37;
|
border: 3px solid #d4af37;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 18px;
|
||||||
width: 80%;
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
color: #fef3c7;
|
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-family: 'Press Start 2P', monospace;
|
||||||
font-size: 9px;
|
font-size: 8px;
|
||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
margin-top: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-items {
|
.settings-close-x {
|
||||||
list-style: none;
|
background: #7f1d1d;
|
||||||
padding: 0;
|
color: #fef08a;
|
||||||
margin: 12px 0;
|
border: 1px solid #ef4444;
|
||||||
}
|
border-radius: 50%;
|
||||||
|
width: 26px;
|
||||||
.inv-item {
|
height: 26px;
|
||||||
font-family: 'MedievalSharp', serif;
|
display: flex;
|
||||||
font-size: 15px;
|
align-items: center;
|
||||||
padding: 4px 0;
|
justify-content: center;
|
||||||
border-bottom: 1px dashed #78350f;
|
font-size: 13px;
|
||||||
}
|
font-weight: bold;
|
||||||
|
|
||||||
.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;
|
|
||||||
cursor: pointer;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ function handleStart() {
|
|||||||
<div class="scroll-section">
|
<div class="scroll-section">
|
||||||
<h3 class="section-title">🎮 WAS DICH IM SPIEL ERWARTET</h3>
|
<h3 class="section-title">🎮 WAS DICH IM SPIEL ERWARTET</h3>
|
||||||
<ul class="features-list">
|
<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>☁️ 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> Sprich mit Verena, Lutz (ZTUL), Armend, Amy, Mane & Pü und gewinne das bayerische Beleidigungs-Gstanzl-Fechten!</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 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>
|
<li><strong>🏆 Highscore-Urkunde:</strong> Hol dir am Ende deine persönliche königliche Punktzahl und vergleiche dich mit allen Partygästen!</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ function selectChoice(choice: typeof PROLOGUE_DIALOGUES['start']['choices'][0])
|
|||||||
if (choice.action === 'unlockBridgerton') {
|
if (choice.action === 'unlockBridgerton') {
|
||||||
store.unlockEasterEgg('bridgerton')
|
store.unlockEasterEgg('bridgerton')
|
||||||
}
|
}
|
||||||
|
if (choice.action === 'resetPrologue') {
|
||||||
|
currentNodeId.value = 'start'
|
||||||
|
return
|
||||||
|
}
|
||||||
if (choice.action === 'startPlatformer') {
|
if (choice.action === 'startPlatformer') {
|
||||||
store.setAct('platformer')
|
store.setAct('platformer')
|
||||||
return
|
return
|
||||||
@@ -24,6 +28,9 @@ function selectChoice(choice: typeof PROLOGUE_DIALOGUES['start']['choices'][0])
|
|||||||
|
|
||||||
if (choice.nextId && PROLOGUE_DIALOGUES[choice.nextId]) {
|
if (choice.nextId && PROLOGUE_DIALOGUES[choice.nextId]) {
|
||||||
currentNodeId.value = 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="prologue-container">
|
<div class="prologue-container" :class="{ 'game-over-active': currentNode().isGameOver }">
|
||||||
<!-- Balcony Visual Scene with Regal Portrait Frame -->
|
<!-- 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="balcony-backdrop"></div>
|
||||||
<div class="scene-tag">
|
<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>
|
||||||
<div class="speaker-frame">
|
<div class="speaker-frame" :class="{ 'game-over-frame': currentNode().isGameOver }">
|
||||||
<img
|
<img
|
||||||
:src="currentSpeaker().portrait"
|
:src="currentSpeaker().portrait"
|
||||||
:alt="currentSpeaker().name"
|
:alt="currentSpeaker().name"
|
||||||
@@ -49,7 +56,7 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Retro Dialogue Card -->
|
<!-- Retro Dialogue Card -->
|
||||||
<div class="dialogue-card">
|
<div class="dialogue-card" :class="{ 'game-over-card': currentNode().isGameOver }">
|
||||||
<div class="dialogue-header">
|
<div class="dialogue-header">
|
||||||
<div class="speaker-name-badge">
|
<div class="speaker-name-badge">
|
||||||
<span class="speaker-name">{{ currentSpeaker().name }}</span>
|
<span class="speaker-name">{{ currentSpeaker().name }}</span>
|
||||||
@@ -57,7 +64,7 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dialogue-body">
|
<div class="dialogue-body" :class="{ 'game-over-body': currentNode().isGameOver }">
|
||||||
<p class="dialogue-text">„{{ currentNode().text }}“</p>
|
<p class="dialogue-text">„{{ currentNode().text }}“</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -66,9 +73,10 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
v-for="(choice, idx) in currentNode().choices"
|
v-for="(choice, idx) in currentNode().choices"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="choice-btn"
|
class="choice-btn"
|
||||||
|
:class="{ 'game-over-btn': currentNode().isGameOver }"
|
||||||
@click="selectChoice(choice)"
|
@click="selectChoice(choice)"
|
||||||
>
|
>
|
||||||
<span class="choice-arrow">▶</span>
|
<span class="choice-arrow">{{ currentNode().isGameOver ? '🔄' : '▶' }}</span>
|
||||||
<span class="choice-text">„{{ choice.text }}“</span>
|
<span class="choice-text">„{{ choice.text }}“</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -227,4 +235,54 @@ const currentSpeaker = () => CHARACTERS[currentNode().speakerId] || CHARACTERS['
|
|||||||
color: #f59e0b;
|
color: #f59e0b;
|
||||||
font-size: 10px;
|
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>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { sound } from '../audio/synthAudio'
|
|||||||
|
|
||||||
const store = useGameStore()
|
const store = useGameStore()
|
||||||
|
|
||||||
type TavernRoom = 'schankraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer'
|
type TavernRoom = 'schankraum' | 'nebenraum' | 'marktplatz' | 'hinterland' | 'hinterzimmer'
|
||||||
const currentRoom = ref<TavernRoom>('schankraum')
|
const currentRoom = ref<TavernRoom>('schankraum')
|
||||||
|
|
||||||
interface ActiveDialog {
|
interface ActiveDialog {
|
||||||
@@ -24,28 +24,141 @@ const activeDialog = ref<ActiveDialog | null>(null)
|
|||||||
const duelActive = ref(false)
|
const duelActive = ref(false)
|
||||||
const duelStep = ref(0)
|
const duelStep = ref(0)
|
||||||
const duelWon = ref(false)
|
const duelWon = ref(false)
|
||||||
|
const duelIntroOpen = ref(false)
|
||||||
const zulumanMirrorActive = ref(false)
|
const zulumanMirrorActive = ref(false)
|
||||||
|
|
||||||
const secretTapDiscovered = ref(store.unlockedEasterEggs.includes('hinterzimmer'))
|
const trapdoorDiscovered = ref(store.unlockedEasterEggs.includes('hinterzimmer'))
|
||||||
|
let lastTrapTap = 0
|
||||||
|
|
||||||
function switchRoom(room: TavernRoom) {
|
function switchRoom(room: TavernRoom) {
|
||||||
sound.playDialogueBlip()
|
sound.playDialogueBlip()
|
||||||
currentRoom.value = room
|
currentRoom.value = room
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerSecretTap() {
|
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()
|
sound.playFanfare()
|
||||||
secretTapDiscovered.value = true
|
trapdoorDiscovered.value = true
|
||||||
store.unlockEasterEgg('hinterzimmer')
|
store.unlockEasterEgg('hinterzimmer')
|
||||||
currentRoom.value = 'hinterzimmer'
|
currentRoom.value = 'hinterzimmer'
|
||||||
}
|
}
|
||||||
|
|
||||||
function enterSecretHinterzimmer() {
|
function enterPartyBunker() {
|
||||||
sound.playFanfare()
|
sound.playFanfare()
|
||||||
store.unlockEasterEgg('hinterzimmer')
|
store.unlockEasterEgg('hinterzimmer')
|
||||||
currentRoom.value = 'hinterzimmer'
|
currentRoom.value = 'hinterzimmer'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
const wildBoarRound = ref(1)
|
||||||
|
const wildBoarResultMsg = ref('')
|
||||||
|
const wildBoarLastPlayerMove = ref<WildBoarChoice | null>(null)
|
||||||
|
const wildBoarLastNatMove = ref<WildBoarChoice | null>(null)
|
||||||
|
const wildBoarGameOver = ref(false)
|
||||||
|
const wildBoarPlayerWon = ref(false)
|
||||||
|
|
||||||
|
function openWildBoarIntro() {
|
||||||
|
sound.playDialogueBlip()
|
||||||
|
wildBoarIntroOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function startWildBoarDuel() {
|
||||||
|
sound.playDialogueBlip()
|
||||||
|
wildBoarIntroOpen.value = false
|
||||||
|
wildBoarDuelActive.value = true
|
||||||
|
wildBoarPlayerScore.value = 0
|
||||||
|
wildBoarNatScore.value = 0
|
||||||
|
wildBoarRound.value = 1
|
||||||
|
wildBoarResultMsg.value = 'Wähle deine Taktik für Runde 1!'
|
||||||
|
wildBoarLastPlayerMove.value = null
|
||||||
|
wildBoarLastNatMove.value = null
|
||||||
|
wildBoarGameOver.value = false
|
||||||
|
wildBoarPlayerWon.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBoarMove(m: WildBoarChoice): string {
|
||||||
|
if (m === 'keiler') return '🐗 Keiler'
|
||||||
|
if (m === 'dickicht') return '🌿 Dickicht'
|
||||||
|
return '🏹 Lanze'
|
||||||
|
}
|
||||||
|
|
||||||
|
function playWildBoarMove(playerChoice: WildBoarChoice, event?: Event) {
|
||||||
|
if (event?.currentTarget) {
|
||||||
|
(event.currentTarget as HTMLElement).blur()
|
||||||
|
}
|
||||||
|
if (wildBoarGameOver.value) return
|
||||||
|
|
||||||
|
const choices: WildBoarChoice[] = ['keiler', 'dickicht', 'lanze']
|
||||||
|
const natChoice = choices[Math.floor(Math.random() * choices.length)]
|
||||||
|
|
||||||
|
wildBoarLastPlayerMove.value = playerChoice
|
||||||
|
wildBoarLastNatMove.value = natChoice
|
||||||
|
|
||||||
|
if (playerChoice === natChoice) {
|
||||||
|
sound.playDialogueBlip()
|
||||||
|
wildBoarResultMsg.value = 'Unentschieden! Zwei Keiler prallen mit den Köpfen zusammen!'
|
||||||
|
} else if (
|
||||||
|
(playerChoice === 'keiler' && natChoice === 'dickicht') ||
|
||||||
|
(playerChoice === 'dickicht' && natChoice === 'lanze') ||
|
||||||
|
(playerChoice === 'lanze' && natChoice === 'keiler')
|
||||||
|
) {
|
||||||
|
sound.playCoin()
|
||||||
|
wildBoarPlayerScore.value++
|
||||||
|
const explanation =
|
||||||
|
playerChoice === 'keiler'
|
||||||
|
? 'Dein Keiler bricht durch NATs Dickicht!'
|
||||||
|
: playerChoice === 'dickicht'
|
||||||
|
? 'Dein Eichen-Dickicht fängt NATs Lanze ab!'
|
||||||
|
: 'Deine Lanze stoppt NATs Keiler!'
|
||||||
|
wildBoarResultMsg.value = `Punkt für Sascha! ${explanation}`
|
||||||
|
} else {
|
||||||
|
sound.playGulp()
|
||||||
|
wildBoarNatScore.value++
|
||||||
|
const explanation =
|
||||||
|
natChoice === 'keiler'
|
||||||
|
? 'NATs Keiler bricht durch dein Dickicht!'
|
||||||
|
: natChoice === 'dickicht'
|
||||||
|
? 'NATs Dickicht verheddert deine Lanze!'
|
||||||
|
: 'NATs Lanze stoppt deinen Keiler!'
|
||||||
|
wildBoarResultMsg.value = `Punkt für NAT! ${explanation}`
|
||||||
|
}
|
||||||
|
|
||||||
|
wildBoarRound.value++
|
||||||
|
|
||||||
|
if (wildBoarPlayerScore.value >= 2) {
|
||||||
|
wildBoarGameOver.value = true
|
||||||
|
wildBoarPlayerWon.value = true
|
||||||
|
sound.playFanfare()
|
||||||
|
store.addKronkorken(10)
|
||||||
|
store.addBeer()
|
||||||
|
store.unlockEasterEgg('wildschwein_meister')
|
||||||
|
} else if (wildBoarNatScore.value >= 2) {
|
||||||
|
wildBoarGameOver.value = true
|
||||||
|
wildBoarPlayerWon.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openCharacterDialog(charId: string) {
|
function openCharacterDialog(charId: string) {
|
||||||
const char = CHARACTERS[charId]
|
const char = CHARACTERS[charId]
|
||||||
if (!char) return
|
if (!char) return
|
||||||
@@ -123,12 +236,11 @@ function openCharacterDialog(charId: string) {
|
|||||||
name: char.name,
|
name: char.name,
|
||||||
title: char.title,
|
title: char.title,
|
||||||
portrait: char.portrait,
|
portrait: char.portrait,
|
||||||
text: 'Servus Sascha! Schau dir meinen Wildschwein-Harnisch an – der glänzt wie ein Kronjuwel! Ich hab dir schon 3 kühle Helle und 5 Kronkorken gesichert, bevor die Meute alles wegsäuft!',
|
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: 'Sauber Josef! (+5 Kronkorken & Helles)',
|
actionLabel: '🐗 Zum Wildschwein-Duell!',
|
||||||
actionFn: () => {
|
actionFn: () => {
|
||||||
store.addKronkorken(5)
|
|
||||||
store.addBeer()
|
|
||||||
activeDialog.value = null
|
activeDialog.value = null
|
||||||
|
openWildBoarIntro()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (charId === 'mane') {
|
} else if (charId === 'mane') {
|
||||||
@@ -150,8 +262,8 @@ function openCharacterDialog(charId: string) {
|
|||||||
name: char.name,
|
name: char.name,
|
||||||
title: char.title,
|
title: char.title,
|
||||||
portrait: char.portrait,
|
portrait: char.portrait,
|
||||||
text: 'Habe die Ehre Sascha! Schau dir dieses Panorama an. Ich halte hier die Stellung und habe dir 4 Kronkorken gesichert!',
|
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: 'Danke Hansi! (+4 Kronkorken)',
|
actionLabel: 'Auf das Konzert, Hansi! (+4 Kronkorken)',
|
||||||
actionFn: () => {
|
actionFn: () => {
|
||||||
store.addKronkorken(4)
|
store.addKronkorken(4)
|
||||||
activeDialog.value = null
|
activeDialog.value = null
|
||||||
@@ -163,7 +275,7 @@ function openCharacterDialog(charId: string) {
|
|||||||
name: char.name,
|
name: char.name,
|
||||||
title: char.title,
|
title: char.title,
|
||||||
portrait: char.portrait,
|
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)',
|
actionLabel: 'Zünde den Kristall-Zauber, Pü! (+5 Kronkorken)',
|
||||||
actionFn: () => {
|
actionFn: () => {
|
||||||
store.addKronkorken(5)
|
store.addKronkorken(5)
|
||||||
@@ -189,7 +301,7 @@ function openCharacterDialog(charId: string) {
|
|||||||
name: char.name,
|
name: char.name,
|
||||||
title: char.title,
|
title: char.title,
|
||||||
portrait: char.portrait,
|
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)',
|
actionLabel: 'Prost Viktor! (+3 Kronkorken & Helles)',
|
||||||
actionFn: () => {
|
actionFn: () => {
|
||||||
store.addKronkorken(3)
|
store.addKronkorken(3)
|
||||||
@@ -206,8 +318,14 @@ function inspectMirror() {
|
|||||||
store.unlockEasterEgg('zuluman')
|
store.unlockEasterEgg('zuluman')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openGstanzlIntro() {
|
||||||
|
sound.playDialogueBlip()
|
||||||
|
duelIntroOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function startGstanzlDuel() {
|
function startGstanzlDuel() {
|
||||||
sound.playDialogueBlip()
|
sound.playDialogueBlip()
|
||||||
|
duelIntroOpen.value = false
|
||||||
duelActive.value = true
|
duelActive.value = true
|
||||||
duelStep.value = 0
|
duelStep.value = 0
|
||||||
duelWon.value = false
|
duelWon.value = false
|
||||||
@@ -238,7 +356,7 @@ function proceedToExpress() {
|
|||||||
|
|
||||||
<!-- ================= 1. GASTSTUBE (SCHANKRAUM) ================= -->
|
<!-- ================= 1. GASTSTUBE (SCHANKRAUM) ================= -->
|
||||||
<template v-if="currentRoom === 'schankraum'">
|
<template v-if="currentRoom === 'schankraum'">
|
||||||
<img src="/assets/scenes/schankraum_v2.jpg?v=2" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
|
<img src="/assets/scenes/schankraum_v2.jpg?v=3" alt="Gaststube Zum wummernden Ritter" class="tavern-bg" />
|
||||||
|
|
||||||
<!-- Verena on the wooden gallery -->
|
<!-- Verena on the wooden gallery -->
|
||||||
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
|
<button class="hotspot hotspot-verena" @click="openCharacterDialog('verena')">
|
||||||
@@ -246,6 +364,12 @@ function proceedToExpress() {
|
|||||||
<span class="hotspot-badge">Verena 👑</span>
|
<span class="hotspot-badge">Verena 👑</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Door to Nebenraum (Kemenate / Atelier) -->
|
||||||
|
<button class="hotspot door-hotspot hotspot-door-nebenraum" @click="switchRoom('nebenraum')">
|
||||||
|
<span class="door-icon">🚪</span>
|
||||||
|
<span class="hotspot-badge glow-door">Zum Nebenraum ▶</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Armend at the forge/anvil -->
|
<!-- Armend at the forge/anvil -->
|
||||||
<button class="hotspot hotspot-armend" @click="openCharacterDialog('armend')">
|
<button class="hotspot hotspot-armend" @click="openCharacterDialog('armend')">
|
||||||
<img :src="CHARACTERS.armend.portrait" class="hotspot-thumb" alt="Armend" />
|
<img :src="CHARACTERS.armend.portrait" class="hotspot-thumb" alt="Armend" />
|
||||||
@@ -258,43 +382,61 @@ function proceedToExpress() {
|
|||||||
<span class="hotspot-badge">Lutz (ZTUL) 🎩</span>
|
<span class="hotspot-badge">Lutz (ZTUL) 🎩</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Amy at the tavern table -->
|
|
||||||
<button class="hotspot hotspot-amy" @click="openCharacterDialog('amy')">
|
|
||||||
<img :src="CHARACTERS.amy.portrait" class="hotspot-thumb" alt="Amy" />
|
|
||||||
<span class="hotspot-badge">Amy 🎨</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Zuluman Magic Mirror on the pillar -->
|
|
||||||
<button class="hotspot hotspot-mirror" @click="inspectMirror">
|
|
||||||
<span class="mirror-icon">🪞</span>
|
|
||||||
<span class="hotspot-badge pulse">Zuluman-Spiegel</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Doorway to Marktplatz -->
|
<!-- Doorway to Marktplatz -->
|
||||||
<button class="hotspot door-hotspot hotspot-door-marktplatz" @click="switchRoom('marktplatz')">
|
<button class="hotspot door-hotspot hotspot-door-marktplatz" @click="switchRoom('marktplatz')">
|
||||||
<span class="door-icon">🚪</span>
|
<span class="door-icon">🎪</span>
|
||||||
<span class="hotspot-badge glow-door">Zum Marktplatz ▶</span>
|
<span class="hotspot-badge glow-door">Zum Marktplatz ▶</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- SECRET EASTER EGG TRIGGER: Hidden Beer Tap to Hinterzimmer (Completely invisible until clicked) -->
|
<!-- Luke zum Party Bunker (100% invisible until double-clicked) -->
|
||||||
<button
|
<button
|
||||||
class="hotspot secret-tap-hotspot"
|
class="hotspot trapdoor-hotspot"
|
||||||
:class="{ 'tap-invisible': !secretTapDiscovered && !store.unlockedEasterEggs.includes('hinterzimmer') }"
|
:class="{ 'trap-invisible': !trapdoorDiscovered && !store.unlockedEasterEggs.includes('hinterzimmer') }"
|
||||||
@click="triggerSecretTap"
|
@click="handleTrapdoorClick"
|
||||||
:title="secretTapDiscovered || store.unlockedEasterEggs.includes('hinterzimmer') ? 'Ein sonderbarer Zapfhahn...' : undefined"
|
@dblclick="triggerTrapdoor"
|
||||||
|
:title="trapdoorDiscovered || store.unlockedEasterEggs.includes('hinterzimmer') ? 'Luke zum Party Bunker' : undefined"
|
||||||
>
|
>
|
||||||
<template v-if="secretTapDiscovered || store.unlockedEasterEggs.includes('hinterzimmer')">
|
<template v-if="trapdoorDiscovered || store.unlockedEasterEggs.includes('hinterzimmer')">
|
||||||
<span class="secret-tap-icon pulse">🗝️</span>
|
<span class="trapdoor-icon">🚪</span>
|
||||||
<span class="secret-badge">Geheimer Zapfhahn</span>
|
<span class="trapdoor-badge">Luke zum Party Bunker 🪩</span>
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- ================= 2. MARKTPLATZ ================= -->
|
<!-- ================= 2. NEBENRAUM (KEMENATE & ATELIER) ================= -->
|
||||||
<template v-else-if="currentRoom === 'marktplatz'">
|
<template v-else-if="currentRoom === 'nebenraum'">
|
||||||
<img src="/assets/scenes/marktplatz.jpg" alt="Marktplatz Haumdaucher" class="tavern-bg" />
|
<img src="/assets/scenes/nebenraum_v2.jpg?v=2" alt="Königlicher Nebenraum & Atelier" class="tavern-bg" />
|
||||||
|
|
||||||
<!-- Gaukler Mario juggling chainsaws with animations & flying sparks -->
|
<!-- Zuluman Magic Mirror on the timber wall -->
|
||||||
|
<button class="hotspot hotspot-mirror-nebenraum" @click="inspectMirror">
|
||||||
|
<span class="mirror-icon">🪞</span>
|
||||||
|
<span class="hotspot-badge pulse">Zuluman-Spiegel</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Amy at the easel painting the canvas -->
|
||||||
|
<button class="hotspot hotspot-amy-nebenraum" @click="openCharacterDialog('amy')">
|
||||||
|
<img :src="CHARACTERS.amy.portrait" class="hotspot-thumb" alt="Amy" />
|
||||||
|
<span class="hotspot-badge">Amy 🎨</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Door back to Gaststube (Schankraum) -->
|
||||||
|
<button class="hotspot door-hotspot hotspot-door-back-gaststube-nr" @click="switchRoom('schankraum')">
|
||||||
|
<span class="door-icon">🍺</span>
|
||||||
|
<span class="hotspot-badge glow-door">◀ Zur Gaststube</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ================= 3. MARKTPLATZ ================= -->
|
||||||
|
<template v-else-if="currentRoom === 'marktplatz'">
|
||||||
|
<img src="/assets/scenes/marktplatz_v2.jpg?v=2" alt="Marktplatz Haumdaucher" class="tavern-bg" />
|
||||||
|
|
||||||
|
<!-- NAT on the festival stage telling stories of his girls and heroic deeds -->
|
||||||
|
<button class="hotspot hotspot-nat" @click="openCharacterDialog('nat')">
|
||||||
|
<img :src="CHARACTERS.nat.portrait" class="hotspot-thumb" alt="NAT" />
|
||||||
|
<span class="hotspot-badge">NAT (Josef) 🐗</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Gaukler Mario juggling chainsaws & hops on the cobblestones -->
|
||||||
<div class="mario-container">
|
<div class="mario-container">
|
||||||
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
|
<button class="hotspot hotspot-mario" @click="openCharacterDialog('mario')">
|
||||||
<div class="chainsaw-circus">
|
<div class="chainsaw-circus">
|
||||||
@@ -308,15 +450,9 @@ function proceedToExpress() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- NAT at the fountain -->
|
|
||||||
<button class="hotspot hotspot-nat" @click="openCharacterDialog('nat')">
|
|
||||||
<img :src="CHARACTERS.nat.portrait" class="hotspot-thumb" alt="NAT" />
|
|
||||||
<span class="hotspot-badge">NAT (Josef) 🐗</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Door back to Gaststube -->
|
<!-- Door back to Gaststube -->
|
||||||
<button class="hotspot door-hotspot hotspot-door-back-gaststube" @click="switchRoom('schankraum')">
|
<button class="hotspot door-hotspot hotspot-door-back-gaststube" @click="switchRoom('schankraum')">
|
||||||
<span class="door-icon">🚪</span>
|
<span class="door-icon">🍺</span>
|
||||||
<span class="hotspot-badge glow-door">◀ Zur Gaststube</span>
|
<span class="hotspot-badge glow-door">◀ Zur Gaststube</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -378,7 +514,7 @@ function proceedToExpress() {
|
|||||||
<!-- Door back to Gaststube -->
|
<!-- Door back to Gaststube -->
|
||||||
<button class="hotspot door-hotspot hotspot-door-back-vip" @click="switchRoom('schankraum')">
|
<button class="hotspot door-hotspot hotspot-door-back-vip" @click="switchRoom('schankraum')">
|
||||||
<span class="door-icon">🚪</span>
|
<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>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -388,9 +524,10 @@ function proceedToExpress() {
|
|||||||
<div class="tavern-bar-title">
|
<div class="tavern-bar-title">
|
||||||
{{
|
{{
|
||||||
currentRoom === 'schankraum' ? '🍻 AKT III: GASTSTUBE ZUM WUMMERNDEN RITTER 🍻' :
|
currentRoom === 'schankraum' ? '🍻 AKT III: GASTSTUBE ZUM WUMMERNDEN RITTER 🍻' :
|
||||||
|
currentRoom === 'nebenraum' ? '🎨 AKT III: KÖNIGLICHER NEBENRAUM & ATELIER 🎨' :
|
||||||
currentRoom === 'marktplatz' ? '🎪 AKT III: HAUMDAUCHER MARKTPLATZ 🎪' :
|
currentRoom === 'marktplatz' ? '🎪 AKT III: HAUMDAUCHER MARKTPLATZ 🎪' :
|
||||||
currentRoom === 'hinterland' ? '🌲 AKT III: ALPINES HINTERLAND 🌲' :
|
currentRoom === 'hinterland' ? '🌲 AKT III: ALPINES HINTERLAND 🌲' :
|
||||||
'🪩 AKT III: GEHEIMES VIP-HINTERZIMMER 🪩'
|
'🪩 AKT III: DER GEHEIME PARTY BUNKER 🪩'
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -403,6 +540,13 @@ function proceedToExpress() {
|
|||||||
>
|
>
|
||||||
🍺 Gaststube
|
🍺 Gaststube
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="room-tab"
|
||||||
|
:class="{ active: currentRoom === 'nebenraum' }"
|
||||||
|
@click="switchRoom('nebenraum')"
|
||||||
|
>
|
||||||
|
🎨 Nebenraum
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="room-tab"
|
class="room-tab"
|
||||||
:class="{ active: currentRoom === 'marktplatz' }"
|
:class="{ active: currentRoom === 'marktplatz' }"
|
||||||
@@ -423,18 +567,18 @@ function proceedToExpress() {
|
|||||||
:class="{ active: currentRoom === 'hinterzimmer' }"
|
:class="{ active: currentRoom === 'hinterzimmer' }"
|
||||||
@click="switchRoom('hinterzimmer')"
|
@click="switchRoom('hinterzimmer')"
|
||||||
>
|
>
|
||||||
🪩 Hinterzimmer
|
🪩 Party Bunker
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Row (Duel & Next Level) -->
|
<!-- Action Row (Duel & Next Level) -->
|
||||||
<div class="tavern-nav-buttons">
|
<div class="tavern-nav-buttons">
|
||||||
<button class="duel-trigger" @click="startGstanzlDuel">
|
<button class="duel-trigger" @click="openGstanzlIntro">
|
||||||
⚔️ Gstanzl-Fechten
|
⚔️ Gstanzl-Fechten
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="next-level-btn" @click="proceedToExpress">
|
<button class="next-level-btn" @click="proceedToExpress">
|
||||||
➡️ Zum Fest-Konvoi!
|
🎸 Zu Akt IV: Abschiedskonzert!
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -477,6 +621,50 @@ function proceedToExpress() {
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Gstanzl Duel Modal -->
|
||||||
<div v-if="duelActive" class="dialog-overlay">
|
<div v-if="duelActive" class="dialog-overlay">
|
||||||
<div class="dialog-box duel-box">
|
<div class="dialog-box duel-box">
|
||||||
@@ -514,6 +702,79 @@ function proceedToExpress() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Wildschwein Schnick-Schnack-Schnuck Minigame Modal -->
|
||||||
|
<div v-if="wildBoarDuelActive" class="dialog-overlay">
|
||||||
|
<div class="dialog-box boar-duel-box">
|
||||||
|
<div class="boar-header">
|
||||||
|
<div class="boar-title-group">
|
||||||
|
<h3>🐗 WILDSCHWEIN-SCHNICK-SCHNACK-SCHNUCK</h3>
|
||||||
|
<span class="boar-score">Stand: Sascha {{ wildBoarPlayerScore }} : {{ wildBoarNatScore }} NAT (Best of 3)</span>
|
||||||
|
</div>
|
||||||
|
<button class="duel-close-btn" @click="wildBoarDuelActive = false" aria-label="Duell verlassen">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rules / Explanations -->
|
||||||
|
<div class="boar-rules-banner">
|
||||||
|
🐗 Keiler bricht 🌿 Dickicht | 🌿 Dickicht fängt 🏹 Lanze | 🏹 Lanze erlegt 🐗 Keiler
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="boar-arena">
|
||||||
|
<div class="boar-status-msg">
|
||||||
|
{{ wildBoarResultMsg }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="wildBoarLastPlayerMove && wildBoarLastNatMove" class="boar-clash">
|
||||||
|
<div class="clash-side">
|
||||||
|
<span class="clash-label">Sascha:</span>
|
||||||
|
<span class="clash-move">{{ formatBoarMove(wildBoarLastPlayerMove) }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="clash-vs">VS</span>
|
||||||
|
<div class="clash-side">
|
||||||
|
<span class="clash-label">NAT:</span>
|
||||||
|
<span class="clash-move">{{ formatBoarMove(wildBoarLastNatMove) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Move Choices -->
|
||||||
|
<div v-if="!wildBoarGameOver" class="boar-choices">
|
||||||
|
<button class="boar-choice-btn" @click="playWildBoarMove('keiler', $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', $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', $event)">
|
||||||
|
<span class="choice-icon">🏹</span>
|
||||||
|
<span class="choice-name">Lanze</span>
|
||||||
|
<span class="choice-sub">Erlegt Keiler</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game Over Screen -->
|
||||||
|
<div v-else class="boar-game-over">
|
||||||
|
<template v-if="wildBoarPlayerWon">
|
||||||
|
<h2 class="victory-title">👑 SASCHA IST WILDSCHWEIN-MEISTER!</h2>
|
||||||
|
<p class="boar-victory-desc">NAT verneigt sich ehrfürchtig! „Du kennst das Dickicht besser als jeder Keiler!“</p>
|
||||||
|
<p class="reward-text">+10 Kronkorken, 1 Kaltes Helles & Titel freigeschaltet!</p>
|
||||||
|
<button class="action-btn" @click="wildBoarDuelActive = false">Herrlich! Prost NAT!</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<h2 class="defeat-title">🐗 NAT GEWINNT DAS DUELL!</h2>
|
||||||
|
<p class="boar-defeat-desc">NAT lacht schallend: „Das Haumdaucher Dickicht verlangt höchste Meisterschaft, Sascha!“</p>
|
||||||
|
<div class="boar-retry-actions">
|
||||||
|
<button class="action-btn retry-btn" @click="startWildBoarDuel">Revanche fordern!</button>
|
||||||
|
<button class="close-btn" @click="wildBoarDuelActive = false">Vielleicht später</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -594,58 +855,68 @@ function proceedToExpress() {
|
|||||||
color: #38bdf8;
|
color: #38bdf8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Secret Tap Easter Egg */
|
/* Trapdoor to Party Bunker Easter Egg */
|
||||||
.secret-tap-hotspot {
|
.trapdoor-hotspot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 72%;
|
top: 84%;
|
||||||
left: 82%;
|
left: 35%;
|
||||||
background: rgba(234, 179, 8, 0.2);
|
transform: translate(-50%, -50%);
|
||||||
border: 1.5px dashed #ffd700;
|
background: rgba(212, 175, 55, 0.25);
|
||||||
border-radius: 12px;
|
border: 2px dashed #ffd700;
|
||||||
padding: 3px 6px;
|
border-radius: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
box-shadow: 0 0 12px rgba(255, 215, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secret-tap-hotspot.tap-invisible,
|
.trapdoor-hotspot.trap-invisible,
|
||||||
.secret-tap-hotspot.tap-invisible:hover,
|
.trapdoor-hotspot.trap-invisible:hover,
|
||||||
.secret-tap-hotspot.tap-invisible:active,
|
.trapdoor-hotspot.trap-invisible:active,
|
||||||
.secret-tap-hotspot.tap-invisible:focus {
|
.trapdoor-hotspot.trap-invisible:focus {
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
transform: translate(-50%, -50%) !important;
|
transform: translate(-50%, -50%) !important;
|
||||||
width: 50px;
|
width: 140px;
|
||||||
height: 50px;
|
height: 65px;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secret-tap-icon {
|
.trapdoor-icon {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secret-badge {
|
.trapdoor-badge {
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
font-size: 5.5px;
|
font-size: 6px;
|
||||||
color: #fef08a;
|
color: #fef08a;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 1. Schankraum Hotspots */
|
/* 1. Schankraum Hotspots */
|
||||||
.hotspot-verena { top: 17%; left: 28%; }
|
.hotspot-verena { top: 17%; left: 28%; }
|
||||||
.hotspot-mirror { top: 30%; left: 68%; }
|
.hotspot-door-nebenraum { top: 42%; left: 34%; }
|
||||||
.hotspot-amy { top: 45%; left: 32%; }
|
|
||||||
.hotspot-lutz { top: 54%; left: 56%; }
|
|
||||||
.hotspot-armend { top: 62%; left: 18%; }
|
.hotspot-armend { top: 62%; left: 18%; }
|
||||||
|
.hotspot-lutz { top: 54%; left: 56%; }
|
||||||
.hotspot-door-marktplatz { top: 50%; left: 84%; }
|
.hotspot-door-marktplatz { top: 50%; left: 84%; }
|
||||||
|
|
||||||
/* 2. Marktplatz Hotspots */
|
/* 2. Nebenraum Hotspots */
|
||||||
|
.hotspot-mirror-nebenraum { top: 36%; left: 24%; }
|
||||||
|
.hotspot-door-back-gaststube-nr { top: 48%; left: 56%; }
|
||||||
|
.hotspot-amy-nebenraum { top: 64%; left: 80%; }
|
||||||
|
|
||||||
|
/* 3. Marktplatz Hotspots */
|
||||||
.mario-container {
|
.mario-container {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 44%;
|
top: 66%;
|
||||||
left: 30%;
|
left: 70%;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,12 +970,12 @@ function proceedToExpress() {
|
|||||||
100% { opacity: 1; transform: scale(1.3); }
|
100% { opacity: 1; transform: scale(1.3); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotspot-nat { top: 62%; left: 70%; }
|
.hotspot-nat { top: 54%; left: 22%; }
|
||||||
.hotspot-door-back-gaststube { top: 72%; left: 20%; }
|
.hotspot-door-back-gaststube { top: 74%; left: 20%; }
|
||||||
.hotspot-door-hinterland { top: 32%; left: 76%; }
|
.hotspot-door-hinterland { top: 34%; left: 76%; }
|
||||||
|
|
||||||
/* 3. Hinterland Hotspots */
|
/* 3. Hinterland Hotspots */
|
||||||
.hotspot-mane { top: 44%; left: 36%; }
|
.hotspot-mane { top: 54%; left: 36%; }
|
||||||
.hiking-anim {
|
.hiking-anim {
|
||||||
animation: hikeBob 1.2s infinite ease-in-out;
|
animation: hikeBob 1.2s infinite ease-in-out;
|
||||||
}
|
}
|
||||||
@@ -719,7 +990,7 @@ function proceedToExpress() {
|
|||||||
transform: rotate(20deg);
|
transform: rotate(20deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hotspot-hansi { top: 60%; left: 74%; }
|
.hotspot-hansi { top: 56%; left: 74%; }
|
||||||
.hotspot-door-back-marktplatz { top: 72%; left: 22%; }
|
.hotspot-door-back-marktplatz { top: 72%; left: 22%; }
|
||||||
|
|
||||||
/* 4. Hinterzimmer Hotspots & Strobe */
|
/* 4. Hinterzimmer Hotspots & Strobe */
|
||||||
@@ -1074,4 +1345,255 @@ function proceedToExpress() {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: 'Press Start 2P', monospace;
|
font-family: 'Press Start 2P', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Wild Boar Duel Modal */
|
||||||
|
.boar-duel-box {
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
border-bottom: 1px solid #78350f;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-title-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-title-group h3 {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 7.5px;
|
||||||
|
color: #22c55e;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-score {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 6.5px;
|
||||||
|
color: #fef08a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-rules-banner {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
border: 1px dashed #22c55e;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 8.5px;
|
||||||
|
color: #bbf7d0;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-arena {
|
||||||
|
background: #1a0f0a;
|
||||||
|
border: 1.5px solid #78350f;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-status-msg {
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fef3c7;
|
||||||
|
min-height: 38px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-clash {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px dashed #451a03;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clash-side {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clash-label {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 6px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clash-move {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clash-vs {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 9px;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-choices {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-choice-btn {
|
||||||
|
background: #3b2014;
|
||||||
|
border: 1.5px solid #16a34a;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-choice-btn:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-choice-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
background: #14532d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.boar-choice-btn:hover {
|
||||||
|
background: #14532d;
|
||||||
|
border-color: #4ade80;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-name {
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
font-size: 7px;
|
||||||
|
color: #fef08a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-sub {
|
||||||
|
font-size: 8.5px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-game-over {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-victory-desc, .boar-defeat-desc {
|
||||||
|
font-family: 'MedievalSharp', serif;
|
||||||
|
font-size: 13.5px;
|
||||||
|
margin: 6px 0;
|
||||||
|
color: #fef3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defeat-title {
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'Press Start 2P', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boar-retry-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
background: #16a34a;
|
||||||
|
border-color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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>
|
</style>
|
||||||
|
|||||||
@@ -111,21 +111,48 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
|||||||
flo_maier: {
|
flo_maier: {
|
||||||
id: 'flo_maier',
|
id: 'flo_maier',
|
||||||
name: 'Flo Maier',
|
name: 'Flo Maier',
|
||||||
title: 'Hof-DJ & Sound-Ingenieur',
|
title: 'Hof-DJ & Feierdeifl',
|
||||||
portrait: '/assets/characters/flo_maier/portrait_v2.jpg',
|
portrait: '/assets/characters/flo_maier/portrait_v2.jpg',
|
||||||
description: 'Mastermind an den Turntables und Behüter der heiligen 135-BPM-Frequenz.',
|
description: 'Mastermind an den Turntables und berüchtigter Feierdeifl des Reiches.',
|
||||||
famousQuote: 'Dieser Track drückt mehr als die königliche Kutsche!',
|
famousQuote: 'Soll i ois ozindn?! A riesigs Freudenfeuer!',
|
||||||
themeColor: '#0284c7'
|
themeColor: '#0284c7'
|
||||||
},
|
},
|
||||||
viktor: {
|
viktor: {
|
||||||
id: 'viktor',
|
id: 'viktor',
|
||||||
name: 'Viktor',
|
name: 'Viktor',
|
||||||
title: 'Nachtwächter & Party-Ritter',
|
title: 'Partyvampir & Nachtwächter',
|
||||||
portrait: '/assets/characters/viktor/portrait_v2.jpg',
|
portrait: '/assets/characters/viktor/portrait_v2.jpg',
|
||||||
description: 'Wacht im samtenen Vampir-Umhang über das Nachtleben und sorgt für eiskaltes Helles.',
|
description: 'Wacht im samtenen Vampir-Umhang über das Nachtleben und lauert auf frisches Helles.',
|
||||||
famousQuote: 'Die Nacht ist jung und der Gerstensaft fließt in Strömen!',
|
famousQuote: 'Ein Biss, und du ravst für alle Ewigkeit!',
|
||||||
themeColor: '#b91c1c'
|
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: {
|
djs: {
|
||||||
id: 'djs',
|
id: 'djs',
|
||||||
name: 'Die Hof-DJs',
|
name: 'Die Hof-DJs',
|
||||||
@@ -145,3 +172,4 @@ export const CHARACTERS: Record<string, CharacterConfig> = {
|
|||||||
themeColor: '#e11d48'
|
themeColor: '#e11d48'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export interface DialogueChoice {
|
export interface DialogueChoice {
|
||||||
text: string
|
text: string
|
||||||
nextId?: string
|
nextId?: string
|
||||||
action?: 'addKronkorken' | 'unlockZuluman' | 'unlockBridgerton' | 'startPlatformer' | 'startExpress' | 'startFinale'
|
action?: 'addKronkorken' | 'unlockZuluman' | 'unlockBridgerton' | 'startPlatformer' | 'startExpress' | 'startFinale' | 'resetPrologue'
|
||||||
reward?: number
|
reward?: number
|
||||||
reply?: {
|
reply?: {
|
||||||
speakerId: string
|
speakerId: string
|
||||||
@@ -13,6 +13,7 @@ export interface DialogueNode {
|
|||||||
id: string
|
id: string
|
||||||
speakerId: string
|
speakerId: string
|
||||||
text: string
|
text: string
|
||||||
|
isGameOver?: boolean
|
||||||
choices: DialogueChoice[]
|
choices: DialogueChoice[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,12 +25,12 @@ export const PROLOGUE_DIALOGUES: Record<string, DialogueNode> = {
|
|||||||
choices: [
|
choices: [
|
||||||
{
|
{
|
||||||
text: 'Zefix Josef! Ohne Kronkorken kein Bier, und ohne Bier kein Bass!',
|
text: 'Zefix Josef! Ohne Kronkorken kein Bier, und ohne Bier kein Bass!',
|
||||||
nextId: 'mop_appears',
|
nextId: 'flo_maier_arrives',
|
||||||
action: 'unlockBridgerton'
|
action: 'unlockBridgerton'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Bridgerton?! Josef, konzentrier dich! Wo san meine 40 Kronkorken?!',
|
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?',
|
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!',
|
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: [
|
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'
|
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: {
|
mop_appears: {
|
||||||
id: 'mop_appears',
|
id: 'mop_appears',
|
||||||
speakerId: 'mop',
|
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: [
|
choices: [
|
||||||
{
|
{
|
||||||
text: 'Auf geht\'s Mop! Starte den Kronkorken-Kreuzzug!',
|
text: 'Auf geht\'s Mop! Meine Schwestern warten – Kronkorken-Sprungfedern an!',
|
||||||
action: 'startPlatformer'
|
action: 'startPlatformer'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -68,16 +123,41 @@ export const PROLOGUE_DIALOGUES: Record<string, DialogueNode> = {
|
|||||||
sascha_ready: {
|
sascha_ready: {
|
||||||
id: 'sascha_ready',
|
id: 'sascha_ready',
|
||||||
speakerId: 'sascha',
|
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: [
|
choices: [
|
||||||
{
|
{
|
||||||
text: 'Level 1 starten: Die Hopfen-Pfade!',
|
text: 'Akt II starten: Die Schwesternrettung!',
|
||||||
action: 'startPlatformer'
|
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 = [
|
export const GSTANZL_DUEL = [
|
||||||
{
|
{
|
||||||
insult: 'Dein Techno-Bass ist so dünn wie schales Exportbier!',
|
insult: 'Dein Techno-Bass ist so dünn wie schales Exportbier!',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface GameState {
|
|||||||
startTime: number
|
startTime: number
|
||||||
elapsedSeconds: number
|
elapsedSeconds: number
|
||||||
soundMuted: boolean
|
soundMuted: boolean
|
||||||
|
isPaused: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STORAGE_KEY = 'sasch40_game_save_v1'
|
export const STORAGE_KEY = 'sasch40_game_save_v1'
|
||||||
@@ -34,6 +35,7 @@ function getInitialState(): GameState {
|
|||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
elapsedSeconds: 0,
|
elapsedSeconds: 0,
|
||||||
soundMuted: false,
|
soundMuted: false,
|
||||||
|
isPaused: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window === 'undefined' || !window.localStorage) {
|
if (typeof window === 'undefined' || !window.localStorage) {
|
||||||
@@ -56,7 +58,8 @@ function getInitialState(): GameState {
|
|||||||
unlockedEasterEggs: Array.isArray(parsed.unlockedEasterEggs) ? parsed.unlockedEasterEggs : [],
|
unlockedEasterEggs: Array.isArray(parsed.unlockedEasterEggs) ? parsed.unlockedEasterEggs : [],
|
||||||
startTime: typeof parsed.startTime === 'number' ? parsed.startTime : defaults.startTime,
|
startTime: typeof parsed.startTime === 'number' ? parsed.startTime : defaults.startTime,
|
||||||
elapsedSeconds: typeof parsed.elapsedSeconds === 'number' ? parsed.elapsedSeconds : 0,
|
elapsedSeconds: typeof parsed.elapsedSeconds === 'number' ? parsed.elapsedSeconds : 0,
|
||||||
soundMuted: Boolean(parsed.soundMuted)
|
soundMuted: Boolean(parsed.soundMuted),
|
||||||
|
isPaused: false
|
||||||
}
|
}
|
||||||
if (state.soundMuted) {
|
if (state.soundMuted) {
|
||||||
sound.setMuted(true)
|
sound.setMuted(true)
|
||||||
@@ -197,6 +200,14 @@ export const useGameStore = defineStore('game', {
|
|||||||
this.soundMuted = sound.toggleMute()
|
this.soundMuted = sound.toggleMute()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
pauseGame() {
|
||||||
|
this.isPaused = true
|
||||||
|
},
|
||||||
|
|
||||||
|
resumeGame() {
|
||||||
|
this.isPaused = false
|
||||||
|
},
|
||||||
|
|
||||||
resetGame() {
|
resetGame() {
|
||||||
if (typeof window !== 'undefined' && window.localStorage) {
|
if (typeof window !== 'undefined' && window.localStorage) {
|
||||||
try {
|
try {
|
||||||
|
|||||||