feat(sasch40): polish retro game layout, clean tavern scene, and improve mobile HUD

- Hide GameBoy D-Pad and action buttons during RPG and dialogue modes
- Add prequel intro landing screen for QR code arrivals with Haumdaucher lore
- Clean baked-in text from tavern scene background and streamline navigation bar
- Add safe-zone margins and HTML HUD in platformer and party express acts
- Optimize Certificate modal layout and scoring breakdown for mobile screens
- Add automated end-to-end browser playtest script with Puppeteer
- Accelerate Docker build via native pre-compilation
This commit is contained in:
Moritz Graf
2026-09-24 07:55:42 +02:00
parent 5852479c21
commit f6d39f01da
13 changed files with 998 additions and 202 deletions
@@ -0,0 +1,288 @@
import puppeteer from 'puppeteer-core';
import path from 'path';
import fs from 'fs';
const ARTIFACTS_DIR = '/Users/moritz/.gemini/antigravity/brain/26049ae8-3106-42e0-922f-356dbbfe162a';
const SCREENSHOTS_DIR = path.join(ARTIFACTS_DIR, 'playtest_screenshots');
if (!fs.existsSync(SCREENSHOTS_DIR)) {
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
}
async function runPlaytest() {
console.log('🚀 Starting Chrome Playtest for Saschas Kronkorken Kreuzzug...');
const logs = [];
const errors = [];
const failedRequests = [];
const browser = await puppeteer.launch({
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--window-size=412,869'
]
});
const page = await browser.newPage();
await page.setViewport({
width: 412,
height: 869,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true
});
page.on('console', msg => {
const text = msg.text();
logs.push(`[${msg.type().toUpperCase()}] ${text}`);
if (msg.type() === 'error') {
errors.push(`Console Error: ${text}`);
}
});
page.on('pageerror', err => {
errors.push(`Page Error: ${err.toString()}`);
});
page.on('requestfailed', req => {
failedRequests.push(`Failed Request: ${req.url()} (${req.failure()?.errorText || 'unknown'})`);
});
page.on('response', resp => {
if (resp.status() >= 400) {
failedRequests.push(`HTTP ${resp.status()}: ${resp.url()}`);
}
});
try {
console.log('🌐 Navigating to https://sasch40.haumdaucher.de...');
await page.goto('https://sasch40.haumdaucher.de', { waitUntil: 'networkidle0', timeout: 30000 });
// 1. INTRO SCREEN
console.log('📸 Capturing Intro Screen...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '01_intro_screen.png') });
// Inspect layout and controls presence in Intro
const introControlsVisible = await page.evaluate(() => {
const dpad = document.querySelector('.dpad');
const intro = document.querySelector('.intro-screen');
const chassis = document.querySelector('.gameboy-chassis');
return {
hasDpad: !!dpad,
dpadOffsetHeight: dpad ? (dpad.offsetHeight > 0) : false,
hasIntro: !!intro,
layoutClass: chassis ? chassis.className : ''
};
});
console.log('Intro Layout Check:', introControlsVisible);
// Click Start Button
console.log('🖱️ Clicking Start Button...');
await page.click('.start-btn');
await new Promise(r => setTimeout(r, 1000));
// 2. PROLOGUE SCREEN
console.log('📸 Capturing Prologue Screen...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02_prologue_screen.png') });
// Advance Prologue Dialogues
console.log('💬 Advancing Prologue Dialogues (clicking choice buttons)...');
// Click choice 1 on 'start'
await page.waitForSelector('.choice-btn', { timeout: 5000 });
const firstChoice = await page.$('.choice-btn');
if (firstChoice) {
await firstChoice.click();
await new Promise(r => setTimeout(r, 800));
console.log('📸 Capturing Mop Appears Prologue Dialogue...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02b_prologue_mop.png') });
}
// Click choice on 'mop_appears' -> triggers 'startPlatformer'
const mopChoice = await page.$('.choice-btn');
if (mopChoice) {
await mopChoice.click();
await new Promise(r => setTimeout(r, 1200));
}
// 3. PLATFORMER SCREEN
console.log('📸 Capturing Platformer (Jump & Run)...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03_platformer_screen.png') });
const platformerControls = await page.evaluate(() => {
const dpad = document.querySelector('.dpad');
const btnA = document.querySelector('.btn-a');
const canvas = document.querySelector('canvas');
return {
hasDpad: !!dpad,
hasBtnA: !!btnA,
hasCanvas: !!canvas,
canvasW: canvas?.width,
canvasH: canvas?.height
};
});
console.log('Platformer Controls Check:', platformerControls);
// Test Jump & Move
console.log('🎮 Testing Jump & Move Controls in Platformer...');
const btnA = await page.$('.btn-a');
if (btnA) {
await btnA.click();
await new Promise(r => setTimeout(r, 400));
await btnA.click();
await new Promise(r => setTimeout(r, 400));
}
const dpadRight = await page.$('.dpad-right');
if (dpadRight) {
await dpadRight.hover();
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03b_platformer_jump.png') });
}
// Directly set act to 'tavern' to test Tavern Point & Click
console.log('🍺 Advancing to Tavern Act...');
await page.evaluate(() => {
const chassis = document.querySelector('.gameboy-chassis');
// access pinia store from window if exposed, or trigger level finish
// We can click the level finish or evaluate store change
window.dispatchEvent(new CustomEvent('test-set-act', { detail: 'tavern' }));
});
// Check if act switched, or use page.evaluate to trigger store change
await page.evaluate(() => {
// Find Pinia root if possible
const app = document.querySelector('#app')?.__vue_app__;
if (app && app.config?.globalProperties?.$pinia) {
const pinia = app.config.globalProperties.$pinia;
// find game store
for (const [id, store] of pinia._s.entries()) {
if (id === 'game') {
store.setAct('tavern');
}
}
}
});
await new Promise(r => setTimeout(r, 1000));
console.log('📸 Capturing Tavern Screen...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '04_tavern_screen.png') });
// Test Tavern Hotspots
console.log('👥 Testing Verena Hotspot...');
const verenaBtn = await page.$('.hotspot-verena');
if (verenaBtn) {
await verenaBtn.click();
await new Promise(r => setTimeout(r, 800));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05_tavern_verena_dialog.png') });
const actionBtn = await page.$('.dialog-actions .action-btn');
if (actionBtn) await actionBtn.click();
await new Promise(r => setTimeout(r, 500));
}
console.log('🔨 Testing Armend Hotspot...');
const armendBtn = await page.$('.hotspot-armend');
if (armendBtn) {
await armendBtn.click();
await new Promise(r => setTimeout(r, 800));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '06_tavern_armend_dialog.png') });
const actionBtn = await page.$('.dialog-actions .action-btn');
if (actionBtn) await actionBtn.click();
await new Promise(r => setTimeout(r, 500));
}
console.log('🪞 Testing Zuluman Magic Mirror Hotspot...');
const mirrorBtn = await page.$('.hotspot-mirror');
if (mirrorBtn) {
await mirrorBtn.click();
await new Promise(r => setTimeout(r, 800));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '07_tavern_zuluman_mirror.png') });
const closeBtn = await page.$('.mirror-box .action-btn');
if (closeBtn) await closeBtn.click();
await new Promise(r => setTimeout(r, 500));
}
console.log('⚔️ Testing Gstanzl-Duel...');
const duelBtn = await page.$('.duel-trigger');
if (duelBtn) {
await duelBtn.click();
await new Promise(r => setTimeout(r, 800));
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_gstanzl_duel.png') });
// Answer Gstanzl question
const correctOption = await page.$('.option-btn');
if (correctOption) {
await correctOption.click();
await new Promise(r => setTimeout(r, 600));
}
}
// 4. PARTY EXPRESS (Act IV)
console.log('🚂 Advancing to Party Express (Act IV)...');
await page.evaluate(() => {
const app = document.querySelector('#app')?.__vue_app__;
if (app && app.config?.globalProperties?.$pinia) {
const pinia = app.config.globalProperties.$pinia;
for (const [id, store] of pinia._s.entries()) {
if (id === 'game') store.setAct('party_express');
}
}
});
await new Promise(r => setTimeout(r, 1000));
console.log('📸 Capturing Party Express...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09_party_express.png') });
// 5. FINALE / CERTIFICATE (Act V)
console.log('👑 Advancing to Finale Certificate (Act V)...');
await page.evaluate(() => {
const app = document.querySelector('#app')?.__vue_app__;
if (app && app.config?.globalProperties?.$pinia) {
const pinia = app.config.globalProperties.$pinia;
for (const [id, store] of pinia._s.entries()) {
if (id === 'game') {
store.addKronkorken(25);
store.serveGuest();
store.serveGuest();
store.setAct('finale');
}
}
}
});
await new Promise(r => setTimeout(r, 1500));
console.log('📸 Capturing Certificate Modal...');
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '10_certificate_screen.png') });
// Check images in document
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--- PLAYTEST REPORT ---');
console.log('Total Console Logs:', logs.length);
console.log('Console Errors:', errors);
console.log('Failed Requests:', failedRequests);
console.log('Broken Images on Certificate Page:', brokenImages);
const report = {
timestamp: new Date().toISOString(),
errors,
failedRequests,
brokenImages,
screenshots: fs.readdirSync(SCREENSHOTS_DIR)
};
fs.writeFileSync(path.join(ARTIFACTS_DIR, 'playtest_report.json'), JSON.stringify(report, null, 2));
} catch (err) {
console.error('Playtest error:', err);
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, 'error_state.png') }).catch(() => {});
} finally {
await browser.close();
console.log('🏁 Playtest browser closed.');
}
}
runPlaytest();