diff --git a/projects/sasch40/AGENTS.md b/projects/sasch40/AGENTS.md index c01f85f..3f7d4ef 100644 --- a/projects/sasch40/AGENTS.md +++ b/projects/sasch40/AGENTS.md @@ -115,16 +115,29 @@ This document records the foundational design decisions, their rationale ("Why") --- -### 9. Easter Egg Invisibility Pattern (`TavernView.vue`) -* **The Decision**: Secret hotspots (e.g. `secret-tap-hotspot` in the Schankraum) must be 100% invisible initially. +### 9. Cellar Trapdoor & Easter Egg Invisibility Pattern (`TavernView.vue`) +* **The Decision**: Secret hotspots (the cellar trapdoor `.trapdoor-hotspot` in the Schankraum leading to the VIP Hinterzimmer) must be 100% invisible initially. * **Why**: An Easter egg marked with a glowing button or text badge is not a secret; it invites immediate trivial clicking. * **The Rule**: * The button must render 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. + * Only upon the user's first click does it reveal the door icon, label itself *„Luke zum Hinterraum“*, play a celebratory fanfare, unlock the Easter egg in the game store (+500 points), and transition to the secret room. --- -### 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 Hinterraum“* 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 the panoramic bench. + 5. **Geheimes VIP-Hinterzimmer**: 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. Must always include a visible `[✕]` close button so players are never trapped. + * **Wildschwein-Schnick-Schnack-Schnuck**: Best-of-3 tactical duel against NAT. Rules: 🐗 Keiler bricht 🌿 Dickicht, 🌿 Dickicht fängt 🏹 Lanze, 🏹 Lanze erlegt 🐗 Keiler. First to 2 points wins (+10 Korken, 1 Helles, `wildschwein_meister` achievement). Includes `[✕]` close button. + +--- + +### 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“*. * **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**: @@ -151,7 +164,7 @@ make all The `Makefile` targets: * `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`. * `push`: Pushes to the Haumdaucher registry. * `deploy`: Executes `kubectl rollout restart deployment/sasch40 -n web` and monitors rollout status. @@ -171,7 +184,9 @@ This script: 2. Verifies the Intro Screen and clicks Start. 3. Simulates dialogue options on the Castle Balcony (Prologue). 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). -6. Advances through the Party Express and reaches the Royal Certificate. -7. Captures 10 screenshots to verify zero visual regressions. -8. Asserts zero console errors, zero failed HTTP requests, and zero broken images. +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. Tests reload persistence in `localStorage`. +7. Advances through the Party Express (Act IV) and reaches the Royal Certificate (Act V). +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. diff --git a/projects/sasch40/frontend/public/assets/scenes/marktplatz.jpg b/projects/sasch40/frontend/public/assets/scenes/marktplatz.jpg index 81fae32..ea8eed3 100644 Binary files a/projects/sasch40/frontend/public/assets/scenes/marktplatz.jpg and b/projects/sasch40/frontend/public/assets/scenes/marktplatz.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/marktplatz_v2.jpg b/projects/sasch40/frontend/public/assets/scenes/marktplatz_v2.jpg new file mode 100644 index 0000000..ea8eed3 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/scenes/marktplatz_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/nebenraum.jpg b/projects/sasch40/frontend/public/assets/scenes/nebenraum.jpg new file mode 100644 index 0000000..ed7852d Binary files /dev/null and b/projects/sasch40/frontend/public/assets/scenes/nebenraum.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/nebenraum_v2.jpg b/projects/sasch40/frontend/public/assets/scenes/nebenraum_v2.jpg new file mode 100644 index 0000000..ed7852d Binary files /dev/null and b/projects/sasch40/frontend/public/assets/scenes/nebenraum_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/schankraum.jpg b/projects/sasch40/frontend/public/assets/scenes/schankraum.jpg index 2c9a67e..f9ef98e 100644 Binary files a/projects/sasch40/frontend/public/assets/scenes/schankraum.jpg and b/projects/sasch40/frontend/public/assets/scenes/schankraum.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/schankraum_v2.jpg b/projects/sasch40/frontend/public/assets/scenes/schankraum_v2.jpg index 2c9a67e..f9ef98e 100644 Binary files a/projects/sasch40/frontend/public/assets/scenes/schankraum_v2.jpg and b/projects/sasch40/frontend/public/assets/scenes/schankraum_v2.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/schankraum_v3.jpg b/projects/sasch40/frontend/public/assets/scenes/schankraum_v3.jpg new file mode 100644 index 0000000..f9ef98e Binary files /dev/null and b/projects/sasch40/frontend/public/assets/scenes/schankraum_v3.jpg differ diff --git a/projects/sasch40/frontend/scripts/playtest.mjs b/projects/sasch40/frontend/scripts/playtest.mjs index 3c4331b..14105b0 100644 --- a/projects/sasch40/frontend/scripts/playtest.mjs +++ b/projects/sasch40/frontend/scripts/playtest.mjs @@ -151,13 +151,52 @@ async function runPlaytest() { await new Promise(r => setTimeout(r, 400)); } - // 6. TAVERN: MARKTPLATZ (Mario with spinning chainsaws) + // 5b. TAVERN: NEBENRAUM (Amy painting & Zuluman Mirror) + console.log('🎨 Navigating to Nebenraum (Kemenate / Atelier)...'); + const nebenraumDoor = await page.$('.hotspot-door-nebenraum'); + if (nebenraumDoor) { + await nebenraumDoor.click(); + } else { + const nrTab = await page.$('.room-tab:nth-child(2)'); + if (nrTab) await nrTab.click(); + } + await new Promise(r => setTimeout(r, 1000)); + console.log('📸 05b: Capturing Nebenraum (Atelier & Mirror)...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05b_tavern_nebenraum.png') }); + + // Test Amy in Nebenraum + console.log('🎨 Testing Amy Dialog in Nebenraum...'); + const amyBtn = await page.$('.hotspot-amy-nebenraum'); + if (amyBtn) { + await amyBtn.click(); + await new Promise(r => setTimeout(r, 700)); + console.log('📸 05c: Capturing Amy Dialog...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05c_tavern_amy_dialog.png') }); + const actionBtn = await page.$('.dialog-actions .action-btn'); + if (actionBtn) await actionBtn.click(); + await new Promise(r => setTimeout(r, 400)); + } + + // Test Zuluman Mirror in Nebenraum + console.log('🪞 Testing Zuluman Magic Mirror in Nebenraum...'); + const mirrorBtn = await page.$('.hotspot-mirror-nebenraum'); + if (mirrorBtn) { + await mirrorBtn.click(); + await new Promise(r => setTimeout(r, 700)); + console.log('📸 14: Capturing Zuluman Mirror...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '14_tavern_zuluman_mirror.png') }); + const closeBtn = await page.$('.mirror-box .action-btn'); + if (closeBtn) await closeBtn.click(); + await new Promise(r => setTimeout(r, 400)); + } + + // 6. TAVERN: MARKTPLATZ (Mario with spinning chainsaws & NAT on stage) console.log('🎪 Navigating to Marktplatz...'); - const marktplatzTab = await page.$('.room-tab:nth-child(2)'); + const marktplatzTab = await page.$('.room-tab:nth-child(3)'); if (marktplatzTab) { await marktplatzTab.click(); await new Promise(r => setTimeout(r, 1000)); - console.log('📸 08: Capturing Marktplatz (Mario chainsaws)...'); + console.log('📸 08: Capturing Marktplatz (Mario chainsaws & NAT stage)...'); await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_marktplatz.png') }); // Click Mario @@ -172,22 +211,45 @@ async function runPlaytest() { await new Promise(r => setTimeout(r, 400)); } - // Click NAT (Josef) + // Click NAT (Josef) & Test Wildschwein Duell const natBtn = await page.$('.hotspot-nat'); if (natBtn) { await natBtn.click(); await new Promise(r => setTimeout(r, 700)); console.log('📸 09b: Capturing NAT (Josef) Dialog...'); await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09b_tavern_nat_dialog.png') }); - const actionBtn = await page.$('.dialog-actions .action-btn'); - if (actionBtn) await actionBtn.click(); - await new Promise(r => setTimeout(r, 400)); + + // Start Wild Boar Duel + console.log('🐗 Launching Wildschwein-Schnick-Schnack-Schnuck Duel...'); + const startBoarBtn = await page.$('.dialog-actions .action-btn'); + if (startBoarBtn) { + await startBoarBtn.click(); + await new Promise(r => setTimeout(r, 800)); + console.log('📸 09c: Capturing Wildschwein Duel Modal...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09c_tavern_boar_duel.png') }); + + // Play a move: Keiler! + const keilerBtn = await page.$('.boar-choice-btn:nth-child(1)'); + if (keilerBtn) { + await keilerBtn.click(); + await new Promise(r => setTimeout(r, 600)); + console.log('📸 09d: Capturing Boar Duel Clash...'); + await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09d_tavern_boar_duel_round.png') }); + } + + // Close Boar Duel modal + const closeBoarBtn = await page.$('.boar-duel-box .duel-close-btn'); + if (closeBoarBtn) { + await closeBoarBtn.click(); + await new Promise(r => setTimeout(r, 400)); + } + } } } // 7. TAVERN: HINTERLAND (Mane hiking) console.log('🌲 Navigating to Hinterland...'); - const hinterlandTab = await page.$('.room-tab:nth-child(3)'); + const hinterlandTab = await page.$('.room-tab:nth-child(4)'); if (hinterlandTab) { await hinterlandTab.click(); await new Promise(r => setTimeout(r, 1000)); @@ -207,18 +269,18 @@ async function runPlaytest() { } } - // 8. TAVERN: SECRET HINTERZIMMER (VIP Rave via Secret Tap) - console.log('🗝️ Returning to Schankraum to check Secret Tap (initially invisible)...'); + // 8. TAVERN: SECRET HINTERZIMMER (VIP Rave via Luke zum Hinterraum) + console.log('🗝️ Returning to Schankraum to check Luke zum Hinterraum (trapdoor)...'); const schankraumTab = await page.$('.room-tab:nth-child(1)'); if (schankraumTab) { await schankraumTab.click(); await new Promise(r => setTimeout(r, 600)); } - console.log('🪩 Triggering Secret Tap Easter Egg...'); - const secretTap = await page.$('.secret-tap-hotspot'); - if (secretTap) { - await secretTap.click(); + console.log('🪩 Triggering Luke zum Hinterraum Easter Egg...'); + const trapdoor = await page.$('.trapdoor-hotspot'); + if (trapdoor) { + await trapdoor.click(); await new Promise(r => setTimeout(r, 1000)); console.log('📸 12: Capturing Secret VIP Hinterzimmer...'); await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '12_tavern_hinterzimmer_vip.png') }); @@ -236,25 +298,13 @@ async function runPlaytest() { } } - // 9. ZULUMAN MAGIC MIRROR & GSTANZL DUEL - console.log('🪞 Testing Zuluman Magic Mirror...'); + // 9. GSTANZL DUEL + console.log('⚔️ Testing Gstanzl-Duel and [✕] Exit Button...'); const backToGaststube = await page.$('.hotspot-door-back-vip'); if (backToGaststube) { await backToGaststube.click(); await new Promise(r => setTimeout(r, 600)); } - const mirrorBtn = await page.$('.hotspot-mirror'); - if (mirrorBtn) { - await mirrorBtn.click(); - await new Promise(r => setTimeout(r, 700)); - console.log('📸 14: Capturing Zuluman Mirror...'); - await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '14_tavern_zuluman_mirror.png') }); - const closeBtn = await page.$('.mirror-box .action-btn'); - if (closeBtn) await closeBtn.click(); - await new Promise(r => setTimeout(r, 400)); - } - - console.log('⚔️ Testing Gstanzl-Duel and [✕] Exit Button...'); const duelBtn = await page.$('.duel-trigger'); if (duelBtn) { await duelBtn.click(); diff --git a/projects/sasch40/frontend/src/components/GameBoyFrame.vue b/projects/sasch40/frontend/src/components/GameBoyFrame.vue index 215f7f1..285c126 100644 --- a/projects/sasch40/frontend/src/components/GameBoyFrame.vue +++ b/projects/sasch40/frontend/src/components/GameBoyFrame.vue @@ -45,7 +45,7 @@ function toggleInventory() {