- Fix character portrait crops and version assets to portrait_v2, ensuring correct identities (resolving Nat/Verena mismatch) - Update Schankraum scene image and configure Nginx cache-control to prevent stale asset caching - Make secret beer tap completely invisible in tavern until clicked - Reposition room hotspots in Act III to eliminate overlapping click targets - Add exit button [✕] to Bavarian Gstanzl duel minigame - Implement localStorage persistence in gameStore to maintain progress across page reloads - Enable clean game state and storage reset when clicking "Nochmal spielen" - Expand automated playtest script to cover Nat dialogue, duel exit, reload persistence, and game restart
391 lines
15 KiB
JavaScript
391 lines
15 KiB
JavaScript
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 Comprehensive 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: 35000 });
|
|
|
|
// 1. INTRO / PREQUEL SCREEN
|
|
console.log('📸 01: Capturing Intro Screen...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '01_intro_screen.png') });
|
|
|
|
// Click Start Button
|
|
console.log('🖱️ Clicking Start Button...');
|
|
await page.click('.start-btn');
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
|
|
// 2. PROLOGUE SCREEN (Balcony Scene)
|
|
console.log('📸 02: Capturing Prologue Screen (Verifying banner/portrait alignment)...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02_prologue_screen.png') });
|
|
|
|
// Advance Prologue Dialogues
|
|
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('📸 02b: Capturing Prologue Mop Dialog...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '02b_prologue_mop.png') });
|
|
}
|
|
|
|
// Advance to Platformer
|
|
const mopChoice = await page.$('.choice-btn');
|
|
if (mopChoice) {
|
|
await mopChoice.click();
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
|
|
// 3. PLATFORMER PRE-LEVEL START SCREEN (Act II)
|
|
console.log('📸 03: Capturing Platformer Start Screen Modal...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '03_platformer_start_modal.png') });
|
|
|
|
// Click Start Act Button on the modal
|
|
const startPlatformerBtn = await page.$('.start-act-btn');
|
|
if (startPlatformerBtn) {
|
|
await startPlatformerBtn.click();
|
|
await new Promise(r => setTimeout(r, 800));
|
|
}
|
|
|
|
// 4. PLATFORMER GAMEPLAY
|
|
console.log('📸 04: Capturing Platformer Gameplay...');
|
|
const btnA = await page.$('.btn-a');
|
|
if (btnA) {
|
|
await btnA.click();
|
|
await new Promise(r => setTimeout(r, 300));
|
|
}
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '04_platformer_gameplay.png') });
|
|
|
|
// 5. TAVERN: SCHANKRAUM (Gaststube)
|
|
console.log('🍺 Advancing to Tavern Act (Schankraum)...');
|
|
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('tavern');
|
|
}
|
|
}
|
|
});
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
console.log('📸 05: Capturing Tavern Gaststube (Schankraum)...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '05_tavern_schankraum.png') });
|
|
|
|
// Test Verena Hotspot
|
|
console.log('👥 Testing Verena Dialog...');
|
|
const verenaBtn = await page.$('.hotspot-verena');
|
|
if (verenaBtn) {
|
|
await verenaBtn.click();
|
|
await new Promise(r => setTimeout(r, 700));
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '06_tavern_verena_dialog.png') });
|
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
if (actionBtn) await actionBtn.click();
|
|
await new Promise(r => setTimeout(r, 400));
|
|
}
|
|
|
|
// Test Armend Hotspot
|
|
console.log('🔨 Testing Armend Dialog...');
|
|
const armendBtn = await page.$('.hotspot-armend');
|
|
if (armendBtn) {
|
|
await armendBtn.click();
|
|
await new Promise(r => setTimeout(r, 700));
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '07_tavern_armend_dialog.png') });
|
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
if (actionBtn) await actionBtn.click();
|
|
await new Promise(r => setTimeout(r, 400));
|
|
}
|
|
|
|
// 6. TAVERN: MARKTPLATZ (Mario with spinning chainsaws)
|
|
console.log('🎪 Navigating to Marktplatz...');
|
|
const marktplatzTab = await page.$('.room-tab:nth-child(2)');
|
|
if (marktplatzTab) {
|
|
await marktplatzTab.click();
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
console.log('📸 08: Capturing Marktplatz (Mario chainsaws)...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '08_tavern_marktplatz.png') });
|
|
|
|
// Click Mario
|
|
const marioBtn = await page.$('.hotspot-mario');
|
|
if (marioBtn) {
|
|
await marioBtn.click();
|
|
await new Promise(r => setTimeout(r, 700));
|
|
console.log('📸 09: Capturing Mario Dialog...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09_tavern_mario_dialog.png') });
|
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
if (actionBtn) await actionBtn.click();
|
|
await new Promise(r => setTimeout(r, 400));
|
|
}
|
|
|
|
// Click NAT (Josef)
|
|
const natBtn = await page.$('.hotspot-nat');
|
|
if (natBtn) {
|
|
await natBtn.click();
|
|
await new Promise(r => setTimeout(r, 700));
|
|
console.log('📸 09b: Capturing NAT (Josef) Dialog...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '09b_tavern_nat_dialog.png') });
|
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
if (actionBtn) await actionBtn.click();
|
|
await new Promise(r => setTimeout(r, 400));
|
|
}
|
|
}
|
|
|
|
// 7. TAVERN: HINTERLAND (Mane hiking)
|
|
console.log('🌲 Navigating to Hinterland...');
|
|
const hinterlandTab = await page.$('.room-tab:nth-child(3)');
|
|
if (hinterlandTab) {
|
|
await hinterlandTab.click();
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
console.log('📸 10: Capturing Hinterland (Mane hiking)...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '10_tavern_hinterland.png') });
|
|
|
|
// Click Mane
|
|
const maneBtn = await page.$('.hotspot-mane');
|
|
if (maneBtn) {
|
|
await maneBtn.click();
|
|
await new Promise(r => setTimeout(r, 700));
|
|
console.log('📸 11: Capturing Mane Dialog...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '11_tavern_mane_dialog.png') });
|
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
if (actionBtn) await actionBtn.click();
|
|
await new Promise(r => setTimeout(r, 400));
|
|
}
|
|
}
|
|
|
|
// 8. TAVERN: SECRET HINTERZIMMER (VIP Rave via Secret Tap)
|
|
console.log('🗝️ Returning to Schankraum to check Secret Tap (initially invisible)...');
|
|
const schankraumTab = await page.$('.room-tab:nth-child(1)');
|
|
if (schankraumTab) {
|
|
await schankraumTab.click();
|
|
await new Promise(r => setTimeout(r, 600));
|
|
}
|
|
|
|
console.log('🪩 Triggering Secret Tap Easter Egg...');
|
|
const secretTap = await page.$('.secret-tap-hotspot');
|
|
if (secretTap) {
|
|
await secretTap.click();
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
console.log('📸 12: Capturing Secret VIP Hinterzimmer...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '12_tavern_hinterzimmer_vip.png') });
|
|
|
|
// Click Pü in Hinterzimmer
|
|
const pueBtn = await page.$('.hotspot-pue');
|
|
if (pueBtn) {
|
|
await pueBtn.click();
|
|
await new Promise(r => setTimeout(r, 700));
|
|
console.log('📸 13: Capturing Pü Dialog...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '13_tavern_pue_dialog.png') });
|
|
const actionBtn = await page.$('.dialog-actions .action-btn');
|
|
if (actionBtn) await actionBtn.click();
|
|
await new Promise(r => setTimeout(r, 400));
|
|
}
|
|
}
|
|
|
|
// 9. ZULUMAN MAGIC MIRROR & GSTANZL DUEL
|
|
console.log('🪞 Testing Zuluman Magic Mirror...');
|
|
const backToGaststube = await page.$('.hotspot-door-back-vip');
|
|
if (backToGaststube) {
|
|
await backToGaststube.click();
|
|
await new Promise(r => setTimeout(r, 600));
|
|
}
|
|
const mirrorBtn = await page.$('.hotspot-mirror');
|
|
if (mirrorBtn) {
|
|
await mirrorBtn.click();
|
|
await new Promise(r => setTimeout(r, 700));
|
|
console.log('📸 14: Capturing Zuluman Mirror...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '14_tavern_zuluman_mirror.png') });
|
|
const closeBtn = await page.$('.mirror-box .action-btn');
|
|
if (closeBtn) await closeBtn.click();
|
|
await new Promise(r => setTimeout(r, 400));
|
|
}
|
|
|
|
console.log('⚔️ Testing Gstanzl-Duel and [✕] Exit Button...');
|
|
const duelBtn = await page.$('.duel-trigger');
|
|
if (duelBtn) {
|
|
await duelBtn.click();
|
|
await new Promise(r => setTimeout(r, 700));
|
|
console.log('📸 15: Capturing Gstanzl Duel with [✕] button...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '15_tavern_gstanzl_duel.png') });
|
|
|
|
// Test the [✕] close button
|
|
const closeDuelBtn = await page.$('.duel-close-btn');
|
|
if (closeDuelBtn) {
|
|
console.log('Testing duel exit via [✕] button...');
|
|
await closeDuelBtn.click();
|
|
await new Promise(r => setTimeout(r, 600));
|
|
const duelClosed = await page.$('.duel-box') === null;
|
|
console.log('Duel modal closed successfully by [✕]:', duelClosed);
|
|
}
|
|
}
|
|
|
|
// Test reload persistence in Tavern Act
|
|
console.log('💾 Testing game state reload persistence...');
|
|
const stateBeforeReload = await page.evaluate(() => {
|
|
const app = document.querySelector('#app')?.__vue_app__;
|
|
const pinia = app?.config?.globalProperties?.$pinia;
|
|
const s = pinia?._s.get('game');
|
|
return s ? { currentAct: s.currentAct, kronkorken: s.kronkorken, beers: s.beers } : null;
|
|
});
|
|
console.log('State before reload:', stateBeforeReload);
|
|
|
|
await page.reload({ waitUntil: 'networkidle0' });
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
|
|
const stateAfterReload = await page.evaluate(() => {
|
|
const app = document.querySelector('#app')?.__vue_app__;
|
|
const pinia = app?.config?.globalProperties?.$pinia;
|
|
const s = pinia?._s.get('game');
|
|
return s ? { currentAct: s.currentAct, kronkorken: s.kronkorken, beers: s.beers } : null;
|
|
});
|
|
console.log('State after reload:', stateAfterReload);
|
|
|
|
// 10. 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('📸 16: Capturing Party Express Start Screen Modal...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '16_party_express_start_modal.png') });
|
|
|
|
// Start Party Express
|
|
const startExpressBtn = await page.$('.start-act-btn');
|
|
if (startExpressBtn) {
|
|
await startExpressBtn.click();
|
|
await new Promise(r => setTimeout(r, 800));
|
|
}
|
|
|
|
console.log('📸 17: Capturing Party Express Gameplay (Redesigned Bollerwagen)...');
|
|
// Serve beer
|
|
const throwBeerBtn = await page.$('.throw-beer-btn');
|
|
if (throwBeerBtn) {
|
|
await throwBeerBtn.click();
|
|
await new Promise(r => setTimeout(r, 400));
|
|
}
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '17_party_express_gameplay.png') });
|
|
|
|
// 11. 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(35);
|
|
store.serveGuest();
|
|
store.serveGuest();
|
|
store.setAct('finale');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
await new Promise(r => setTimeout(r, 1500));
|
|
console.log('📸 18: Capturing Certificate Modal...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '18_certificate_screen.png') });
|
|
|
|
// Test NOCHMAL SPIELEN button
|
|
console.log('🔄 Testing NOCHMAL SPIELEN button...');
|
|
const restartBtn = await page.$('.restart-btn');
|
|
if (restartBtn) {
|
|
await restartBtn.click();
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
console.log('📸 19: Capturing post-restart Intro screen...');
|
|
await page.screenshot({ path: path.join(SCREENSHOTS_DIR, '19_post_restart_intro.png') });
|
|
}
|
|
|
|
// Verify all images loaded properly
|
|
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:', 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();
|