Textsystem: Bis-Strich mit Leerzeichen (Zahl – Zahl) per Agent-Sweep
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>
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 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');
|
||||
})();
|
||||
Reference in New Issue
Block a user