/** * gen-display-preview.js * ---------------------- * Lokales Admin-Werkzeug: erstellt Bildschirmfotos aller Simulationen * (Onboarding/Start + Spielverlauf) in zwei Auflösungen (iPad + Full HD) * und generiert daraus eine statische Galerie-Seite. * * Ausgabe: App/.LocalDeveloperTools/display-preview/ * shots/____.png * display_preview.html * * Start: node gen-display-preview.js */ const { chromium } = require('@playwright/test'); const fs = require('fs'); const path = require('path'); // --- Pfade --------------------------------------------------------------- const APP_DIR = 'C:/xampp/htdocs/geograsim/App'; const SIMS_DIR = path.join(APP_DIR, 'sims'); const OUT_DIR = path.join(APP_DIR, '.LocalDeveloperTools', 'display-preview'); const SHOTS_DIR = path.join(OUT_DIR, 'shots'); const BASE_URL = 'http://localhost/geograsim/App/sims'; // --- Viewports ----------------------------------------------------------- const VIEWPORTS = [ { key: 'ipad', label: 'iPad', w: 1180, h: 820 }, { key: 'fullhd', label: 'Full HD', w: 1920, h: 1080 }, ]; // --- Freundliche Namen --------------------------------------------------- const NAMES = { weltkueche: '🍲 Welt-Küche', fluss: '🌊 Flussmanagement', heli: '🚁 Helikopter-Navigation', energiemanager: '⚡ Energiemanager', 'eu-werkstatt': '🇪🇺 EU-Werkstatt', farmer: '🌾 Farmer', busfahrt: '🚌 Busfahrt', sonnensystem: '🪐 Sonnensystem', tourismustal: '🏔️ Tourismustal', tourismusregion: '🗺️ Tourismusregion', logistik: '🚚 Logistik', fluggesellschaft:'✈️ Fluggesellschaft', staustufen: '💧 Staustufen', entscheidungstag:'📅 Entscheidungstag', vulkan: '🌋 Vulkan', wal: '🐋 Wal', kofferdetektiv: '🧳 Kofferdetektiv', stadt: '🏙️ Stadt', }; // --- Sims finden --------------------------------------------------------- function findSims() { return fs.readdirSync(SIMS_DIR, { withFileTypes: true }) .filter(d => d.isDirectory() && fs.existsSync(path.join(SIMS_DIR, d.name, 'game.html'))) .map(d => d.name) .sort(); } // --- In-Page: sichtbaren Start-/Weiter-Button klicken -------------------- async function clickStartLike(page) { return await page.evaluate(() => { const isVisible = (el) => { if (!el) return false; const r = el.getBoundingClientRect(); if (r.width < 4 || r.height < 4) return false; if (r.bottom < 0 || r.right < 0) return false; const st = getComputedStyle(el); return st.visibility !== 'hidden' && st.display !== 'none' && st.opacity !== '0' && !el.disabled && !el.classList.contains('hidden'); }; // 1) Prioritäts-Selektoren (bekannte Start-/Primary-Buttons der Plattform) const prio = [ '.ggs-qi-btn-primary', '[data-action="start"]', '#btnStart', '#obs-start', '#phase-start', '.sn-btn-primary', '.ggs-btn-primary', '.era-card-cta', '.scene-btn', '.map-btn', '.btn.primary', '.primary', '.next', ]; for (const sel of prio) { const el = [...document.querySelectorAll(sel)].find(isVisible); if (el) { el.click(); return sel + ' :: ' + (el.textContent || '').trim().slice(0, 24); } } // 2) Text-basiert — führende Emojis/Pfeile/Ziffern strippen, dann Anfang prüfen const clean = (s) => (s || '').replace(/^[^\p{L}]+/u, '').trim(); const rx = /^(los geht|los!|los →|los\b|start|starten|jetzt starten|neue partie|partie starten|spielen|spiel starten|beginnen|anfangen|einsteigen|erntejahr starten|routenplanung starten|in dieser welt starten|szene|weiter|verstanden|fertig|karte wählen|auswählen)/i; const cands = [...document.querySelectorAll('button, a.btn, a[role=button], .btn, [role=button], [onclick]')]; const el = cands.find(e => { if (!isVisible(e)) return false; const t = clean(e.textContent); return t.length > 0 && t.length < 44 && rx.test(t); }); if (el) { el.click(); return 'text :: ' + (el.textContent || '').trim().slice(0, 24); } return null; }).catch(() => null); } // --- Ein Screenshot ------------------------------------------------------ async function shoot(page, file) { try { await page.screenshot({ path: file }); // Viewport-Ausschnitt (exakte Auflösung) return true; } catch (e) { console.log(' ! Screenshot-Fehler:', e.message.slice(0, 80)); return false; } } (async () => { const sims = findSims(); // Optional: nur bestimmte Sims neu schießen (HTML wird immer komplett neu erzeugt). const onlyArg = (process.argv.find(a => a.startsWith('--only=')) || '').slice(7); const shootList = onlyArg ? onlyArg.split(',').map(s => s.trim()).filter(Boolean) : sims; console.log('Gefundene Sims (' + sims.length + '):', sims.join(', ')); if (onlyArg) console.log('Neu geschossen werden nur:', shootList.join(', ')); fs.mkdirSync(SHOTS_DIR, { recursive: true }); const browser = await chromium.launch(); // results[id][vp] = { start:bool, play:bool, play2:bool, errs:[] } const results = {}; for (const vp of VIEWPORTS) { console.log('\n===== Viewport ' + vp.label + ' (' + vp.w + '×' + vp.h + ') ====='); for (const id of shootList) { results[id] = results[id] || {}; const r = { start: false, play: false, play2: false, errs: [] }; results[id][vp.key] = r; const ctx = await browser.newContext({ viewport: { width: vp.w, height: vp.h }, deviceScaleFactor: 1, }); const page = await ctx.newPage(); page.on('pageerror', e => { if (r.errs.length < 4) r.errs.push('PE:' + e.message.slice(0, 60)); }); page.on('console', m => { if (m.type() === 'error' && r.errs.length < 4) r.errs.push('CON:' + m.text().slice(0, 60)); }); const url = BASE_URL + '/' + id + '/game.html'; try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); } catch (e) { r.errs.push('GOTO:' + e.message.slice(0, 50)); } // 1) Onboarding/Start await page.waitForTimeout(2500); r.start = await shoot(page, path.join(SHOTS_DIR, id + '__' + vp.key + '__start.png')); // 2) Spielverlauf: mehrere Runden klicken (Onboarding schließen -> Menü -> Spiel -> Tutorial weg) let clicks = 0, last = null; for (let round = 0; round < 4; round++) { const clicked = await clickStartLike(page); if (!clicked) break; clicks++; if (clicked === last) break; // gleiches Element erneut -> Abbruch last = clicked; await page.waitForTimeout(800); } if (clicks > 0) { await page.waitForTimeout(2500); r.play = await shoot(page, path.join(SHOTS_DIR, id + '__' + vp.key + '__play.png')); // 3) optionaler zweiter Play-Screen: noch ein Weiter/Start, falls vorhanden const more = await clickStartLike(page); if (more && more !== last) { await page.waitForTimeout(2000); r.play2 = await shoot(page, path.join(SHOTS_DIR, id + '__' + vp.key + '__play2.png')); } } const flags = [r.start ? 'S' : '-', r.play ? 'P' : '-', r.play2 ? 'P2' : '--'].join(' '); console.log(' ' + id.padEnd(18) + '[' + flags + ']' + (clicks ? ' clicks=' + clicks : ' (kein Start-Button)') + (r.errs.length ? ' ' + r.errs.slice(0, 2).join(' | ') : '')); await ctx.close(); } } await browser.close(); // --- HTML generieren --------------------------------------------------- const genDate = new Date().toLocaleString('de-AT', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); const screens = [ { key: 'start', label: 'Onboarding / Start' }, { key: 'play', label: 'Spielverlauf' }, { key: 'play2', label: 'Spielverlauf (2)' }, ]; function cell(id, vp, screenKey) { const rel = 'shots/' + id + '__' + vp.key + '__' + screenKey + '.png'; const abs = path.join(SHOTS_DIR, id + '__' + vp.key + '__' + screenKey + '.png'); if (fs.existsSync(abs)) { return '' + '' + id + ' ' + vp.label + ' ' + screenKey + '' + '' + vp.label + ' · ' + vp.w + '×' + vp.h + ''; } return '
— kein Screenshot —' + vp.label + '
'; } let blocks = ''; for (const id of sims) { const name = NAMES[id] || (id.charAt(0).toUpperCase() + id.slice(1)); // nur Screen-Zeilen zeigen, die mindestens ein Bild haben const activeScreens = screens.filter(sc => VIEWPORTS.some(vp => fs.existsSync(path.join(SHOTS_DIR, id + '__' + vp.key + '__' + sc.key + '.png'))) ); const rows = (activeScreens.length ? activeScreens : [screens[0]]).map(sc => '' + sc.label + '' + VIEWPORTS.map(vp => '' + cell(id, vp, sc.key) + '').join('') + '' ).join('\n'); blocks += '\n
' + '

' + name + ' ' + id + '

' + '' + VIEWPORTS.map(vp => '').join('') + '' + rows + '
' + vp.label + ' ' + vp.w + '×' + vp.h + '
'; } const nav = sims.map(id => '' + (NAMES[id] || id) + '' ).join(''); const html = ` Display-Vorschau (lokal)

Display-Vorschau (lokal)

${sims.length} Simulationen · je Onboarding/Start + Spielverlauf · iPad 1180×820 & Full HD 1920×1080 · generiert am ${genDate}
${blocks}
`; const outFile = path.join(OUT_DIR, 'display_preview.html'); fs.writeFileSync(outFile, html, 'utf8'); console.log('\nHTML geschrieben: ' + outFile); // --- Zusammenfassung --------------------------------------------------- console.log('\n===== ZUSAMMENFASSUNG ====='); let totalShots = 0, missingPlay = []; for (const id of sims) { const parts = []; for (const vp of VIEWPORTS) { const r = (results[id] && results[id][vp.key]) || {}; const n = (r.start ? 1 : 0) + (r.play ? 1 : 0) + (r.play2 ? 1 : 0); totalShots += n; parts.push(vp.label + ':' + n + '/3(' + (r.start ? 'S' : '-') + (r.play ? 'P' : '-') + (r.play2 ? '2' : '-') + ')'); if (!r.play) missingPlay.push(id + '/' + vp.key); } console.log(' ' + id.padEnd(18) + parts.join(' ')); } console.log('\nScreenshots gesamt: ' + totalShots); console.log('Fehlende Play-Screens: ' + (missingPlay.length ? missingPlay.join(', ') : 'keine')); console.log('Seite: http://localhost/geograsim/App/.LocalDeveloperTools/display-preview/display_preview.html'); })();