diff --git a/projects/sasch40/AGENTS.md b/projects/sasch40/AGENTS.md index d8cfb70..00c8880 100644 --- a/projects/sasch40/AGENTS.md +++ b/projects/sasch40/AGENTS.md @@ -67,8 +67,12 @@ This document records the foundational design decisions, their rationale ("Why") ### 2e. Akt III: Raum „Hinter den Bergen“ & Liar's Dice (`TavernView.vue`) * **The Decision**: Akt III features a special mountain hideaway scene *„Hinter den Bergen“* accessible via Hinterland signpost or tab bar. * **Why**: Introduces Saschas father Papa Markus and his Taiwanese connection with Gaoliang drinking culture into the anniversary narrative. +* **Papa Markus Canonical Appearance**: + * **Head & Face**: Completely bald dome (Glatze), **strictly clean-shaven (NO BEARD, NO STUBBLE)**, dark rectangular Ray-Ban glasses, strong masculine angular jawline & nose closely resembling Sascha (see `source_photos/markus2.jpeg`). + * **Attire**: Smart-casual dark jacket and dark open-collar shirt (**STRICTLY NO LEDERHOSEN**). + * **Scene Composition**: Seated outdoors directly in front of a bustling Taiwanese street restaurant (re-chao bistro) at a wooden night-market table with Taiwanese business companions in modern suits. On the table: small shot glasses, dice cups for Liar's Dice (Chui Niu / 吹牛), and a bottle of 58% Kinmen Gaoliang liquor. + * **Atmospheric Clash**: Authentic Taiwan street eatery with cyberpunk Tokyo lighting, glowing neon kanji/hanzi signs, and red paper lanterns, surrounded by towering medieval alpine cliffs and pine trees in 1990s LucasArts SCUMM 16-bit pixel art style. * **Mechanics**: - * Papa Markus sits at a table with 6 Taiwanese friends in an alpine valley nested with Taiwanese/Tokyo skyscrapers. * Minigame **Liar's Dice (Chui Niu / 吹牛)**: 5 dice each, 1s are wild, sequential bidding, bluff-calling reveals, loser drinks a 58% Kinmen Gaoliang shot (`🍶`). * Rewards: +10 Kronkorken, inventory item *58% Kinmen Gaoliang*, Easter Egg title `taiwan_dice_champion`. @@ -169,11 +173,22 @@ This document records the foundational design decisions, their rationale ("Why") ### 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. + 1. **Gaststube (Schankraum)**: + * **Authentic Tavern Atmosphere**: Patrons and villagers sitting at rustic wooden tables drinking beer from foamy glass steins. + * **Theke & Schankanlage**: Wooden bar counter ("Theke") with shiny brass taps where a bartender serves and pours fresh beer into steins. + * **Queen Verena**: Standing on the upper wooden gallery/balcony holding a foamy beer stein. + * **Lutz (ZTUL)**: Relaxing in the steaming wooden hot pot / wash tub on the corner gallery, wearing funny party sunglasses and a feathered hat. + * **Armend**: Hammering bottle caps on his anvil right next to a blazing blacksmith forge fire with glowing coals. + * **Techno Gimmicks**: Wooden beer barrel retrofitted with a high-end subwoofer cone, glowing cyan LED ring, and golden dust cap; subtle neon cables along beams. + * **Navigation & Exits**: Sturdy wooden door to Nebenraum on the left, arched stone doorway to the sunny Marktplatz on the right, and the secret *„Luke zum Party Bunker“* in the floorboards leading down towards the bunker. 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. +* **Hotspot UI Placement & Visibility Rules**: + * **Zero Text Occlusion**: Hotspot badges must NEVER be positioned directly over hand-painted text on signs (e.g. the carved arrow text *„Hinter den Bergen“* must remain fully legible; place the button slightly above or to the side). + * **Zero Character Occlusion**: Hotspot badges must NEVER cover the face or body of another character (e.g. Hansi's button must sit high on the distant mountain path, well clear of Mane's head and backpack). + * **Bottom Margin Safety**: Room transition buttons must be placed at a safe vertical altitude (`top: <= 82%`) so they are never obscured or cropped by the bottom room-switch navigation tabs on mobile screens. * **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. diff --git a/projects/sasch40/frontend/public/assets/characters/markus/portrait.jpg b/projects/sasch40/frontend/public/assets/characters/markus/portrait.jpg index 06d6cee..db3c247 100644 Binary files a/projects/sasch40/frontend/public/assets/characters/markus/portrait.jpg and b/projects/sasch40/frontend/public/assets/characters/markus/portrait.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/hinter_den_bergen.jpg b/projects/sasch40/frontend/public/assets/scenes/hinter_den_bergen.jpg index 0a2ad99..cc20b9e 100644 Binary files a/projects/sasch40/frontend/public/assets/scenes/hinter_den_bergen.jpg and b/projects/sasch40/frontend/public/assets/scenes/hinter_den_bergen.jpg differ diff --git a/projects/sasch40/frontend/public/assets/scenes/schankraum.jpg b/projects/sasch40/frontend/public/assets/scenes/schankraum.jpg index c3d256e..de618c3 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_v5_theke_bunker.jpg b/projects/sasch40/frontend/public/assets/scenes/schankraum_v5_theke_bunker.jpg new file mode 100644 index 0000000..de618c3 Binary files /dev/null and b/projects/sasch40/frontend/public/assets/scenes/schankraum_v5_theke_bunker.jpg differ diff --git a/projects/sasch40/frontend/scripts/capture_sisters.mjs b/projects/sasch40/frontend/scripts/capture_sisters.mjs new file mode 100644 index 0000000..8be0e09 --- /dev/null +++ b/projects/sasch40/frontend/scripts/capture_sisters.mjs @@ -0,0 +1,82 @@ +import puppeteer from 'puppeteer-core'; +import path from 'path'; + +const ARTIFACTS_DIR = '/Users/moritz/.gemini/antigravity/brain/4c86ae52-822a-485e-bcc7-fed5de9e5a4b/playtest_screenshots'; + +async function sleep(ms) { + return new Promise(r => setTimeout(r, ms)); +} + +async function run() { + const browser = await puppeteer.launch({ + executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + headless: 'new', + args: ['--no-sandbox', '--window-size=412,869'] + }); + + const page = await browser.newPage(); + await page.setViewport({ width: 412, height: 869, deviceScaleFactor: 2, isMobile: true, hasTouch: true }); + await page.goto('http://localhost:4173', { waitUntil: 'networkidle0' }); + + // Advance to platformer + await page.evaluate(() => { + const store = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('game'); + store.setAct('platformer'); + }); + await sleep(800); + + // Close intro modal + const startBtn = await page.$('.start-act-btn'); + if (startBtn) await startBtn.click(); + await sleep(1000); + + // Gameplay Level 1 with fixed sky background + await page.screenshot({ path: path.join(ARTIFACTS_DIR, '10_act2_02_doodle_jump_level1.png') }); + console.log('📸 Updated 10_act2_02_doodle_jump_level1.png'); + + // Trigger Saskia Rescue + await page.evaluate(() => { + if (window.__platformer) window.__platformer.setRescuedSister('saskia'); + }); + await sleep(600); + await page.screenshot({ path: path.join(ARTIFACTS_DIR, '11_act2_03_rescue_saskia.png') }); + console.log('📸 Captured 11_act2_03_rescue_saskia.png'); + + // Click proceed button + const proceedBtn1 = await page.$('.proceed-btn'); + if (proceedBtn1) await proceedBtn1.click(); + await sleep(1000); + + // Level 2 Gameplay + await page.screenshot({ path: path.join(ARTIFACTS_DIR, '12_act2_04_doodle_jump_level2.png') }); + console.log('📸 Updated 12_act2_04_doodle_jump_level2.png'); + + // Trigger Maja Rescue + await page.evaluate(() => { + if (window.__platformer) window.__platformer.setRescuedSister('maja'); + }); + await sleep(600); + await page.screenshot({ path: path.join(ARTIFACTS_DIR, '13_act2_05_rescue_maja.png') }); + console.log('📸 Captured 13_act2_05_rescue_maja.png'); + + // Click proceed button + const proceedBtn2 = await page.$('.proceed-btn'); + if (proceedBtn2) await proceedBtn2.click(); + await sleep(1000); + + // Level 3 Gameplay + await page.screenshot({ path: path.join(ARTIFACTS_DIR, '14_act2_06_doodle_jump_level3.png') }); + console.log('📸 Updated 14_act2_06_doodle_jump_level3.png'); + + // Trigger Miriam Rescue + await page.evaluate(() => { + if (window.__platformer) window.__platformer.setRescuedSister('miriam'); + }); + await sleep(600); + await page.screenshot({ path: path.join(ARTIFACTS_DIR, '15_act2_07_rescue_miriam.png') }); + console.log('📸 Captured 15_act2_07_rescue_miriam.png'); + + await browser.close(); +} + +run(); diff --git a/projects/sasch40/frontend/scripts/record_playthrough.mjs b/projects/sasch40/frontend/scripts/record_playthrough.mjs new file mode 100644 index 0000000..b9c0d33 --- /dev/null +++ b/projects/sasch40/frontend/scripts/record_playthrough.mjs @@ -0,0 +1,683 @@ +import puppeteer from 'puppeteer-core'; +import path from 'path'; +import fs from 'fs'; + +const ARTIFACTS_DIR = '/Users/moritz/.gemini/antigravity/brain/4c86ae52-822a-485e-bcc7-fed5de9e5a4b'; +const SCREENSHOTS_DIR = path.join(ARTIFACTS_DIR, 'playtest_screenshots'); +if (!fs.existsSync(SCREENSHOTS_DIR)) { + fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true }); +} + +async function takeScreenshot(page, filename, delay = 300) { + if (delay > 0) await new Promise(r => setTimeout(r, delay)); + const fullPath = path.join(SCREENSHOTS_DIR, filename); + try { + await page.screenshot({ path: fullPath, timeout: 20000 }); + console.log(`📸 Saved: ${filename}`); + } catch (err) { + console.warn(`⚠️ Warning: Screenshot ${filename} failed:`, err.message); + } +} + +async function sleep(ms) { + return new Promise(r => setTimeout(r, ms)); +} + +async function run() { + console.log('🚀 Starting Full Playthrough & Screenshot Capture for Saschas Kronkorken Kreuzzug...'); + + const browser = await puppeteer.launch({ + executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + headless: 'new', + protocolTimeout: 60000, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--window-size=412,869' + ] + }); + + const page = await browser.newPage(); + await page.setViewport({ + width: 412, + height: 869, + deviceScaleFactor: 2, + isMobile: true, + hasTouch: true + }); + + const logs = []; + const errors = []; + const failedRequests = []; + + page.on('console', msg => { + const text = msg.text(); + logs.push(`[${msg.type().toUpperCase()}] ${text}`); + if (msg.type() === 'error') errors.push(text); + }); + page.on('pageerror', err => errors.push(err.toString())); + page.on('requestfailed', req => failedRequests.push(`${req.url()} (${req.failure()?.errorText || 'failed'})`)); + + try { + const targetUrl = 'http://localhost:4173'; + console.log(`🌐 Navigating to ${targetUrl}...`); + await page.goto(targetUrl, { waitUntil: 'networkidle0', timeout: 30000 }); + + // Clean start: clear local storage and reload + await page.evaluate(() => { + localStorage.clear(); + window.location.reload(); + }); + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + await sleep(800); + + // ========================================== + // 1. INTRO / PREQUEL SCREEN + // ========================================== + console.log('--- 1. Intro Screen ---'); + await takeScreenshot(page, '01_intro_qr_landing.png', 500); + + // Test Global Settings Modal + console.log('--- 2. Global Settings Modal ---'); + const settingsBtn = await page.$('.settings-toggle-btn'); + if (settingsBtn) { + await settingsBtn.click(); + await sleep(600); + await takeScreenshot(page, '02_settings_pause_modal.png', 300); + const resumeBtn = await page.$('.resume-btn'); + if (resumeBtn) await resumeBtn.click(); + await sleep(500); + } + + // Click Start Game + console.log('--- 3. Starting Game -> Prologue ---'); + await page.click('.start-btn'); + await sleep(1000); + + // ========================================== + // 2. PROLOGUE (BALCONY SCENE) + // ========================================== + console.log('--- Prologue: NAT Opening ---'); + await takeScreenshot(page, '03_prologue_01_nat_balcony.png', 500); + + // Click NAT response -> Flo Maier arrives + const natChoice = await page.$('.choice-btn'); + if (natChoice) await natChoice.click(); + await sleep(800); + console.log('--- Prologue: Flo Maier Feierdeifl ---'); + await takeScreenshot(page, '04_prologue_02_flo_feierdeifl.png', 400); + + // Test Flo Game Over branch: "Ja freilich, zünd ois o, Flo!" + const fireChoice = await page.$('.choice-btn:nth-child(1)'); + if (fireChoice) await fireChoice.click(); + await sleep(800); + console.log('--- Prologue: Flo Castle Inferno Game Over ---'); + await takeScreenshot(page, '05_prologue_03_flo_game_over.png', 400); + + // Reset via Löschzug + const resetFireBtn = await page.$('.game-over-btn'); + if (resetFireBtn) await resetFireBtn.click(); + await sleep(1000); + + // Skip NAT to Flo + await page.waitForSelector('.choice-btn', { timeout: 4000 }); + const natChoice2 = await page.$('.choice-btn'); + if (natChoice2) await natChoice2.click(); + await sleep(800); + + // Decline Flo -> Viktor arrives + const declineFloBtn = await page.$('.choice-btn:nth-child(2)'); + if (declineFloBtn) await declineFloBtn.click(); + await sleep(800); + console.log('--- Prologue: Viktor Partyvampir ---'); + await takeScreenshot(page, '06_prologue_04_viktor_partyvampir.png', 400); + + // Test Viktor Game Over branch: "Ja geil, mach mi unsterblich!" + const biteChoice = await page.$('.choice-btn:nth-child(1)'); + if (biteChoice) await biteChoice.click(); + await sleep(800); + console.log('--- Prologue: Viktor Glitter Dust Game Over ---'); + await takeScreenshot(page, '07_prologue_05_viktor_game_over.png', 400); + + // Reset via Knoblauch-Kur + const resetVampBtn = await page.$('.game-over-btn'); + if (resetVampBtn) await resetVampBtn.click(); + await sleep(1000); + + // Advance safely to Mop: NAT -> decline Flo -> decline Viktor + await page.waitForSelector('.choice-btn', { timeout: 4000 }); + const nc = await page.$('.choice-btn'); + if (nc) await nc.click(); + await sleep(600); + + const fc = await page.$('.choice-btn:nth-child(2)'); + if (fc) await fc.click(); + await sleep(600); + + const vc = await page.$('.choice-btn:nth-child(2)'); + if (vc) await vc.click(); + await sleep(800); + + console.log('--- Prologue: Mop Sister Rescue Quest ---'); + await takeScreenshot(page, '08_prologue_06_mop_quest.png', 400); + + // Click to advance to Akt II (Doodle Jump Schwesternrettung) + const startAkt2Btn = await page.$('.choice-btn'); + if (startAkt2Btn) await startAkt2Btn.click(); + await sleep(1000); + + // ========================================== + // 3. AKT II: DOODLE JUMP SCHWESTERNRETTUNG + // ========================================== + console.log('--- Akt II: Intro Modal ---'); + await takeScreenshot(page, '09_act2_01_doodle_jump_intro.png', 500); + + // Click Start Act Button on modal + const startDoodleBtn = await page.$('.start-act-btn'); + if (startDoodleBtn) await startDoodleBtn.click(); + await sleep(1200); + + console.log('--- Akt II: Level 1 Gameplay (Saskia) ---'); + await takeScreenshot(page, '10_act2_02_doodle_jump_level1.png', 400); + + // Trigger Sister 1 Rescue (Saskia) + await page.evaluate(() => { + const el = document.querySelector('.platformer-viewport'); + const exposed = el?.__vueParentComponent?.exposed; + if (exposed?.triggerSisterRescue) { + exposed.triggerSisterRescue('saskia'); + } else { + // Fallback: search components in tree + const app = document.querySelector('#app')?.__vue_app__; + const findComp = (inst) => { + if (!inst) return null; + if (inst.exposed?.triggerSisterRescue) return inst.exposed; + if (inst.ctx?.triggerSisterRescue) return inst.ctx; + if (inst.subTree?.children) { + for (const c of inst.subTree.children) { + const res = findComp(c?.component); + if (res) return res; + } + } + return null; + }; + const target = findComp(app?._instance); + if (target) target.triggerSisterRescue('saskia'); + } + }); + await sleep(800); + + const rescueOverlay1 = await page.$('.rescue-overlay'); + if (rescueOverlay1) { + console.log('--- Akt II: Rescued Saskia Modal ---'); + await takeScreenshot(page, '11_act2_03_rescue_saskia.png', 400); + const proceedBtn1 = await page.$('.proceed-btn'); + if (proceedBtn1) await proceedBtn1.click(); + await sleep(1000); + } + + console.log('--- Akt II: Level 2 Gameplay (Maja) ---'); + await takeScreenshot(page, '12_act2_04_doodle_jump_level2.png', 400); + + // Trigger Sister 2 Rescue (Maja) + await page.evaluate(() => { + const el = document.querySelector('.platformer-viewport'); + const exposed = el?.__vueParentComponent?.exposed; + if (exposed?.triggerSisterRescue) { + exposed.triggerSisterRescue('maja'); + } else { + const app = document.querySelector('#app')?.__vue_app__; + const findComp = (inst) => { + if (!inst) return null; + if (inst.exposed?.triggerSisterRescue) return inst.exposed; + if (inst.ctx?.triggerSisterRescue) return inst.ctx; + if (inst.subTree?.children) { + for (const c of inst.subTree.children) { + const res = findComp(c?.component); + if (res) return res; + } + } + return null; + }; + const target = findComp(app?._instance); + if (target) target.triggerSisterRescue('maja'); + } + }); + await sleep(800); + + const rescueOverlay2 = await page.$('.rescue-overlay'); + if (rescueOverlay2) { + console.log('--- Akt II: Rescued Maja Modal ---'); + await takeScreenshot(page, '13_act2_05_rescue_maja.png', 400); + const proceedBtn2 = await page.$('.proceed-btn'); + if (proceedBtn2) await proceedBtn2.click(); + await sleep(1000); + } + + console.log('--- Akt II: Level 3 Gameplay (Miriam) ---'); + await takeScreenshot(page, '14_act2_06_doodle_jump_level3.png', 400); + + // Trigger Sister 3 Rescue (Miriam) + await page.evaluate(() => { + const el = document.querySelector('.platformer-viewport'); + const exposed = el?.__vueParentComponent?.exposed; + if (exposed?.triggerSisterRescue) { + exposed.triggerSisterRescue('miriam'); + } else { + const app = document.querySelector('#app')?.__vue_app__; + const findComp = (inst) => { + if (!inst) return null; + if (inst.exposed?.triggerSisterRescue) return inst.exposed; + if (inst.ctx?.triggerSisterRescue) return inst.ctx; + if (inst.subTree?.children) { + for (const c of inst.subTree.children) { + const res = findComp(c?.component); + if (res) return res; + } + } + return null; + }; + const target = findComp(app?._instance); + if (target) target.triggerSisterRescue('miriam'); + } + }); + await sleep(800); + + const rescueOverlay3 = await page.$('.rescue-overlay'); + if (rescueOverlay3) { + console.log('--- Akt II: Rescued Miriam Modal ---'); + await takeScreenshot(page, '15_act2_07_rescue_miriam.png', 400); + const proceedBtn3 = await page.$('.proceed-btn'); + if (proceedBtn3) await proceedBtn3.click(); + await sleep(1200); + } + + // ========================================== + // 4. AKT III: TAVERNE & VILLAGE HUB + // ========================================== + console.log('--- Akt III: Tavern Room 1 - Schankraum ---'); + // Ensure we are in tavern act + await page.evaluate(() => { + const app = document.querySelector('#app')?.__vue_app__; + const store = app?.config?.globalProperties?.$pinia?._s.get('game'); + if (store) store.setAct('tavern'); + }); + await sleep(1000); + + await takeScreenshot(page, '16_act3_01_tavern_schankraum.png', 600); + + // Verena Dialogue + console.log('--- Schankraum: Verena Dialog ---'); + const verenaBtn = await page.$('.hotspot-verena'); + if (verenaBtn) { + await verenaBtn.click(); + await sleep(600); + await takeScreenshot(page, '17_act3_02_verena_dialog.png', 300); + const closeBtn = await page.$('.dialog-actions .action-btn'); + if (closeBtn) await closeBtn.click(); + await sleep(400); + } + + // Armend Dialogue & Beer Tap + console.log('--- Schankraum: Armend Dialog ---'); + const armendBtn = await page.$('.hotspot-armend'); + if (armendBtn) { + await armendBtn.click(); + await sleep(600); + await takeScreenshot(page, '18_act3_03_armend_dialog.png', 300); + const closeBtn = await page.$('.dialog-actions .action-btn'); + if (closeBtn) await closeBtn.click(); + await sleep(400); + } + + // Lutz Dialogue + console.log('--- Schankraum: Lutz Dialog ---'); + const lutzBtn = await page.$('.hotspot-lutz'); + if (lutzBtn) { + await lutzBtn.click(); + await sleep(600); + await takeScreenshot(page, '19_act3_04_lutz_dialog.png', 300); + const closeBtn = await page.$('.dialog-actions .action-btn'); + if (closeBtn) await closeBtn.click(); + await sleep(400); + } + + // Room 2: Nebenraum (Kemenate / Atelier) + console.log('--- Akt III: Room 2 - Nebenraum ---'); + const nrTab = await page.$('.room-tab:nth-child(2)'); + if (nrTab) await nrTab.click(); + await sleep(800); + await takeScreenshot(page, '20_act3_05_nebenraum_overview.png', 500); + + // Amy Dialogue + console.log('--- Nebenraum: Amy Dialog ---'); + const amyBtn = await page.$('.hotspot-amy-nebenraum'); + if (amyBtn) { + await amyBtn.click(); + await sleep(600); + await takeScreenshot(page, '21_act3_06_amy_dialog.png', 300); + const closeBtn = await page.$('.dialog-actions .action-btn'); + if (closeBtn) await closeBtn.click(); + await sleep(400); + } + + // Zuluman Magic Mirror + console.log('--- Nebenraum: Zuluman Mirror ---'); + const mirrorBtn = await page.$('.hotspot-mirror-nebenraum'); + if (mirrorBtn) { + await mirrorBtn.click(); + await sleep(600); + await takeScreenshot(page, '22_act3_07_zuluman_mirror.png', 300); + const closeBtn = await page.$('.mirror-box .action-btn'); + if (closeBtn) await closeBtn.click(); + await sleep(400); + } + + // Room 3: Marktplatz + console.log('--- Akt III: Room 3 - Marktplatz ---'); + const marktplatzTab = await page.$('.room-tab:nth-child(3)'); + if (marktplatzTab) await marktplatzTab.click(); + await sleep(800); + await takeScreenshot(page, '23_act3_08_marktplatz_overview.png', 500); + + // Mario Chainsaw Juggling Dialog + console.log('--- Marktplatz: Mario Dialog ---'); + const marioBtn = await page.$('.hotspot-mario'); + if (marioBtn) { + await marioBtn.click(); + await sleep(600); + await takeScreenshot(page, '24_act3_09_mario_dialog.png', 300); + const closeBtn = await page.$('.dialog-actions .action-btn'); + if (closeBtn) await closeBtn.click(); + await sleep(400); + } + + // NAT Stage Dialog + console.log('--- Marktplatz: NAT Dialog ---'); + const natTavernBtn = await page.$('.hotspot-nat'); + if (natTavernBtn) { + await natTavernBtn.click(); + await sleep(600); + await takeScreenshot(page, '25_act3_10_nat_dialog.png', 300); + + // Start Wildschwein Duel Intro + const startBoarIntroBtn = await page.$('.dialog-actions .action-btn'); + if (startBoarIntroBtn) { + await startBoarIntroBtn.click(); + await sleep(700); + console.log('--- Marktplatz: Wildschwein Duel Intro ---'); + await takeScreenshot(page, '26_act3_11_wildschwein_duel_intro.png', 300); + + // Start Duel + const startDuelActionBtn = await page.$('.start-duel-action-btn'); + if (startDuelActionBtn) { + await startDuelActionBtn.click(); + await sleep(600); + + // Click Keiler + const keilerBtn = await page.$('.boar-choice-btn:nth-child(1)'); + if (keilerBtn) await keilerBtn.click(); + await sleep(700); + console.log('--- Marktplatz: Wildschwein Duel Round Clash ---'); + await takeScreenshot(page, '27_act3_12_wildschwein_duel_round.png', 300); + + // Close duel + const closeDuelBtn = await page.$('.boar-duel-box .duel-close-btn'); + if (closeDuelBtn) await closeDuelBtn.click(); + await sleep(400); + } + } + } + + // Room 4: Alpines Hinterland + console.log('--- Akt III: Room 4 - Alpines Hinterland ---'); + const hinterlandTab = await page.$('.room-tab:nth-child(4)'); + if (hinterlandTab) await hinterlandTab.click(); + await sleep(800); + await takeScreenshot(page, '28_act3_13_hinterland_overview.png', 500); + + // Mane Dialog + console.log('--- Hinterland: Mane Dialog ---'); + const maneBtn = await page.$('.hotspot-mane'); + if (maneBtn) { + await maneBtn.click(); + await sleep(600); + await takeScreenshot(page, '29_act3_14_mane_dialog.png', 300); + const closeBtn = await page.$('.dialog-actions .action-btn'); + if (closeBtn) await closeBtn.click(); + await sleep(400); + } + + // Hansi Dialog + console.log('--- Hinterland: Hansi Dialog ---'); + const hansiBtn = await page.$('.hotspot-hansi'); + if (hansiBtn) { + await hansiBtn.click(); + await sleep(600); + await takeScreenshot(page, '30_act3_15_hansi_dialog.png', 300); + const closeBtn = await page.$('.dialog-actions .action-btn'); + if (closeBtn) await closeBtn.click(); + await sleep(400); + } + + // Room 5: Hinter den Bergen (Taiwan Valley & Papa Markus) + console.log('--- Akt III: Room 5 - Hinter den Bergen ---'); + const hdbTab = await page.$('.room-tab:nth-child(5)'); + if (hdbTab) await hdbTab.click(); + await sleep(1000); + await takeScreenshot(page, '31_act3_16_hinter_den_bergen_overview.png', 500); + + // Papa Markus Dialog + console.log('--- Hinter den Bergen: Papa Markus Dialog ---'); + const markusBtn = await page.$('.hotspot-markus'); + if (markusBtn) { + await markusBtn.click(); + await sleep(600); + await takeScreenshot(page, '32_act3_17_markus_dialog.png', 300); + + // Start Liar's Dice Intro + const startDiceIntroBtn = await page.$('.dialog-actions .action-btn'); + if (startDiceIntroBtn) { + await startDiceIntroBtn.click(); + await sleep(700); + console.log('--- Hinter den Bergen: Liar\'s Dice Intro ---'); + await takeScreenshot(page, '33_act3_18_liars_dice_intro.png', 300); + + // Start Liar's Dice Match + const startDiceMatchBtn = await page.$('.start-duel-action-btn'); + if (startDiceMatchBtn) { + await startDiceMatchBtn.click(); + await sleep(1000); + console.log('--- Hinter den Bergen: Liar\'s Dice Gameplay ---'); + await takeScreenshot(page, '34_act3_19_liars_dice_gameplay.png', 300); + + // Place Bid + const bidBtn = await page.$('.bid-action-btn'); + if (bidBtn) { + await bidBtn.click(); + await sleep(1500); // wait for Papa Markus counter-bid + } + + // Call Bluff + const bluffBtn = await page.$('.call-bluff-btn'); + if (bluffBtn) { + await bluffBtn.click(); + await sleep(1200); // shot animation + console.log('--- Hinter den Bergen: Liar\'s Dice Bluff & Shot ---'); + await takeScreenshot(page, '35_act3_20_liars_dice_bluff_shot.png', 300); + } + + // Close Liar's Dice + const closeDiceBtn = await page.$('.liars-dice-box .duel-close-btn'); + if (closeDiceBtn) await closeDiceBtn.click(); + await sleep(400); + } + } + } + + // Back to Schankraum: Test Secret Floor Trapdoor (Double-Click) + console.log('--- Schankraum: Double-Click Trapdoor to Party Bunker ---'); + const schankraumTab = await page.$('.room-tab:nth-child(1)'); + if (schankraumTab) await schankraumTab.click(); + await sleep(800); + + const trapdoor = await page.$('.trapdoor-hotspot'); + if (trapdoor) { + await trapdoor.click(); + await sleep(150); + await trapdoor.click(); + await sleep(1000); + + // Captured Party Bunker + console.log('--- VIP Party Bunker Overview ---'); + await takeScreenshot(page, '37_act3_22_party_bunker_overview.png', 600); + + // Pü Dialog + const pueBtn = await page.$('.hotspot-pue'); + if (pueBtn) { + await pueBtn.click(); + await sleep(600); + console.log('--- Party Bunker: Pü Dialog ---'); + await takeScreenshot(page, '38_act3_23_pue_dialog.png', 300); + const closeBtn = await page.$('.dialog-actions .action-btn'); + if (closeBtn) await closeBtn.click(); + await sleep(400); + } + + // Return to Schankraum to see revealed trapdoor + const backDoor = await page.$('.hotspot-door-back-vip'); + if (backDoor) { + await backDoor.click(); + await sleep(800); + console.log('--- Schankraum: Trapdoor Revealed ---'); + await takeScreenshot(page, '36_act3_21_trapdoor_revealed.png', 400); + } + } + + // Minigame: Bayerisches Gstanzl-Fechten + console.log('--- Tavern: Bayerisches Gstanzl-Fechten ---'); + const gstanzlTrigger = await page.$('.duel-trigger'); + if (gstanzlTrigger) { + await gstanzlTrigger.click(); + await sleep(600); + console.log('--- Gstanzl: Rules Intro ---'); + await takeScreenshot(page, '39_act3_24_gstanzl_intro.png', 300); + + const startGstanzlBtn = await page.$('.start-duel-action-btn'); + if (startGstanzlBtn) { + await startGstanzlBtn.click(); + await sleep(700); + console.log('--- Gstanzl: Duel Battle Gameplay ---'); + await takeScreenshot(page, '40_act3_25_gstanzl_battle.png', 300); + + const closeGstanzl = await page.$('.duel-box .duel-close-btn'); + if (closeGstanzl) await closeGstanzl.click(); + await sleep(400); + } + } + + // ========================================== + // 5. AKT IV: MITTELALTER GUITAR HERO + // ========================================== + console.log('--- Akt IV: Castle Stage Rock Concert ---'); + await page.evaluate(() => { + const app = document.querySelector('#app')?.__vue_app__; + const store = app?.config?.globalProperties?.$pinia?._s.get('game'); + if (store) store.setAct('party_express'); + }); + await sleep(1000); + + console.log('--- Akt IV: Concert Intro Modal ---'); + await takeScreenshot(page, '41_act4_01_lute_hero_intro.png', 500); + + const startConcertBtn = await page.$('.start-act-btn'); + if (startConcertBtn) await startConcertBtn.click(); + await sleep(1500); + + // Hit a few notes on the strings + const lane0 = await page.$('.btn-lane-0'); + const lane1 = await page.$('.btn-lane-1'); + const lane2 = await page.$('.btn-lane-2'); + if (lane0) await lane0.click(); + await sleep(150); + if (lane1) await lane1.click(); + await sleep(150); + if (lane2) await lane2.click(); + await sleep(250); + + console.log('--- Akt IV: Concert Gameplay ---'); + await takeScreenshot(page, '42_act4_02_lute_hero_gameplay.png', 400); + + // Trigger Concert Finish + await page.evaluate(() => { + const app = document.querySelector('#app')?.__vue_app__; + const findComp = (inst) => { + if (!inst) return null; + if (inst.finishConcert) return inst; + if (inst.subTree?.children) { + for (const c of inst.subTree.children) { + const res = findComp(c?.component?.ctx); + if (res) return res; + } + } + return null; + }; + const target = findComp(app?._instance); + if (target) target.finishConcert(); + }); + await sleep(800); + + console.log('--- Akt IV: Concert Victory Screen ---'); + await takeScreenshot(page, '43_act4_03_lute_hero_victory.png', 400); + + // ========================================== + // 6. AKT V: FINALE & ROYAL CERTIFICATE + // ========================================== + console.log('--- Akt V: Royal Certificate Screen ---'); + await page.evaluate(() => { + const app = document.querySelector('#app')?.__vue_app__; + const store = app?.config?.globalProperties?.$pinia?._s.get('game'); + if (store) { + store.addKronkorken(35); + store.serveGuest(); + store.serveGuest(); + store.setGstanzlWon(); + store.unlockEasterEgg('hinterzimmer'); + store.unlockEasterEgg('zuluman_mirror'); + store.setAct('finale'); + } + }); + await sleep(1500); + + await takeScreenshot(page, '44_act5_01_royal_certificate.png', 800); + + // Check for broken images or console errors + const brokenImages = await page.evaluate(() => { + const imgs = Array.from(document.querySelectorAll('img')); + return imgs + .filter(img => !img.complete || img.naturalWidth === 0) + .map(img => ({ src: img.src, alt: img.alt })); + }); + + console.log('\n=========================================='); + console.log('🎉 PLAYTHROUGH COMPLETE!'); + console.log(`📸 Total Screenshots Captured: ${fs.readdirSync(SCREENSHOTS_DIR).length}`); + console.log(`⚠️ Console Errors: ${errors.length}`); + if (errors.length > 0) console.log(errors); + console.log(`❌ Failed Requests: ${failedRequests.length}`); + if (failedRequests.length > 0) console.log(failedRequests); + console.log(`🖼️ Broken Images: ${brokenImages.length}`); + if (brokenImages.length > 0) console.log(brokenImages); + console.log('==========================================\n'); + + } catch (err) { + console.error('Fatal error during playthrough:', err); + await takeScreenshot(page, '99_fatal_error.png', 100); + } finally { + await browser.close(); + console.log('🏁 Browser closed.'); + } +} + +run(); diff --git a/projects/sasch40/frontend/src/components/PlatformerView.vue b/projects/sasch40/frontend/src/components/PlatformerView.vue index 9400992..0f4c1e5 100644 --- a/projects/sasch40/frontend/src/components/PlatformerView.vue +++ b/projects/sasch40/frontend/src/components/PlatformerView.vue @@ -166,7 +166,8 @@ function onRight(pressed: boolean) { defineExpose({ onJump, onLeft, - onRight + onRight, + triggerSisterRescue }) // Touch controls on screen halves @@ -357,9 +358,11 @@ function drawGame(ctx: CanvasRenderingContext2D) { // 1. Background sky parallax if (bgImg && bgImg.complete && bgImg.naturalWidth > 0) { - const scrollFactor = (cameraY / (levelHeight - H)) - const bgY = -scrollFactor * (bgImg.naturalHeight - H) - ctx.drawImage(bgImg, 0, bgY, W, bgImg.naturalHeight * (W / bgImg.naturalWidth)) + const drawnH = bgImg.naturalHeight * (W / bgImg.naturalWidth) + const maxScroll = Math.max(0, drawnH - H) + const scrollFactor = Math.max(0, Math.min(1, cameraY / (levelHeight - H))) + const bgY = -scrollFactor * maxScroll + ctx.drawImage(bgImg, 0, bgY, W, drawnH) } else { // Fallback sky gradient const skyGrad = ctx.createLinearGradient(0, 0, 0, H) @@ -603,6 +606,11 @@ function gameLoop() { onMounted(() => { window.addEventListener('keydown', handleKeyDown) window.addEventListener('keyup', handleKeyUp) + ;(window as any).__platformer = { + triggerSisterRescue, + setRescuedSister: (s: any) => { rescuedSister.value = s }, + startLevel + } initCurrentLevel() animationFrameId = requestAnimationFrame(gameLoop) }) @@ -610,6 +618,7 @@ onMounted(() => { onUnmounted(() => { window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keyup', handleKeyUp) + delete (window as any).__platformer if (animationFrameId) cancelAnimationFrame(animationFrameId) }) diff --git a/projects/sasch40/frontend/src/components/TavernView.vue b/projects/sasch40/frontend/src/components/TavernView.vue index c21c7b8..9fc0bdb 100644 --- a/projects/sasch40/frontend/src/components/TavernView.vue +++ b/projects/sasch40/frontend/src/components/TavernView.vue @@ -1270,8 +1270,8 @@ function proceedToExpress() { /* Trapdoor to Party Bunker Easter Egg */ .trapdoor-hotspot { position: absolute; - top: 76%; - left: 32%; + top: 86%; + left: 50%; transform: translate(-50%, -50%); background: rgba(212, 175, 55, 0.25); border: 2px dashed #ffd700; @@ -1313,11 +1313,11 @@ function proceedToExpress() { } /* 1. Schankraum Hotspots */ -.hotspot-verena { top: 44%; left: 10%; } -.hotspot-door-nebenraum { top: 36%; left: 54%; } -.hotspot-armend { top: 58%; left: 22%; } -.hotspot-lutz { top: 48%; left: 38%; } -.hotspot-door-marktplatz { top: 90%; left: 18%; } +.hotspot-verena { top: 16%; left: 36%; } +.hotspot-door-nebenraum { top: 64%; left: 14%; } +.hotspot-armend { top: 62%; left: 84%; } +.hotspot-lutz { top: 22%; left: 83%; } +.hotspot-door-marktplatz { top: 44%; left: 78%; } /* 2. Nebenraum Hotspots */ .hotspot-mirror-nebenraum { top: 36%; left: 24%; } @@ -1402,13 +1402,13 @@ function proceedToExpress() { transform: rotate(20deg); } -.hotspot-hansi { top: 48%; left: 66%; } -.hotspot-door-hinter-den-bergen { top: 38%; left: 20%; } +.hotspot-hansi { top: 30%; left: 64%; } +.hotspot-door-hinter-den-bergen { top: 34%; left: 24%; } .hotspot-door-back-marktplatz { top: 80%; left: 16%; } /* 5. Hinter den Bergen Hotspots */ -.hotspot-markus { top: 68%; left: 50%; } -.hotspot-door-back-hinterland { top: 88%; left: 16%; } +.hotspot-markus { top: 70%; left: 52%; } +.hotspot-door-back-hinterland { top: 90%; left: 16%; } /* 6. Hinterzimmer Hotspots & Strobe */ .strobe-overlay {