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
@@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 606 KiB |
|
Before Width: | Height: | Size: 356 KiB After Width: | Height: | Size: 849 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 1.0 MiB |
|
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 {
|
||||
|
||||