/** * regen-gameplay-shots.cjs * ------------------------ * Ersetzt die "Spielverlauf"-Screenshots (play / play2) der Display-Vorschau * durch ECHTE Spielszenen: Auswahl-Screens (Welt/Karte/Szenario) werden * betreten und das Onboarding wird zuverlässig weggeklickt (Skip-Buttons + * mehrseitige "Weiter"-Tutorials, auch wenn der Button unter dem Fold liegt), * bevor der Screenshot fällt. * * Ersetzt NUR die play/play2-Bilder in-place; die start-Bilder (Onboarding) * und display_preview.html bleiben unangetastet, damit die Galerie * unverändert funktioniert. * * Start (aus dem playwright-Ordner wegen node_modules): * cd /c/xampp/htdocs/geograsim/.dontDeploy/playwright && \ * NODE_PATH="C:\\xampp\\htdocs\\geograsim\\.dontDeploy\\playwright\\node_modules" \ * node ../../App/.LocalDeveloperTools/display-preview/regen-gameplay-shots.cjs * * Optional: --only=sonnensystem,wal --vp=ipad */ const { chromium } = require('@playwright/test'); const path = require('path'); const SHOTS_DIR = 'C:/xampp/htdocs/geograsim/App/.LocalDeveloperTools/display-preview/shots'; const BASE_URL = 'http://localhost/geograsim/App/sims'; const VIEWPORTS = [ { key: 'ipad', w: 1180, h: 820 }, { key: 'fullhd', w: 1920, h: 1080 }, ]; // Sims mit "play"-Screenshot in der Galerie (fluss hat keinen play -> weggelassen). const SIMS = [ { id: 'busfahrt' }, { id: 'energiemanager', play2: true }, { id: 'entscheidungstag' }, { id: 'eu-werkstatt' }, { id: 'farmer' }, { id: 'fluggesellschaft' }, { id: 'heli' }, { id: 'logistik' }, { id: 'sonnensystem', play2: true }, { id: 'staustufen' }, { id: 'tourismusregion' }, { id: 'tourismustal' }, { id: 'vulkan', play2: true }, { id: 'wal' }, { id: 'weltkueche' }, ]; // ---- In-Page: EIN Onboarding-Schritt wegklicken ------------------------- async function skipStep(page) { return await page.evaluate(() => { // "loose": gerendert, aber darf auch unter dem Fold liegen (.click() geht trotzdem) const rendered = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); if (r.width < 6 || r.height < 6) return false; const s = getComputedStyle(el); return s.visibility !== 'hidden' && s.display !== 'none' && +s.opacity > 0.05 && !el.disabled && el.getAttribute('aria-hidden') !== 'true' && !el.classList.contains('hidden'); }; const clean = (s) => (s || '').replace(/\s+/g, ' ').trim(); const norm = (s) => clean(s).replace(/[^\p{L}\p{N} ]+/gu, '').trim().toLowerCase(); // Namensfelder füllen document.querySelectorAll('input[type=text],input[type=search],input:not([type])').forEach((i) => { if (rendered(i) && !i.value) { i.value = 'Demo'; i.dispatchEvent(new Event('input', { bubbles: true })); i.dispatchEvent(new Event('change', { bubbles: true })); } }); // 1) Explizite Skip-Buttons (schließen das GANZE Tutorial auf einmal) const skipSel = [ '#emOnbSkip', '#tutorial-skip', '#heliOnbSkip', '#snOnbSkip', '[onclick*="skipTutorial"]', '[onclick*="closeTutorial"]', '[onclick*="skipOnboarding"]', '[onclick*="skipHeliOnboarding"]', ]; for (const sel of skipSel) { const el = [...document.querySelectorAll(sel)].find(rendered); if (el) { el.click(); return 'skip:' + sel; } } // Skip per Text { const el = [...document.querySelectorAll('button,a,[role=button]')].find( (e) => rendered(e) && /^(uberspringen|tutorial uberspringen|einfuhrung uberspringen|nicht mehr anzeigen)$/.test(norm(e.textContent)) ); if (el) { el.click(); return 'skiptext'; } } // 2) "Weiter"/"Los geht's" NUR innerhalb eines sichtbaren Overlays // (verhindert das Anklicken von Gameplay-Buttons wie Vulkan "Weiter →") const overSel = '.ggs-modal-overlay,.ggs-overlay,.ggs-overlay-card,.sn-overlay,' + '.overlay,.overlay-card,.splash-overlay,.lg-splash-overlay,.modal,' + '.st-hint-overlay,.st-hint-modal,#vulk-overlay,#vulk-modal,.ggs-qi-back,' + '[id*="Onb"],[id*="onboard"],[id*="overlay" i],[id*="modal" i],' + '[class*="onb"],[class*="welcome"],[class*="tutorial"],[class*="intro"],' + '[class*="willkommen"],[class*="overlay" i],[class*="ggs-qi"]'; const overlays = [...document.querySelectorAll(overSel)].filter((o) => { if (!rendered(o)) return false; const r = o.getBoundingClientRect(); return r.width > 240 && r.height > 150; }); const proceedRx = /^(weiter|weiter gehts|los gehts|los geht s|auf gehts|verstanden|alles klar|habe verstanden|fertig|los arbeiten|jetzt starten|jetzt loslegen|jetzt spielen|spiel starten|neue partie|partie starten|los|okay|ok|schliessen|start|starten|beginnen)$/; for (const ov of overlays) { const btns = [...ov.querySelectorAll('button,a,[role=button],.btn')].filter(rendered); let el = btns.find((b) => proceedRx.test(norm(b.textContent))); if (!el) el = btns.find((b) => /^(x|close)$/.test(norm(b.textContent)) || /(^|\s)(close|ggs-modal-close)(\s|$)/i.test(b.className)); if (el) { el.click(); return 'proceed:' + norm(el.textContent).slice(0, 24); } } return null; }).catch(() => null); } // Noch ein Onboarding-Overlay sichtbar? (Heuristik für den Report) async function onboardingStillVisible(page) { return await page.evaluate(() => { const rx = /(willkommen|wusstest du|anleitung|was lerne ich|so funktioniert|einf(ü|u)hrung|beobachtungs-?aufgaben|dein kreislauf|uberspringen|überspringen|vulkanologie|observatorium|du erkundest die planeten|was tust du)/i; const els = [...document.querySelectorAll('.overlay,.ggs-overlay,.ggs-modal-overlay,.sn-overlay,.st-hint-overlay,#vulk-overlay,[class*="onb"],[class*="tutorial"],[class*="intro"],[class*="welcome"],[class*="overlay" i]')]; return els.some((el) => { const r = el.getBoundingClientRect(); if (r.width < 240 || r.height < 150) return false; const s = getComputedStyle(el); if (s.visibility === 'hidden' || s.display === 'none' || +s.opacity < 0.05 || el.classList.contains('hidden')) return false; return rx.test((el.textContent || '').slice(0, 500)); }); }).catch(() => false); } async function clickSel(page, sel) { return await page.evaluate((sel) => { const el = [...document.querySelectorAll(sel)].find((e) => { const r = e.getBoundingClientRect(); const s = getComputedStyle(e); return r.width > 4 && r.height > 4 && s.visibility !== 'hidden' && s.display !== 'none' && !e.classList.contains('hidden'); }); if (el) { el.click(); return true; } return false; }, sel).catch(() => false); } async function clickByText(page, source) { return await page.evaluate((src) => { const rx = new RegExp(src, 'i'); const norm = (s) => (s || '').replace(/\s+/g, ' ').trim().replace(/[^\p{L}\p{N} ]+/gu, '').trim(); const el = [...document.querySelectorAll('button,a,[role=button],.btn')].find((e) => { const r = e.getBoundingClientRect(); const s = getComputedStyle(e); if (r.width < 6 || r.height < 6 || s.visibility === 'hidden' || s.display === 'none' || e.classList.contains('hidden')) return false; return rx.test(norm(e.textContent)); }); if (el) { el.click(); return true; } return false; }, source).catch(() => false); } async function skipLoop(page, rounds = 12) { const seq = []; for (let i = 0; i < rounds; i++) { const r = await skipStep(page); if (!r) break; seq.push(r); await page.waitForTimeout(420); } return seq; } // ---- pro-Sim: Auswahl-Screen betreten, dann Tutorial wegklicken --------- const ENTRY = { // heli: Flugkarte braucht PHP-injizierte Waypoints (WP), die beim direkten // Aufruf von game.html fehlen (-> "WP is not defined"). Daher bleibt der // stabile Gameplay-Screen die Einsatzzentrale (Missionsauswahl nach Onboarding). 'wal': async (page) => { await clickSel(page, '.era-card-cta'); // "In dieser Welt starten →" await page.waitForTimeout(1800); await skipLoop(page); }, 'tourismusregion': async (page) => { await clickSel(page, '#mapList .map-btn, .map-btn'); // Wegkarte wählen await page.waitForTimeout(3000); // 3D-Szene await skipLoop(page); // "Willkommen …" weg }, 'tourismustal': async (page) => { await clickSel(page, '[data-scene]'); // Szene wählen await page.waitForTimeout(3000); await skipLoop(page); // "Die Gäste …" weg }, 'eu-werkstatt': async (page) => { await clickSel(page, '.scenario-card'); // Szenario betreten await page.waitForTimeout(1600); await skipLoop(page, 8); // Konzept-Intro weg await page.waitForTimeout(900); await clickByText(page, 'los gehts|los geht|^weiter|verstanden|alles klar'); await page.waitForTimeout(700); await skipLoop(page, 4); }, }; // Nach dem Wegklicken des Onboardings noch aufräumen, um die echte Szene zu zeigen. const POST = { 'vulkan': async (page) => { // "Los geht's" öffnet direkt das erste Aufgaben-Modal (#vulk-overlay). // Schließen -> darunter liegt die 3D-Vulkanszene + Steuerpanel (Slider). await page.evaluate(() => { const o = document.getElementById('vulk-overlay'); if (o) o.classList.remove('show'); }); await page.waitForTimeout(500); }, }; const PLAY2 = { 'vulkan': async (page) => { await clickSel(page, '#erupt-btn'); await page.waitForTimeout(3200); }, 'sonnensystem': async (page) => { await clickSel(page, '#obs-open-btn'); await page.waitForTimeout(1800); await skipLoop(page, 4); await page.waitForTimeout(1200); }, 'energiemanager': async (page) => { await clickSel(page, '#btnRunDay'); await page.waitForTimeout(3200); }, }; (async () => { const onlyArg = (process.argv.find((a) => a.startsWith('--only=')) || '').slice(7); const vpArg = (process.argv.find((a) => a.startsWith('--vp=')) || '').slice(5); const list = onlyArg ? SIMS.filter((s) => onlyArg.split(',').includes(s.id)) : SIMS; const vps = vpArg ? VIEWPORTS.filter((v) => v.key === vpArg) : VIEWPORTS; const browser = await chromium.launch({ args: ['--use-gl=swiftshader', '--ignore-gpu-blocklist', '--enable-unsafe-swiftshader'], }); for (const vp of vps) { console.log(`\n===== ${vp.key} (${vp.w}x${vp.h}) =====`); for (const sim of list) { const ctx = await browser.newContext({ viewport: { width: vp.w, height: vp.h }, deviceScaleFactor: 1 }); const page = await ctx.newPage(); const errs = []; page.on('pageerror', (e) => { if (errs.length < 3) errs.push('PE:' + e.message.slice(0, 50)); }); const url = `${BASE_URL}/${sim.id}/game.html`; try { await page.goto(url, { waitUntil: 'load', timeout: 40000 }); } catch (e) { errs.push('GOTO:' + e.message.slice(0, 40)); } await page.waitForTimeout(3200); // 3D / CDN let seq = await skipLoop(page, 14); // sofortiges Onboarding if (ENTRY[sim.id]) await ENTRY[sim.id](page); // Auswahl-Screen + Tutorial await page.waitForTimeout(2000); // Zweiter Durchlauf: 3D-Sims (vulkan/sonnensystem) laden ihr Onboarding // asynchron erst NACH dem ersten Skip -> hier erneut wegklicken. seq = seq.concat(await skipLoop(page, 10)); if (POST[sim.id]) await POST[sim.id](page); await page.waitForTimeout(1000); const stuck = await onboardingStillVisible(page); let ok = false; try { await page.screenshot({ path: path.join(SHOTS_DIR, `${sim.id}__${vp.key}__play.png`) }); ok = true; } catch (e) { errs.push('SHOT:' + e.message.slice(0, 40)); } let ok2 = null; if (sim.play2 && PLAY2[sim.id]) { try { await PLAY2[sim.id](page); await page.waitForTimeout(1000); await page.screenshot({ path: path.join(SHOTS_DIR, `${sim.id}__${vp.key}__play2.png`) }); ok2 = true; } catch (e) { ok2 = false; errs.push('SHOT2:' + e.message.slice(0, 40)); } } console.log( ' ' + sim.id.padEnd(18) + (ok ? 'play OK' : 'play FAIL') + (ok2 === null ? '' : ok2 ? ' · play2 OK' : ' · play2 FAIL') + (stuck ? ' [!! ONBOARDING NOCH SICHTBAR]' : '') + ' clicks=' + seq.length + (seq.length ? ' (' + seq.slice(0, 6).join(',') + ')' : '') + (errs.length ? ' ' + errs.slice(0, 2).join(' | ') : '') ); await ctx.close(); } } await browser.close(); console.log('\nFertig. Galerie: http://localhost/geograsim/App/.LocalDeveloperTools/display-preview/display_preview.html'); })();