feat(sasch40): overhaul Act III tavern schankraum and canonical papa markus scene

- Generate and integrate authentic 16-bit LucasArts taproom scene with Theke, brass taps, bartender, drinking patrons, and bunker trapdoor
- Realign Schankraum hotspots for Verena, Lutz in hot tub, Armend at forge, room transitions, and centered bunker trapdoor
- Update Papa Markus canonical portrait and Taiwan street dining scene to be bald and clean-shaven with glasses and modern attire
- Enforce hotspot zero-occlusion rules for signposts, characters, and mobile room navigation bars
- Document Papa Markus specifications, tavern invariants, and layout guidelines in AGENTS.md
- Add automated sister capture and playthrough recording scripts
This commit is contained in:
Moritz Graf
2026-09-25 08:36:39 +02:00
parent b61cf84082
commit 07fef4547c
9 changed files with 806 additions and 17 deletions
+17 -2
View File
@@ -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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

After

Width:  |  Height:  |  Size: 606 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

After

Width:  |  Height:  |  Size: 849 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

@@ -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();
@@ -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();
@@ -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)
})
</script>
@@ -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 {