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>
283 lines
12 KiB
JavaScript
283 lines
12 KiB
JavaScript
/**
|
|
* 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');
|
|
})();
|