fecd8bb9c9
562 Ersetzungen in 50 Dateien: Halbgeviertstrich zwischen zwei Ziffern (= "bis"-Bereich) bekommt Leerzeichen davor/danach (3–4 -> 3 – 4). Regex /([0-9])\s*–\s*(?=[0-9])/ auf Rohtext (format-erhaltend, CRLF/Einrueckung unveraendert), Bindestriche in Woertern und Minuszeichen unangetastet, alle JSON weiter gueltig. Energiemanager-Zeitbloecke sind Display-Labels (kein Logik-Key) -> safe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
295 lines
14 KiB
JavaScript
295 lines
14 KiB
JavaScript
/**
|
||
* 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/<id>__<vp>__<screen>.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 '<a href="' + rel + '" target="_blank" class="shot">'
|
||
+ '<img loading="lazy" src="' + rel + '" alt="' + id + ' ' + vp.label + ' ' + screenKey + '">'
|
||
+ '<span class="cap">' + vp.label + ' · ' + vp.w + '×' + vp.h + '</span></a>';
|
||
}
|
||
return '<div class="missing">— kein Screenshot —<span class="cap">' + vp.label + '</span></div>';
|
||
}
|
||
|
||
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 =>
|
||
'<tr><th class="screen">' + sc.label + '</th>'
|
||
+ VIEWPORTS.map(vp => '<td>' + cell(id, vp, sc.key) + '</td>').join('')
|
||
+ '</tr>'
|
||
).join('\n');
|
||
|
||
blocks += '\n<section class="sim" id="sim-' + id + '">'
|
||
+ '<h2>' + name + ' <span class="simid">' + id + '</span></h2>'
|
||
+ '<table><thead><tr><th class="screen"></th>'
|
||
+ VIEWPORTS.map(vp => '<th>' + vp.label + ' <span class="res">' + vp.w + '×' + vp.h + '</span></th>').join('')
|
||
+ '</tr></thead><tbody>' + rows + '</tbody></table></section>';
|
||
}
|
||
|
||
const nav = sims.map(id =>
|
||
'<a href="#sim-' + id + '">' + (NAMES[id] || id) + '</a>'
|
||
).join('');
|
||
|
||
const html = `<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Display-Vorschau (lokal)</title>
|
||
<style>
|
||
:root { --bg:#f4f2ec; --card:#fff; --ink:#1f2b26; --accent:#1f4b37; --muted:#6b7a72; --line:rgba(0,0,0,.08); }
|
||
* { box-sizing:border-box; }
|
||
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:var(--bg); color:var(--ink); }
|
||
header { background:var(--accent); color:#fff; padding:20px 28px; position:sticky; top:0; z-index:10; box-shadow:0 2px 10px rgba(0,0,0,.15); }
|
||
header h1 { margin:0; font-size:1.4rem; }
|
||
header .meta { font-size:.82rem; opacity:.85; margin-top:4px; }
|
||
nav.toc { display:flex; flex-wrap:wrap; gap:6px; padding:14px 28px; background:#e9e6dd; border-bottom:1px solid var(--line); }
|
||
nav.toc a { font-size:.82rem; text-decoration:none; color:var(--accent); background:#fff; border:1px solid var(--line); border-radius:999px; padding:4px 11px; }
|
||
nav.toc a:hover { background:var(--accent); color:#fff; }
|
||
main { padding:24px 28px 60px; max-width:1500px; margin:0 auto; }
|
||
section.sim { background:var(--card); border-radius:14px; padding:18px 20px 22px; margin-bottom:26px; box-shadow:0 4px 14px rgba(0,0,0,.06); scroll-margin-top:90px; }
|
||
section.sim h2 { margin:0 0 14px; font-size:1.25rem; color:var(--accent); display:flex; align-items:baseline; gap:10px; }
|
||
.simid { font-size:.72rem; font-weight:600; color:var(--muted); background:#eef0ec; padding:2px 8px; border-radius:6px; letter-spacing:.03em; }
|
||
table { width:100%; border-collapse:collapse; table-layout:fixed; }
|
||
thead th { text-align:left; font-size:.82rem; color:var(--muted); padding:0 0 8px; font-weight:700; }
|
||
thead th .res { font-weight:500; opacity:.7; }
|
||
th.screen { width:150px; vertical-align:top; font-size:.9rem; color:var(--ink); padding-top:8px; }
|
||
td { padding:6px 6px 14px; vertical-align:top; width:calc((100% - 150px)/2); }
|
||
a.shot { display:block; border:1px solid var(--line); border-radius:8px; overflow:hidden; background:#0d1512; text-decoration:none; }
|
||
a.shot img { display:block; width:100%; max-width:100%; height:auto; }
|
||
.cap { display:block; font-size:.72rem; color:var(--muted); padding:5px 8px; background:#fafafa; border-top:1px solid var(--line); }
|
||
a.shot .cap { color:#c8d4ce; background:#141f1a; border-top:none; }
|
||
.missing { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; min-height:120px; border:1px dashed var(--line); border-radius:8px; color:var(--muted); font-size:.85rem; background:#faf9f5; }
|
||
.missing .cap { background:none; border:none; }
|
||
@media (max-width:820px){ th.screen{width:90px;} td{width:calc((100% - 90px)/2);} }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>Display-Vorschau (lokal)</h1>
|
||
<div class="meta">${sims.length} Simulationen · je Onboarding/Start + Spielverlauf · iPad 1180×820 & Full HD 1920×1080 · generiert am ${genDate}</div>
|
||
</header>
|
||
<nav class="toc">${nav}</nav>
|
||
<main>${blocks}
|
||
</main>
|
||
</body>
|
||
</html>`;
|
||
|
||
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');
|
||
})();
|