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:
2026-08-22 12:38:19 +02:00
parent 9c99359d9c
commit fecd8bb9c9
139 changed files with 1827 additions and 411 deletions
+34
View File
@@ -0,0 +1,34 @@
const { chromium } = require('@playwright/test');
(async () => {
const b = await chromium.launch(); const p = await b.newPage();
const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
await p.route('**/api/admin', r => r.fulfill({ contentType:'application/json', body: JSON.stringify({authenticated:true}) }));
await p.goto('http://localhost/geograsim/App/admin-weltkueche.html');
await p.waitForTimeout(2000);
const r = await p.evaluate(() => {
const dishes = [...document.querySelectorAll('.dish')];
const names = dishes.map(d => d.querySelector('.nm')?.textContent);
function dishInfo(namePart){
const d = dishes.find(x => (x.querySelector('.nm')?.textContent||'').toLowerCase().includes(namePart));
if(!d) return null;
const rows = [...d.querySelectorAll('tbody tr')].map(tr => ({
ing: tr.querySelector('.ing-nm')?.textContent.replace(/\s+/g,' ').trim(),
ok: [...tr.querySelectorAll('.cty.ok')].map(c=>c.textContent.replace(/\s+/g,' ').trim()),
dist: [...tr.querySelectorAll('.cty.dist')].map(c=>c.title.split(' · ')[0]),
}));
return rows;
}
const groestl = dishInfo('tiroler gröstl');
const kart = groestl && groestl.find(r => /Kartoffel/i.test(r.ing));
const eierRow = groestl && groestl.find(r => /Eier/i.test(r.ing));
return {
dishCount: dishes.length,
sampleNames: names.slice(0,6),
kartoffelOk: kart ? kart.ok : null,
eierOk: eierRow ? eierRow.ok : null,
warnCount: document.querySelectorAll('.warn').length,
};
});
console.log(JSON.stringify({result:r, pageErrors:errs}, null, 2));
await b.close();
})();
+19
View File
@@ -0,0 +1,19 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const r = await page.request.get('https://geograsim.at/teacher.html');
const html = await r.text();
console.log('STATUS', r.status(), 'LEN', html.length);
console.log('has renderLiveGesamt:', html.includes('renderLiveGesamt'));
console.log('has _liveQualityBar:', html.includes('_liveQualityBar'));
console.log('has tab-bar:', html.includes('tab-bar'));
console.log('has Ergebnisse:', html.includes('Ergebnisse'));
console.log('has external script src (module?):', /<script[^>]+src=/i.test(html));
// show script tags
const scripts = [...html.matchAll(/<script[^>]*>/gi)].map(m=>m[0]);
console.log('SCRIPT TAGS:', JSON.stringify(scripts, null, 2));
// show first 600 chars to see redirect logic
console.log('HEAD:', html.slice(0, 1200));
await browser.close();
})();
+22
View File
@@ -0,0 +1,22 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const r = await page.request.get('https://geograsim.at/teacher.html');
const html = await r.text();
const idxLogin = html.indexOf('login.html');
console.log('--- context around first login.html reference ---');
console.log(html.slice(idxLogin - 400, idxLogin + 120));
for (const name of ['renderLiveGesamt', '_liveQualityBar']) {
const idx = html.indexOf('function ' + name);
console.log('\n--- "function ' + name + '" @' + idx + ' ---');
if (idx >= 0) console.log(html.slice(idx - 60, idx + 120));
}
const idxErg = html.indexOf('Ergebnisse');
console.log('\n--- context around Ergebnisse ---');
console.log(html.slice(idxErg - 220, idxErg + 80));
await browser.close();
})();
+29
View File
@@ -0,0 +1,29 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const r = await page.request.get('https://geograsim.at/teacher.html');
const html = await r.text();
// 1) Static tab-bar markup
const tb = html.indexOf('id="tab-bar"');
console.log('--- #tab-bar markup ---');
console.log(tb >= 0 ? html.slice(tb - 40, tb + 700) : 'tab-bar id NOT found');
// 2) Extract _liveQualityBar function body and run it in isolation
const start = html.indexOf('function _liveQualityBar');
// find matching braces
let i = html.indexOf('{', start), depth = 0, end = -1;
for (; i < html.length; i++) {
if (html[i] === '{') depth++;
else if (html[i] === '}') { depth--; if (depth === 0) { end = i + 1; break; } }
}
const fnSrc = html.slice(start, end);
const fn = new Function(fnSrc + '\nreturn _liveQualityBar;')();
const sample = fn(8, 2);
console.log('\n--- _liveQualityBar(8,2) OUTPUT ---');
console.log(sample);
console.log('\ncontains "80":', String(sample).includes('80'));
await browser.close();
})();
@@ -0,0 +1,294 @@
/**
* 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 &amp; 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');
})();
@@ -0,0 +1,17 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const res = await page.goto('http://localhost/geograsim/App/modul-weltkueche');
await page.waitForTimeout(700);
const r = await page.evaluate(() => {
const sec = document.getElementById('lehrplan-bezug');
const komps = [...document.querySelectorAll('.md-kompetenz .md-kompetenz-title')].map(e=>e.textContent.trim());
const anchors = document.querySelectorAll('.md-anchor').length;
const countryChips = [...document.querySelectorAll('.md-lehrplan-filters .md-chip')].map(c=>c.textContent.trim());
const empty = document.body.textContent.includes('Noch keine Kompetenzen zugeordnet');
return { hasSection: !!sec, kompetenzTitles: komps, anchorCount: anchors, countryChips, showsEmpty: empty };
});
console.log(JSON.stringify({ httpStatus: res.status(), result: r }, null, 2));
await browser.close();
})();
@@ -0,0 +1,17 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const res = await page.goto('https://geograsim.at/modul-weltkueche', { waitUntil: 'networkidle' });
await page.waitForTimeout(700);
const r = await page.evaluate(() => {
const sec = document.getElementById('lehrplan-bezug');
const komps = [...document.querySelectorAll('.md-kompetenz .md-kompetenz-title')].map(e=>e.textContent.trim());
const anchors = document.querySelectorAll('.md-anchor').length;
const countryChips = [...document.querySelectorAll('.md-lehrplan-filters .md-chip')].map(c=>c.textContent.trim());
const empty = document.body.textContent.includes('Noch keine Kompetenzen zugeordnet');
return { hasSection: !!sec, kompetenzTitles: komps, anchorCount: anchors, countryChips, showsEmpty: empty };
});
console.log(JSON.stringify({ httpStatus: res.status(), result: r }, null, 2));
await browser.close();
})();
@@ -0,0 +1,37 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const errs=[]; page.on('pageerror',e=>errs.push(e.message));
await page.goto('http://localhost/geograsim/App/modul-weltkueche', { waitUntil:'domcontentloaded' });
await page.waitForTimeout(700);
async function snapshot() {
return await page.evaluate(() => {
const all = [...document.querySelectorAll('.md-anchor')];
const visible = all.filter(a => !a.classList.contains('hidden'));
const byType = t => visible.filter(a => a.dataset.anchorType===t).length;
const fachCountries = {};
visible.filter(a=>a.dataset.anchorType==='fach').forEach(a=>{ fachCountries[a.dataset.country]=(fachCountries[a.dataset.country]||0)+1; });
return { total: all.length, visible: visible.length, fachVisible: byType('fach'), fachCountries };
});
}
async function clickChip(country) {
await page.evaluate((c) => {
const btn = [...document.querySelectorAll('.md-chip')].find(b => (b.dataset.country||'')===c);
if (btn) btn.click();
}, country);
await page.waitForTimeout(200);
}
const start = await snapshot();
await clickChip(''); // Alle Länder
const alle = await snapshot();
await clickChip('DE'); // Deutschland
const de = await snapshot();
await clickChip('AT'); // Österreich
const at = await snapshot();
console.log(JSON.stringify({ start, alle, de, at, pageErrors: errs }, null, 2));
await browser.close();
})();
+15
View File
@@ -0,0 +1,15 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const errs=[]; page.on('pageerror',e=>errs.push(e.message));
await page.goto('https://geograsim.at/modul-weltkueche', { waitUntil:'domcontentloaded' });
await page.waitForTimeout(900);
async function snap(){ return await page.evaluate(()=>{ const all=[...document.querySelectorAll('.md-anchor')]; const vis=all.filter(a=>!a.classList.contains('hidden')); const fc={}; vis.filter(a=>a.dataset.anchorType==='fach').forEach(a=>fc[a.dataset.country]=(fc[a.dataset.country]||0)+1); return {total:all.length, visible:vis.length, fach:vis.filter(a=>a.dataset.anchorType==='fach').length, fachCountries:fc}; }); }
async function chip(c){ await page.evaluate(c=>{const b=[...document.querySelectorAll('.md-chip')].find(x=>(x.dataset.country||'')===c); if(b)b.click();},c); await page.waitForTimeout(200); }
await chip(''); const alle=await snap();
await chip('DE'); const de=await snap();
await chip('AT'); const at=await snap();
console.log(JSON.stringify({alle,de,at,pageErrors:errs},null,2));
await browser.close();
})();
+25
View File
@@ -0,0 +1,25 @@
const { chromium } = require('@playwright/test');
(async () => {
const b = await chromium.launch(); const p = await b.newPage();
await p.addInitScript(() => {
window.__GGS__ = { simId: 'test', baseUrl: '' };
window.fetch = () => Promise.resolve({ ok:true, json:()=>Promise.resolve({}) }); // Heartbeat stummschalten
});
await p.goto('about:blank');
await p.addScriptTag({ path: 'C:/xampp/htdocs/geograsim/App/assets/js/live-client.js' });
const read = () => p.evaluate(() => window.__GGS_LIVE__ ? window.__GGS_LIVE__.activeMs : null);
const set = (v) => p.evaluate((v) => window.__GGS_LIVE__.setPaused(v), v);
await p.waitForTimeout(300);
const a1 = await read();
await p.waitForTimeout(2500); const a2 = await read(); // sollte gestiegen sein
await set(true);
await p.waitForTimeout(2500); const a3 = await read(); // sollte eingefroren sein
await set(false);
await p.waitForTimeout(2500); const a4 = await read(); // sollte wieder steigen
const grewNormal = (a2 - a1) > 1500;
const frozen = (a3 - a2) < 300;
const resumed = (a4 - a3) > 1500;
console.log(JSON.stringify({ a1,a2,a3,a4, grewNormal, frozen, resumed,
verdict: (grewNormal && frozen && resumed) ? 'PASS' : 'FAIL' }, null, 2));
await b.close();
})();
+74
View File
@@ -0,0 +1,74 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const out = {};
// 1) modul-weltkueche: Lehrplan-Bezug jetzt vorhanden?
{
const page = await browser.newPage();
await page.goto('https://geograsim.at/modul-weltkueche', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
out.modulWeltkueche = await page.evaluate(() => ({
kompetenzTitles: [...document.querySelectorAll('.md-kompetenz-title')].map(e=>e.textContent.trim()),
anchorCount: document.querySelectorAll('.md-anchor').length,
showsEmpty: document.body.textContent.includes('Noch keine Kompetenzen zugeordnet'),
}));
await page.close();
}
// 2) Weltküche-Sim: Artikel + Ukraine-Feedback
{
const page = await browser.newPage();
const errs=[]; page.on('pageerror',e=>errs.push(e.message));
await page.goto('https://geograsim.at/sims/weltkueche/game.html', { waitUntil:'domcontentloaded' });
await page.waitForTimeout(2000);
out.weltkueche = await page.evaluate(async () => {
try {
if (!DISHES.length) await loadWeltkuecheData();
const dish = DISHES.find(d=>d.id==='tiroler-groestl'); current={dish};
buildTaskFor('kartoffel');
return {
kartoffelArt: ING.kartoffel.art,
badArticles: Object.entries(ING).filter(([k,v])=>!['der','die','das'].includes(v&&v.art)).map(([k])=>k),
wrongUA: explainChoice('kartoffel','ukraine',false).text,
};
} catch(e){ return { error:String(e) }; }
});
out.weltkueche.pageErrors = errs;
await page.close();
}
// 3) teacher.html: lädt + neue Funktionen da (direkter Link, den das Produkt nutzt)
{
const page = await browser.newPage();
const errs=[]; page.on('pageerror',e=>errs.push(e.message));
// teacher.html leitet ohne Login auf login.html um — Navigation abfangen, sofort auswerten
await page.route('**/login.html', r=>r.abort());
await page.goto('https://geograsim.at/teacher.html', { waitUntil:'domcontentloaded' }).catch(()=>{});
await page.waitForTimeout(1500);
out.teacher = await page.evaluate(() => ({
hasRenderGesamt: typeof renderLiveGesamt === 'function',
hasQualityBar: typeof _liveQualityBar === 'function',
qualitySample: (typeof _liveQualityBar==='function') ? _liveQualityBar(8,2) : null,
tabLabels: [...document.querySelectorAll('#tab-bar .tab')].map(t=>t.textContent.trim()),
})).catch(e=>({evalError:String(e)}));
out.teacher.pageErrors = errs;
await page.close();
}
// 4) Tourismusregion passstrasse Karte
{
const page = await browser.newPage();
const mc = await page.request.get('https://geograsim.at/sims/tourismusregion/js/maps-custom.json');
const j = await mc.json();
out.tourismus = {
keys: Object.keys(j),
passBergaufPoints: j.passstrasse?.routes?.bergauf?.length ?? null,
passSlots: (j.passstrasse?.slots||[]).length,
};
await page.close();
}
console.log(JSON.stringify(out, null, 2));
await browser.close();
})();
@@ -0,0 +1,21 @@
const { chromium } = require('@playwright/test');
(async () => {
const b = await chromium.launch(); const p = await b.newPage();
const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
await p.route('**/login.html', r=>r.abort());
await p.goto('http://localhost/geograsim/App/teacher.html',{waitUntil:'domcontentloaded'}).catch(()=>{});
await p.waitForTimeout(1200);
const r = await p.evaluate(() => ({
hasSimWord: typeof simWord==='function',
hasSimCode: typeof simCode==='function',
logistikWord: typeof simWord==='function' ? simWord('logistik') : null,
logistikCode: typeof simCode==='function' ? simCode('logistik') : null,
unknownCode: typeof simCode==='function' ? simCode('irgendwas') : null,
logistikFields: (typeof LIVE_PRIMARY_FIELDS==='object' && LIVE_PRIMARY_FIELDS.logistik) ? LIVE_PRIMARY_FIELDS.logistik.map(f=>f.label) : null,
liveShortLogistik: typeof LIVE_SIM_SHORT==='object' ? LIVE_SIM_SHORT.logistik : null,
// Kürzel-Eindeutigkeit
codesUnique: (function(){ if(typeof SIM_LABELS!=='object') return null; var seen={},dup=[]; Object.keys(SIM_LABELS).forEach(k=>{var c=SIM_LABELS[k].c; if(seen[c])dup.push(c); seen[c]=1;}); return dup; })(),
}));
console.log(JSON.stringify({result:r, pageErrors:errs}, null, 2));
await b.close();
})();
@@ -0,0 +1,26 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const errors = [];
page.on('pageerror', e => errors.push('PAGEERROR: '+e.message));
page.on('console', m => { if (m.type()==='error') errors.push(m.text()); });
const res = await page.goto('http://localhost/geograsim/App/teacher');
await page.waitForTimeout(1000);
const r = await page.evaluate(() => {
const tabs = [...document.querySelectorAll('#tab-bar .tab')].map(t => t.textContent.trim());
let toggle = null;
try { toggle = (typeof _liveToggleHtml === 'function') ? _liveToggleHtml() : null; } catch(e){ toggle = 'ERR:'+e.message; }
return {
tabLabels: tabs,
hasSetLiveMode: typeof setLiveMode === 'function',
hasRenderGesamt: typeof renderLiveGesamt === 'function',
hasQualityBar: typeof _liveQualityBar === 'function',
qualitySample: (typeof _liveQualityBar === 'function') ? _liveQualityBar(8,2) : null,
toggleHasGesamt: (typeof toggle === 'string') ? toggle.includes('Gesamt') : toggle,
toggleHasTag: (typeof toggle === 'string') ? toggle.includes('Tag') : toggle,
};
});
console.log(JSON.stringify({ httpStatus: res.status(), result: r, pageErrors: errors }, null, 2));
await browser.close();
})();
@@ -0,0 +1,21 @@
const { chromium } = require('@playwright/test');
(async () => {
const b = await chromium.launch(); const p = await b.newPage({ viewport:{width:1180,height:820} });
const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
await p.goto('http://localhost/geograsim/App/sims/tourismusregion/game.html');
await p.waitForTimeout(1500);
await p.click('.map-btn'); // Route klicken → Spiel startet → Willkommenskarte
await p.waitForTimeout(2000);
const r = await p.evaluate(() => {
const card = document.getElementById('cardText');
const txt = card ? card.textContent : '';
return {
started: !!window.__ttState,
cardHasEnvText: /Umwelt ist deine Landschaft/.test(txt),
cardHasNaturschutz: /Naturschutzgebiet/.test(txt),
envKpi: document.getElementById('kEnv')?.textContent,
};
});
console.log(JSON.stringify({ result:r, pageErrors: errs.filter(e=>!/404|Failed to load resource/.test(e)) }, null, 2));
await b.close();
})();
@@ -0,0 +1,38 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const errors = [];
page.on('pageerror', e => errors.push('PAGEERROR: '+e.message));
page.on('console', m => { if (m.type()==='error') errors.push(m.text()); });
// 1) maps-custom.json direkt prüfen
const mc = await page.request.get('http://localhost/geograsim/App/sims/tourismusregion/js/maps-custom.json');
const mcJson = await mc.json();
const ps = mcJson.passstrasse || null;
// 2) Hintergrundbild erreichbar?
const img = await page.request.get('http://localhost/geograsim/App/sims/tourismusregion/assets/maps/passstrasse.png');
// 3) Sim-Startscreen laden, Kartenliste prüfen
const res = await page.goto('http://localhost/geograsim/App/sims/tourismusregion/game.html');
await page.waitForTimeout(2500);
const startInfo = await page.evaluate(() => {
const list = document.getElementById('mapList');
const labels = list ? [...list.querySelectorAll('*')].map(e=>e.textContent).filter(t=>t && t.length<40) : [];
const hasPass = !!(list && /Passstra/i.test(list.textContent||''));
return { mapListText: (list?list.textContent:'').replace(/\s+/g,' ').trim().slice(0,300), hasPass };
});
console.log(JSON.stringify({
httpStatus: res.status(),
passInMapsCustom: !!ps,
routeKeys: ps ? Object.keys(ps.routes||{}) : null,
bergaufPoints: ps && ps.routes && ps.routes.bergauf ? ps.routes.bergauf.length : null,
slotCount: ps ? (ps.slots||[]).length : null,
bgImageStatus: img.status(),
startInfo,
pageErrors: errors
}, null, 2));
await browser.close();
})();
@@ -0,0 +1,30 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1180, height: 820 } }); // iPad Landscape
const errs=[]; page.on('pageerror',e=>errs.push(e.message)); page.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
await page.goto('http://localhost/geograsim/App/sims/tourismusregion/game.html');
await page.waitForTimeout(1500);
const before = await page.evaluate(() => ({
hasPlayerName: !!document.getElementById('playerName'),
hasClassCode: !!document.getElementById('classCode'),
hasBtnStart: !!document.getElementById('btnStart'),
hasBtnSave: !!document.getElementById('btnSave'),
mapBtnCount: document.querySelectorAll('.map-btn').length,
startOverlayHidden: document.getElementById('startOverlay').classList.contains('hidden'),
}));
// ② Klick auf erste Route sollte SOFORT starten
await page.click('.map-btn');
await page.waitForTimeout(2500);
const after = await page.evaluate(() => ({
startOverlayHidden: document.getElementById('startOverlay').classList.contains('hidden'),
gameStarted: !!window.__ttState,
mapId: window.__ttState && window.__ttState.map && window.__ttState.map.id,
}));
await page.screenshot({ path: 'C:/Users/herr_/AppData/Local/Temp/claude/c--xampp-htdocs-geograsim/c02572ea-7616-4524-a974-dff0e5670d2c/scratchpad/tr-ipad.png' });
console.log(JSON.stringify({ before, after, pageErrors: errs }, null, 2));
await browser.close();
})();
@@ -0,0 +1,27 @@
const { chromium } = require('@playwright/test');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const errors = [];
page.on('console', m => { if (m.type()==='error') errors.push(m.text()); });
page.on('pageerror', e => errors.push('PAGEERROR: '+e.message));
await page.goto('http://localhost/geograsim/App/sims/weltkueche/game.html');
await page.waitForTimeout(1500);
const r = await page.evaluate(async () => {
try {
if (!DISHES.length) await loadWeltkuecheData();
const dish = DISHES.find(d => d.id === 'tiroler-groestl');
if (!dish) return { error: 'no tiroler-groestl dish' };
current = { dish };
const task = buildTaskFor('kartoffel');
const art = ING.kartoffel.art;
const correctAT = explainChoice('kartoffel', 'oesterreich', true).text;
const correctDE = explainChoice('kartoffel', 'deutschland', true).text;
const wrongUA = explainChoice('kartoffel', 'ukraine', false).text;
const bad = Object.entries(ING).filter(([k,v]) => !['der','die','das'].includes(v && v.art)).map(([k])=>k);
return { art, taskCorrect: task.correct, correctAT, correctDE, wrongUA, badArticles: bad, ingCount: Object.keys(ING).length };
} catch (e) { return { error: String(e), stack: e.stack }; }
});
console.log(JSON.stringify({ result: r, consoleErrors: errors }, null, 2));
await browser.close();
})();
+38
View File
@@ -0,0 +1,38 @@
const { chromium } = require('@playwright/test');
(async () => {
const b = await chromium.launch(); const p = await b.newPage();
const errs=[]; p.on('pageerror',e=>errs.push(e.message)); p.on('console',m=>{if(m.type()==='error')errs.push(m.text());});
await p.goto('http://localhost/geograsim/App/sims/weltkueche/game.html');
await p.waitForTimeout(1500);
const r = await p.evaluate(async () => {
try {
if (!DISHES.length) await loadWeltkuecheData();
function taskFor(ingKey, heimat){
current = { dish: { id:'test-'+ingKey, heimat: heimat, ings:[ingKey], signature:{} } };
try { delete _taskCache[ingKey]; } catch(_){}
return buildTaskFor(ingKey);
}
const dEggsDE = _wkDist('oesterreich','deutschland');
const dEggsFR = _wkDist('oesterreich','frankreich');
const eier = taskFor('eier','oesterreich');
const kart = taskFor('kartoffel','oesterreich');
const milch = taskFor('milch','oesterreich');
// alle Zutaten müssen fehlerfrei bauen
let buildErr = null;
for (const k of Object.keys(ING)) {
current = { dish:{id:'x',heimat:'oesterreich',ings:[k],signature:{}} };
try { delete _taskCache[k]; buildTaskFor(k); } catch(e){ buildErr = k+': '+e.message; break; }
}
return {
distDE: Math.round(dEggsDE), distFR: Math.round(dEggsFR),
eierCorrect: eier.correct, eierChoices: eier.choices,
eierDistractors: eier.choices.length - eier.correct.length,
kartoffelCorrect: kart.correct,
milchCorrect: milch.correct,
buildErr, ingCount: Object.keys(ING).length,
};
} catch(e){ return { error:String(e), stack:e.stack }; }
});
console.log(JSON.stringify({result:r, pageErrors:errs}, null, 2));
await b.close();
})();
@@ -0,0 +1,78 @@
<!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">16 Simulationen · je Onboarding/Start + Spielverlauf · iPad 1180×820 &amp; Full HD 1920×1080 · generiert am 19.08.2026, 15:06</div>
</header>
<nav class="toc"><a href="#sim-busfahrt">🚌 Busfahrt</a><a href="#sim-energiemanager">⚡ Energiemanager</a><a href="#sim-entscheidungstag">📅 Entscheidungstag</a><a href="#sim-eu-werkstatt">🇪🇺 EU-Werkstatt</a><a href="#sim-farmer">🌾 Farmer</a><a href="#sim-fluggesellschaft">✈️ Fluggesellschaft</a><a href="#sim-fluss">🌊 Flussmanagement</a><a href="#sim-heli">🚁 Helikopter-Navigation</a><a href="#sim-logistik">🚚 Logistik</a><a href="#sim-sonnensystem">🪐 Sonnensystem</a><a href="#sim-staustufen">💧 Staustufen</a><a href="#sim-tourismusregion">🗺️ Tourismusregion</a><a href="#sim-tourismustal">🏔️ Tourismustal</a><a href="#sim-vulkan">🌋 Vulkan</a><a href="#sim-wal">🐋 Wal</a><a href="#sim-weltkueche">🍲 Welt-Küche</a></nav>
<main>
<section class="sim" id="sim-busfahrt"><h2>🚌 Busfahrt <span class="simid">busfahrt</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/busfahrt__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/busfahrt__ipad__start.png" alt="busfahrt iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/busfahrt__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/busfahrt__fullhd__start.png" alt="busfahrt Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/busfahrt__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/busfahrt__ipad__play.png" alt="busfahrt iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/busfahrt__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/busfahrt__fullhd__play.png" alt="busfahrt Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-energiemanager"><h2>⚡ Energiemanager <span class="simid">energiemanager</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/energiemanager__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__ipad__start.png" alt="energiemanager iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/energiemanager__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__fullhd__start.png" alt="energiemanager Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/energiemanager__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__ipad__play.png" alt="energiemanager iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/energiemanager__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__fullhd__play.png" alt="energiemanager Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf (2)</th><td><a href="shots/energiemanager__ipad__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__ipad__play2.png" alt="energiemanager iPad play2"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/energiemanager__fullhd__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/energiemanager__fullhd__play2.png" alt="energiemanager Full HD play2"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-entscheidungstag"><h2>📅 Entscheidungstag <span class="simid">entscheidungstag</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/entscheidungstag__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/entscheidungstag__ipad__start.png" alt="entscheidungstag iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/entscheidungstag__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/entscheidungstag__fullhd__start.png" alt="entscheidungstag Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/entscheidungstag__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/entscheidungstag__ipad__play.png" alt="entscheidungstag iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/entscheidungstag__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/entscheidungstag__fullhd__play.png" alt="entscheidungstag Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-eu-werkstatt"><h2>🇪🇺 EU-Werkstatt <span class="simid">eu-werkstatt</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/eu-werkstatt__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/eu-werkstatt__ipad__start.png" alt="eu-werkstatt iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/eu-werkstatt__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/eu-werkstatt__fullhd__start.png" alt="eu-werkstatt Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/eu-werkstatt__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/eu-werkstatt__ipad__play.png" alt="eu-werkstatt iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/eu-werkstatt__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/eu-werkstatt__fullhd__play.png" alt="eu-werkstatt Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-farmer"><h2>🌾 Farmer <span class="simid">farmer</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/farmer__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/farmer__ipad__start.png" alt="farmer iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/farmer__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/farmer__fullhd__start.png" alt="farmer Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/farmer__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/farmer__ipad__play.png" alt="farmer iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/farmer__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/farmer__fullhd__play.png" alt="farmer Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-fluggesellschaft"><h2>✈️ Fluggesellschaft <span class="simid">fluggesellschaft</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/fluggesellschaft__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluggesellschaft__ipad__start.png" alt="fluggesellschaft iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/fluggesellschaft__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluggesellschaft__fullhd__start.png" alt="fluggesellschaft Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/fluggesellschaft__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluggesellschaft__ipad__play.png" alt="fluggesellschaft iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/fluggesellschaft__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluggesellschaft__fullhd__play.png" alt="fluggesellschaft Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-fluss"><h2>🌊 Flussmanagement <span class="simid">fluss</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/fluss__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluss__ipad__start.png" alt="fluss iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/fluss__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/fluss__fullhd__start.png" alt="fluss Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-heli"><h2>🚁 Helikopter-Navigation <span class="simid">heli</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/heli__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/heli__ipad__start.png" alt="heli iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/heli__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/heli__fullhd__start.png" alt="heli Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/heli__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/heli__ipad__play.png" alt="heli iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/heli__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/heli__fullhd__play.png" alt="heli Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-logistik"><h2>🚚 Logistik <span class="simid">logistik</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/logistik__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/logistik__ipad__start.png" alt="logistik iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/logistik__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/logistik__fullhd__start.png" alt="logistik Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/logistik__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/logistik__ipad__play.png" alt="logistik iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/logistik__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/logistik__fullhd__play.png" alt="logistik Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-sonnensystem"><h2>🪐 Sonnensystem <span class="simid">sonnensystem</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/sonnensystem__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__ipad__start.png" alt="sonnensystem iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/sonnensystem__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__fullhd__start.png" alt="sonnensystem Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/sonnensystem__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__ipad__play.png" alt="sonnensystem iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/sonnensystem__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__fullhd__play.png" alt="sonnensystem Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf (2)</th><td><a href="shots/sonnensystem__ipad__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__ipad__play2.png" alt="sonnensystem iPad play2"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/sonnensystem__fullhd__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/sonnensystem__fullhd__play2.png" alt="sonnensystem Full HD play2"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-staustufen"><h2>💧 Staustufen <span class="simid">staustufen</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/staustufen__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/staustufen__ipad__start.png" alt="staustufen iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/staustufen__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/staustufen__fullhd__start.png" alt="staustufen Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/staustufen__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/staustufen__ipad__play.png" alt="staustufen iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/staustufen__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/staustufen__fullhd__play.png" alt="staustufen Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-tourismusregion"><h2>🗺️ Tourismusregion <span class="simid">tourismusregion</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/tourismusregion__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismusregion__ipad__start.png" alt="tourismusregion iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/tourismusregion__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismusregion__fullhd__start.png" alt="tourismusregion Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/tourismusregion__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismusregion__ipad__play.png" alt="tourismusregion iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/tourismusregion__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismusregion__fullhd__play.png" alt="tourismusregion Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-tourismustal"><h2>🏔️ Tourismustal <span class="simid">tourismustal</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/tourismustal__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismustal__ipad__start.png" alt="tourismustal iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/tourismustal__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismustal__fullhd__start.png" alt="tourismustal Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/tourismustal__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismustal__ipad__play.png" alt="tourismustal iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/tourismustal__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/tourismustal__fullhd__play.png" alt="tourismustal Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-vulkan"><h2>🌋 Vulkan <span class="simid">vulkan</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/vulkan__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__ipad__start.png" alt="vulkan iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/vulkan__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__fullhd__start.png" alt="vulkan Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/vulkan__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__ipad__play.png" alt="vulkan iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/vulkan__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__fullhd__play.png" alt="vulkan Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf (2)</th><td><a href="shots/vulkan__ipad__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__ipad__play2.png" alt="vulkan iPad play2"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/vulkan__fullhd__play2.png" target="_blank" class="shot"><img loading="lazy" src="shots/vulkan__fullhd__play2.png" alt="vulkan Full HD play2"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-wal"><h2>🐋 Wal <span class="simid">wal</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/wal__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/wal__ipad__start.png" alt="wal iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/wal__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/wal__fullhd__start.png" alt="wal Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/wal__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/wal__ipad__play.png" alt="wal iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/wal__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/wal__fullhd__play.png" alt="wal Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
<section class="sim" id="sim-weltkueche"><h2>🍲 Welt-Küche <span class="simid">weltkueche</span></h2><table><thead><tr><th class="screen"></th><th>iPad <span class="res">1180×820</span></th><th>Full HD <span class="res">1920×1080</span></th></tr></thead><tbody><tr><th class="screen">Onboarding / Start</th><td><a href="shots/weltkueche__ipad__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/weltkueche__ipad__start.png" alt="weltkueche iPad start"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/weltkueche__fullhd__start.png" target="_blank" class="shot"><img loading="lazy" src="shots/weltkueche__fullhd__start.png" alt="weltkueche Full HD start"><span class="cap">Full HD · 1920×1080</span></a></td></tr>
<tr><th class="screen">Spielverlauf</th><td><a href="shots/weltkueche__ipad__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/weltkueche__ipad__play.png" alt="weltkueche iPad play"><span class="cap">iPad · 1180×820</span></a></td><td><a href="shots/weltkueche__fullhd__play.png" target="_blank" class="shot"><img loading="lazy" src="shots/weltkueche__fullhd__play.png" alt="weltkueche Full HD play"><span class="cap">Full HD · 1920×1080</span></a></td></tr></tbody></table></section>
</main>
</body>
</html>
@@ -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');
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Test-Checkliste (19.08.2026)</title>
<style>
:root{
--bg:#f4f2ec; --card:#fff; --ink:#242018; --muted:#7a7266; --line:rgba(0,0,0,.08);
--accent:#4a7c8a; --accent-d:#356070; --ok:#4a8a5a; --warn:#c58a2a; --sim:#7a5ca8;
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--ink);line-height:1.5;padding-bottom:4rem}
header{position:sticky;top:0;z-index:10;background:#fff;border-bottom:1px solid var(--line);
padding:.9rem 1.2rem;display:flex;align-items:center;gap:1rem;flex-wrap:wrap;box-shadow:0 1px 6px rgba(0,0,0,.04)}
header h1{font-size:1.15rem;font-weight:900;color:var(--accent-d);letter-spacing:-.02em}
header .sub{font-size:.72rem;color:var(--muted)}
.progress{margin-left:auto;display:flex;align-items:center;gap:.6rem;font-size:.8rem;font-weight:700;color:var(--accent-d)}
.bar{width:160px;height:9px;border-radius:99px;background:#e4e0d6;overflow:hidden}
.bar > div{height:100%;background:var(--ok);width:0;transition:width .3s}
.reset{font-size:.66rem;color:var(--muted);border:1px solid var(--line);background:#fff;border-radius:6px;padding:.25rem .5rem;cursor:pointer}
.wrap{max-width:920px;margin:1.4rem auto;padding:0 1.1rem}
.intro{font-size:.86rem;color:var(--muted);margin-bottom:1.4rem;background:#fff;border:1px solid var(--line);border-radius:10px;padding:.8rem 1rem}
.intro code{background:#eee9df;padding:.05rem .35rem;border-radius:4px;font-size:.85em}
section{margin-bottom:1.8rem}
.sec-h{font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:.1em;color:var(--accent);
margin:0 0 .6rem .2rem;display:flex;align-items:center;gap:.5rem}
.sec-h .cnt{font-size:.62rem;color:var(--muted);font-weight:600;letter-spacing:.02em}
.item{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:.85rem 1rem;margin-bottom:.7rem;
box-shadow:0 1px 3px rgba(0,0,0,.03);transition:opacity .2s}
.item.done{opacity:.55}
.item-top{display:flex;gap:.7rem;align-items:flex-start}
.item-top input[type=checkbox]{width:22px;height:22px;margin-top:.1rem;accent-color:var(--ok);cursor:pointer;flex-shrink:0}
.item-title{font-weight:800;font-size:.98rem}
.item.done .item-title{text-decoration:line-through;text-decoration-color:rgba(0,0,0,.3)}
.tag{display:inline-block;font-size:.58rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;
padding:.12rem .5rem;border-radius:99px;vertical-align:middle;margin-left:.5rem}
.tag.live{background:#dceadd;color:#2f6b3a}
.tag.pending{background:#fdeecd;color:#8a6510}
.body{padding-left:calc(22px + .7rem);margin-top:.4rem;font-size:.86rem;color:#4a463d}
.body .row{margin:.28rem 0}
.body b{color:var(--ink)}
.k{display:inline-block;min-width:74px;font-weight:700;color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.03em}
a.url{color:var(--accent-d);font-family:ui-monospace,monospace;font-size:.82em;text-decoration:none;border-bottom:1px dotted var(--accent)}
a.url:hover{background:#eaf2f4}
.exp{background:#f0f6f1;border-left:3px solid var(--ok);border-radius:0 6px 6px 0;padding:.35rem .6rem;margin-top:.35rem;font-size:.82rem;color:#33613f}
.note{width:100%;margin-top:.5rem;border:1px solid var(--line);border-radius:7px;padding:.4rem .55rem;font-family:inherit;
font-size:.82rem;resize:vertical;min-height:0;height:2rem;background:#fcfbf8;color:var(--ink)}
.note::placeholder{color:#b3ac9e}
footer{text-align:center;color:var(--muted);font-size:.72rem;margin:2rem 0}
</style>
</head>
<body>
<header>
<div>
<h1>🧪 GeoGraSim — Test-Checkliste</h1>
<div class="sub">Alle Erledigungen seit den Weltküche-Lebensmitteln · Stand 19.08.2026 · lokal testen</div>
</div>
<div class="progress">
<span id="pcount">0 / 0</span>
<div class="bar"><div id="pbar"></div></div>
<button class="reset" onclick="if(confirm('Alle Haken + Notizen zurücksetzen?')){localStorage.clear();location.reload();}">reset</button>
</div>
</header>
<div class="wrap">
<div class="intro">
Basis-URL lokal: <code>http://localhost/geograsim/App/</code> · <b>Haken &amp; Notizen</b> werden im Browser gespeichert.
<span class="tag live">live</span> = auf Prod, <span class="tag pending">bereit</span> = committed, kommt mit dem nächsten Deploy.
Für Admin-Seiten zuerst über <a class="url" href="http://localhost/geograsim/App/admin.html" target="_blank">admin.html</a> anmelden (Username <b>Thomas</b> → PIN ist lokal vorausgefüllt).
</div>
<!-- ================= WELTKÜCHE (SIM) ================= -->
<section data-sec="Weltküche Simulation"></section>
<!-- ================= WELTKÜCHE (LEHRPLAN/ADMIN) ================= -->
<section data-sec="Weltküche Lehrplan &amp; Admin"></section>
<!-- ================= LEHRER-COCKPIT ================= -->
<section data-sec="Lehrer-Cockpit (teacher.html)"></section>
<!-- ================= TOURISMUSREGION ================= -->
<section data-sec="Tourismusregion"></section>
<!-- ================= LOGISTIK & PLATTFORM ================= -->
<section data-sec="Logistik &amp; Plattform"></section>
<!-- ================= ADMIN & AUTH ================= -->
<section data-sec="Admin &amp; Auth-Fundament"></section>
</div>
<footer>GeoGraSim · Test-Checkliste · lokal generiert 19.08.2026</footer>
<script>
// Alle Test-Items als Daten — daraus wird die Liste gebaut.
const BASE = 'http://localhost/geograsim/App/';
function u(path){ return BASE + path; }
const ITEMS = {
'Weltküche Simulation': [
{ id:'wk-artikel', t:'Korrekte Artikel für alle 70 Zutaten', tag:'live',
w:'der/die/das je Zutat („die Kartoffel", „der Pfeffer", „das Olivenöl") statt Platzhalter.',
url:'sims/weltkueche/game.html', steps:'Ein Gericht spielen, Zutat lösen/falsch klicken im Feedback-Satz steht der richtige Artikel.',
exp:'Nie mehr „der/die/das Kartoffel" immer „die Kartoffel" usw.' },
{ id:'wk-ukraine', t:'Verdrehter Herkunfts-Satz korrigiert (Ukraine-Fall)', tag:'live',
w:'Bei fernem Großproduzenten (z. B. Ukraine-Kartoffel im AT-Gericht) sachlich korrektes Feedback.',
url:'sims/weltkueche/game.html', steps:'Tiroler Gröstl → Kartoffel → ein fernes Land wie Ukraine anklicken.',
exp:'„…in unseren Handel kommt davon kaum etwas… nimmt man die Kartoffel aus Österreich oder einem Nachbarland." (nicht mehr „…stammt die Kartoffel von dort").' },
{ id:'wk-heimat', t:'Heimat-Banner neu formuliert', tag:'live',
w:'Erklärt „bevorzugt aus AT, aber AT deckt den Bedarf nicht allein".',
url:'sims/weltkueche/game.html', steps:'AT-Gericht, eine Heimat-Zutat lösen → grünes Banner lesen.',
exp:'„Zutaten werden zwar bevorzugt aus Österreich verwendet, aber Österreich allein deckt den Bedarf nicht ab darum zählt auch ein Nachbarland."' },
{ id:'wk-gravitation', t:'Herkunft per Handels-Gravitation (Nähe × Menge)', tag:'pending',
w:'Zweit-Lieferant = nächster großer Nachbar, nicht ferner Großproduzent.',
url:'sims/weltkueche/game.html', steps:'Ein AT-Gericht mit Eiern/Kartoffel spielen richtige Länder prüfen.',
exp:'AT-Eier → Österreich + Deutschland (NICHT Frankreich), obwohl FR mehr Eier erzeugt.' },
{ id:'wk-punkte', t:'Punktefix: „alle Falschen zuerst = 0"', tag:'pending',
w:'Strafe pro Fehlklick = 100 ÷ Anzahl Distraktoren.',
url:'sims/weltkueche/game.html', steps:'Bei einer Zutat ALLE falschen Länder zuerst anklicken, dann die richtigen.',
exp:'Ergebnis 0 Punkte. Banner zeigt die tatsächliche Strafe pro Fehlklick.' },
{ id:'wk-phase', t:'Live-State zeigt echten Fortschritt statt „forschen"', tag:'live',
w:'GGS_LIVE_STATE-Phase = „Gericht X · Zutat Y/Z".',
url:'teacher.html', steps:'Als Lehrer im Cockpit „Aktuell" ein Weltküche-Kind ansehen (oder Live-State im Sim prüfen).',
exp:'Phase/Info zeigt „Gericht 2 · Zutat 3/5" nicht dauerhaft „forschen".' },
{ id:'wk-submit', t:'Backend-Submit + Persistenz geschaffter Gerichte', tag:'live',
w:'Pro fertigem Gericht ein assessments-Eintrag; geschaffte Gerichte bleiben nach Reload.',
url:'sims/weltkueche/game.html', steps:'Ein Gericht abschließen, Seite neu laden.',
exp:'Bereits geschaffte Gerichte bleiben erkennbar; im Lehrer-Cockpit taucht das Ergebnis auf.' },
],
'Weltküche Lehrplan &amp; Admin': [
{ id:'wk-lehrplan', t:'Lehrplan-Bezug auf der Weltküche-Modulseite', tag:'live',
w:'Kompetenz „Welternährung, Konsum und globale Disparitäten" + Anker.',
url:'modul-weltkueche', steps:'Seite öffnen, zu „Lehrplan-Bezug" scrollen.',
exp:'Kompetenzen + Anker sichtbar (nicht mehr „Noch keine Kompetenzen zugeordnet").' },
{ id:'wk-filter', t:'Länder-Filter im Lehrplan-Bezug wirkt', tag:'live',
w:'Filter-Chips (Alle/AT/DE/CH/FL) blenden Fach-Anker wirklich um.',
url:'modul-weltkueche', steps:'Bei „Lehrplan-Bezug" auf Deutschland, dann Österreich klicken.',
exp:'Sichtbare Fach-Anker ändern sich je Land (vorher passierte nichts).' },
{ id:'wk-admin', t:'Admin-QA: Weltküche-Herkunftsübersicht (NEU)', tag:'pending',
w:'Pro Gericht × Zutat richtige Länder + Distraktoren; „nur Auffällige"-Filter.',
url:'admin-weltkueche.html', steps:'Als Admin einloggen, Seite öffnen. Suche/Kategorie testen, „nur Auffällige" aktivieren.',
exp:'30 Gerichte; Kartoffel/Eier → 🟢 AT+DE. „Nur Auffällige" zeigt ~5 Kandidaten zum Prüfen.' },
],
'Lehrer-Cockpit (teacher.html)': [
{ id:'tc-menu', t:'Menü „Live" → „Ergebnisse" mit Tag · Aktuell · Vergleich · Gesamt', tag:'pending',
w:'Umbenannt + 4. Unteransicht „Gesamt" (alte Ergebnis-Ansicht).',
url:'teacher.html', steps:'Als Lehrer einloggen, Klasse wählen, Tab „Ergebnisse".',
exp:'Vier Unteransichten; „Gesamt" zeigt die frühere Ergebnis-/Auswertungsansicht.' },
{ id:'tc-quali', t:'Qualitäts-Kennzahl (richtig ↔ falsch als Balken)', tag:'pending',
w:'Grafischer Balken (grün/rot) je Kind in Aktuell + Vergleich.',
url:'teacher.html', steps:'Ergebnisse → Aktuell/Vergleich bei aktivem Weltküche-Kind.',
exp:'Zweifarbiger Balken + %-Wert („blödeln vs. gut" sichtbar).' },
{ id:'tc-labels', t:'Klartext-Labels statt Jargon', tag:'pending',
w:'„Stark/Solide/Viele Fehler" statt „Kritisch/OK"; „Gerade dabei" statt „Phase".',
url:'teacher.html', steps:'Aktuell/Vergleich ansehen.',
exp:'Keine „Kritisch"-Bezeichnung mehr.' },
{ id:'tc-simlabels', t:'Sim-Bezeichner: Wort + 3-Buchstaben-Kürzel', tag:'pending',
w:'Kein „Log L1" mehr — enger Balken zeigt Kürzel (LOG), breiter das Wort.',
url:'teacher.html', steps:'Ergebnisse → Tag, Session-Balken ansehen.',
exp:'Balken tragen LOG/Logistik statt „Log L1".' },
{ id:'tc-logistik', t:'Logistik hat kuratierte Spalten (statt Roh-Dump)', tag:'pending',
w:'Stufe · Budget · Geliefert · Unterwegs · Offen · Verspätet · Fortschritt.',
url:'teacher.html', steps:'Ergebnisse → Aktuell mit aktivem Logistik-Kind.',
exp:'Sinnvolle Spalten keine simId/__activeMs/timestamp-Spalten mehr.' },
{ id:'tc-streifen', t:'Aktivitätsstreifen: Farbe je Sim, Höhe=Erfolg, Rahmen=Zustand', tag:'pending',
w:'Fertig = geschlossener Rahmen · läuft = rechts offen · Abbruch = gestrichelt + ✖.',
url:'teacher.html', steps:'Ergebnisse → Tag, mit Sessions eines Tages.',
exp:'Balken je Sim gefärbt, höher = besser; Rahmen/✖ zeigen den Zustand; Legende erklärt es.' },
{ id:'tc-assess', t:'assessment-Skip-Fix (keine leeren Ping-Zeilen mehr)', tag:'pending',
w:'„started/running"-Pings landen nicht mehr als leere Zeilen in Ergebnisse/Tag.',
url:'teacher.html', steps:'Nach etwas Spielzeit Ergebnisse/Tag ansehen.',
exp:'Nur echte Abschlüsse als Balken kein leeres Rauschen.' },
],
'Tourismusregion': [
{ id:'tr-start', t:'Startseite: keine Name/Klasscode-Felder, Klick auf Route startet', tag:'live',
w:'Felder entfernt; Klick auf eine Wegkarte startet sofort (kein „Partie starten").',
url:'sims/tourismusregion/game.html', steps:'Sim öffnen, eine Wegkarte anklicken.',
exp:'Keine Eingabefelder; Partie startet direkt bei Kartenklick.' },
{ id:'tr-ipad', t:'iPad-Responsive (Panels decken die Karte nicht mehr zu)', tag:'live',
w:'Panels mit Höhen-Deckel + eigene iPad-Landscape-Regeln. AM IPAD TESTEN!',
url:'sims/tourismusregion/game.html', steps:'Am echten iPad (Landscape) öffnen und ein Gebäude antippen.',
exp:'KPIs/Baumenü/Info-Panel überdecken die Karte nicht; Info-Panel scrollt statt zu überziehen.' },
{ id:'tr-lift', t:'Seilbahn hat eine Talstation (nicht mehr „transparent")', tag:'live',
w:'Talstation mit Dach + Umlenkrad unten am Lift.',
url:'sims/tourismusregion/game.html', steps:'Seilbahn bauen (Berg erschließen), auf die Talstation schauen.',
exp:'Solider Baukörper unten kein durchsichtiger Lift-Anfang.' },
{ id:'tr-save', t:'Abschluss: kein „Ergebnis speichern"-Button (Auto-Save)', tag:'live',
w:'Automatisches Speichern am Partie-Ende; Button entfernt.',
url:'sims/tourismusregion/game.html', steps:'Eine Partie zu Ende bringen.',
exp:'Hinweis „automatisch gespeichert", kein Speichern-Button.' },
{ id:'tr-pass', t:'Neue Karte „Passstraße"', tag:'live',
w:'Route bergauf (136 Punkte) + 41 Bauslots + Hintergrundbild.',
url:'sims/tourismusregion/game.html', steps:'Im Startbildschirm „Passstraße" wählen.',
exp:'Karte lädt, Route + Bauslots passen aufs Bild (optisch prüfen).' },
{ id:'tr-balance', t:'Balance: Umwelt beißt jetzt (Übertourismus)', tag:'pending',
w:'Zu viel Bauen + Verkehr senken die Umwelt spürbar; Umwelt-Malus wirkt.',
url:'sims/tourismusregion/game.html', steps:'Viel/dicht bauen und laufen lassen; Umwelt-KPI beobachten.',
exp:'Umwelt fällt deutlich (Richtung <45), umweltbewusste Gäste werden unzufriedener.' },
{ id:'tr-erklaer', t:'Schüler-Erklärung: Umwelt & Naturschutzgebiet', tag:'pending',
w:'Willkommenskarte + Warnkarte bei Umwelt<50 + klarere Naturschutz-Beschreibung.',
url:'sims/tourismusregion/game.html', steps:'Neu starten → Willkommenskarte lesen; Umwelt absacken lassen → Warnkarte.',
exp:'Karte erklärt Umwelt-Mechanik + Naturschutzgebiet; Warnkarte erscheint bei <50.' },
],
'Logistik &amp; Plattform': [
{ id:'lg-pause', t:'Logistik-Pause friert die Aktivzeit ein', tag:'pending',
w:'Bei Pause zählt die „Seit"-Zeit im Cockpit nicht weiter.',
url:'sims/logistik/game.html', steps:'Logistik spielen, ⏸ drücken, im Lehrer-Cockpit die Aktivzeit beobachten.',
exp:'Aktivzeit steht still, solange pausiert; läuft nach dem Fortsetzen weiter.' },
{ id:'pf-scripts', t:'Wrapper-Bug behoben (/teacher lud kein Script mehr)', tag:'live',
w:'Favicon wurde ins letzte </head> injiziert zerschoss ein JS-Popup-Script.',
url:'teacher', steps:'Die /teacher-Route (Wrapper) öffnen.',
exp:'Seite lädt sauber, keine „Invalid token"-Konsolenfehler. (teacher.html direkt lief immer.)' },
],
'Admin &amp; Auth-Fundament': [
{ id:'ad-login', t:'Lokaler Admin-Login ohne Mailversand (Dev-PIN)', tag:'pending',
w:'PIN wird lokal vorausgefüllt; Mailer-Fehler sperrt nicht mehr aus. Auf Prod bleibt 2FA-Mail Pflicht.',
url:'admin.html', steps:'Username Thomas + Passwort → „Anmelden".',
exp:'PIN-Feld ist vorausgefüllt („Dev: … vorausgefüllt"), „Bestätigen" → eingeloggt.' },
{ id:'auth-found', t:'Auth-Fundament JWT + Redis (schlafend, non-breaking)', tag:'pending',
w:'firebase/php-jwt + predis + Jwt/SessionStore + Session-Kanalisierung. AUTH_BACKEND=session.',
url:'', steps:'Kein direkter Test nötig — nur sicherstellen, dass Login/Admin normal funktionieren.',
exp:'Alles läuft wie bisher (das Fundament ist inaktiv, bis der JWT-Cutover kommt).' },
],
};
const listWrap = document.querySelector('.wrap');
document.querySelectorAll('section[data-sec]').forEach(sec => {
const name = sec.getAttribute('data-sec');
const items = ITEMS[name] || [];
let html = '<div class="sec-h">' + name + ' <span class="cnt">' + items.length + ' Punkte</span></div>';
items.forEach(it => {
const urlHtml = it.url ? '<a class="url" href="' + u(it.url) + '" target="_blank" rel="noopener">' + u(it.url) + '</a>' : '<span style="color:#b3ac9e">—</span>';
html += ''
+ '<div class="item" data-id="' + it.id + '">'
+ '<div class="item-top">'
+ '<input type="checkbox" data-check="' + it.id + '">'
+ '<div style="flex:1"><span class="item-title">' + it.t + '</span>'
+ '<span class="tag ' + it.tag + '">' + (it.tag==='live'?'live':'bereit') + '</span></div>'
+ '</div>'
+ '<div class="body">'
+ '<div class="row"><span class="k">Was</span> ' + it.w + '</div>'
+ '<div class="row"><span class="k">Öffnen</span> ' + urlHtml + '</div>'
+ '<div class="row"><span class="k">Testen</span> ' + it.steps + '</div>'
+ '<div class="exp">✓ Erwartet: ' + it.exp + '</div>'
+ '<textarea class="note" data-note="' + it.id + '" placeholder="Notiz / gefundene Abweichung…"></textarea>'
+ '</div>'
+ '</div>';
});
sec.innerHTML = html;
});
// Persistenz + Fortschritt
const checks = document.querySelectorAll('input[data-check]');
function refresh(){
let done = 0;
checks.forEach(c => { if(c.checked){ done++; c.closest('.item').classList.add('done'); } else c.closest('.item').classList.remove('done'); });
document.getElementById('pcount').textContent = done + ' / ' + checks.length;
document.getElementById('pbar').style.width = (checks.length? (done/checks.length*100):0) + '%';
}
checks.forEach(c => {
const key = 'chk_' + c.getAttribute('data-check');
c.checked = localStorage.getItem(key) === '1';
c.addEventListener('change', () => { localStorage.setItem(key, c.checked?'1':'0'); refresh(); });
});
document.querySelectorAll('textarea[data-note]').forEach(t => {
const key = 'note_' + t.getAttribute('data-note');
t.value = localStorage.getItem(key) || '';
t.addEventListener('input', () => localStorage.setItem(key, t.value));
});
refresh();
</script>
</body>
</html>
+1 -1
View File
@@ -173,7 +173,7 @@ var SIMS = [
start_population:{min:1000,max:20000,def:6000,label:'Start-Bevölkerung'}, start_population:{min:1000,max:20000,def:6000,label:'Start-Bevölkerung'},
income_per_10k:{min:50,max:500,def:220,label:'Einnahmen pro 10.000 Bürger'}, income_per_10k:{min:50,max:500,def:220,label:'Einnahmen pro 10.000 Bürger'},
budget_loss:{min:-2000,max:0,def:-500,label:'Pleite-Grenze (Mio €, negativ)'}, budget_loss:{min:-2000,max:0,def:-500,label:'Pleite-Grenze (Mio €, negativ)'},
event_frequency:{min:0,max:1,def:0.3,step:0.1,label:'Ereignis-Häufigkeit (01)'}, event_frequency:{min:0,max:1,def:0.3,step:0.1,label:'Ereignis-Häufigkeit (0 1)'},
time_limit:{min:30,max:150,def:75,label:'Simulationsdauer (Runden)'} time_limit:{min:30,max:150,def:75,label:'Simulationsdauer (Runden)'}
}}, }},
{id:'sim-05', name:'Klimawächter 2D (V1)', icon:'🌡️', img:'assets/img/card-climate.webp', {id:'sim-05', name:'Klimawächter 2D (V1)', icon:'🌡️', img:'assets/img/card-climate.webp',
+1 -1
View File
@@ -9,7 +9,7 @@
* Icons sind sauber generierte Emblems (Flat-Scandinavian, keine Emojis) unter * Icons sind sauber generierte Emblems (Flat-Scandinavian, keine Emojis) unter
* assets/img/badges/. Gesperrte Abzeichen bleiben sichtbar (Zielvorstellung), * assets/img/badges/. Gesperrte Abzeichen bleiben sichtbar (Zielvorstellung),
* die Beschreibung ist das Ziel. Sterne laufen parallel; ggsErfolgToStars() * die Beschreibung ist das Ziel. Sterne laufen parallel; ggsErfolgToStars()
* bildet den normierten Erfolg (0100) auf 05 Sterne ab. * bildet den normierten Erfolg (0 100) auf 0 5 Sterne ab.
* *
* progressObj (pro Sim): { plays, best_stars, level } * progressObj (pro Sim): { plays, best_stars, level }
* ==========================================================================*/ * ==========================================================================*/
+1 -1
View File
@@ -53,7 +53,7 @@
const MONSOON = [5, 6, 7, 8]; const MONSOON = [5, 6, 7, 8];
// Regen pro Monat in mm. // Regen pro Monat in mm.
// Normal: Mumbai (Colaba) 19812010, India Meteorological Department. // Normal: Mumbai (Colaba) 1981 2010, India Meteorological Department.
const NORMAL = [1, 0, 1, 0, 16, 506, 769, 472, 356, 82, 9, 3]; const NORMAL = [1, 0, 1, 0, 16, 506, 769, 472, 356, 82, 9, 3];
// Schwacher Monsun (Dürre-Jahr): Spitzen-Monate stark reduziert. // Schwacher Monsun (Dürre-Jahr): Spitzen-Monate stark reduziert.
const WEAK = [1, 0, 1, 0, 10, 280, 420, 240, 180, 60, 6, 2]; const WEAK = [1, 0, 1, 0, 10, 280, 420, 240, 180, 60, 6, 2];
@@ -322,7 +322,7 @@
svgEl('text', { x: 450, y: 22, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg).textContent = 'Zusammengelegt'; svgEl('text', { x: 450, y: 22, 'text-anchor': 'middle', 'font-size': '12', 'font-weight': '800', fill: '#1f4e5a' }, svg).textContent = 'Zusammengelegt';
const mergedG = svgEl('g', {}, svg); const mergedG = svgEl('g', {}, svg);
// Balken: Stückkosten je Gast (Skala 050 €, Höhe 110 px) // Balken: Stückkosten je Gast (Skala 0 50 €, Höhe 110 px)
const BAR_BASE = 240, BAR_MAXH = 110, SCALE = 50; const BAR_BASE = 240, BAR_MAXH = 110, SCALE = 50;
svgEl('line', { x1: 30, y1: BAR_BASE, x2: 570, y2: BAR_BASE, stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg); svgEl('line', { x1: 30, y1: BAR_BASE, x2: 570, y2: BAR_BASE, stroke: '#b5c9cf', 'stroke-width': '1.5' }, svg);
// Referenzbalken links: 45 € (statisch) // Referenzbalken links: 45 € (statisch)
@@ -500,9 +500,9 @@
}; };
// Farbzonen // Farbzonen
[ [
{ a1: -80, a2: -40, c: '#c85c4a' }, // 49,8049,90 { a1: -80, a2: -40, c: '#c85c4a' }, // 49,80 49,90
{ a1: -40, a2: -20, c: '#e8833a' }, // 49,9049,95 { a1: -40, a2: -20, c: '#e8833a' }, // 49,90 49,95
{ a1: -20, a2: 20, c: '#5a8a5e' }, // 49,9550,05 { a1: -20, a2: 20, c: '#5a8a5e' }, // 49,95 50,05
{ a1: 20, a2: 40, c: '#e8833a' }, { a1: 20, a2: 40, c: '#e8833a' },
{ a1: 40, a2: 80, c: '#c85c4a' } { a1: 40, a2: 80, c: '#c85c4a' }
].forEach(z => svgEl('path', { d: arcPath(z.a1, z.a2, R), stroke: z.c, 'stroke-width': '20', fill: 'none', 'stroke-linecap': 'butt', opacity: '0.9' }, svg)); ].forEach(z => svgEl('path', { d: arcPath(z.a1, z.a2, R), stroke: z.c, 'stroke-width': '20', fill: 'none', 'stroke-linecap': 'butt', opacity: '0.9' }, svg));
@@ -599,7 +599,7 @@
<div class="ge-gh-pill" id="ge-ps-status" data-state="now">Das Unterbecken ist voll — pumpe Wasser hinauf!</div> <div class="ge-gh-pill" id="ge-ps-status" data-state="now">Das Unterbecken ist voll — pumpe Wasser hinauf!</div>
</div> </div>
<p class="ge-note">Pumpspeicherkraftwerke sind bis heute der einzige großtechnische <p class="ge-note">Pumpspeicherkraftwerke sind bis heute der einzige großtechnische
Stromspeicher: Aus 100 kWh Pumpstrom kommen rund 7580 kWh wieder heraus Stromspeicher: Aus 100 kWh Pumpstrom kommen rund 75 80 kWh wieder heraus
(Gesamtwirkungsgrad, hier gerechnet mit 78 %). Quellen: VERBUND (Malta- und (Gesamtwirkungsgrad, hier gerechnet mit 78 %). Quellen: VERBUND (Malta- und
Kaprun-Kraftwerke) · Fraunhofer ISE.</p> Kaprun-Kraftwerke) · Fraunhofer ISE.</p>
</div> </div>
+7 -7
View File
@@ -119,7 +119,7 @@
<li><a href="#mechanik">Mechanik-Erkenntnisse aus Trace-Tests</a></li> <li><a href="#mechanik">Mechanik-Erkenntnisse aus Trace-Tests</a></li>
<li><a href="#ui">UI / Interaktion</a></li> <li><a href="#ui">UI / Interaktion</a></li>
<li><a href="#visual">Visualisierung (2D / 3D)</a></li> <li><a href="#visual">Visualisierung (2D / 3D)</a></li>
<li><a href="#erklaerungen">Erklärungen für Kinder 1014</a></li> <li><a href="#erklaerungen">Erklärungen für Kinder 10 14</a></li>
<li><a href="#folgen">Weitere Folgen des Klimawandels</a></li> <li><a href="#folgen">Weitere Folgen des Klimawandels</a></li>
<li><a href="#offen">Offene Punkte & nächste Schritte</a></li> <li><a href="#offen">Offene Punkte & nächste Schritte</a></li>
</ol> </ol>
@@ -129,7 +129,7 @@
<section id="ziel"> <section id="ziel">
<h2>1. Didaktisches Ziel und Zielgruppe</h2> <h2>1. Didaktisches Ziel und Zielgruppe</h2>
<p>Der <strong>Klimawächter</strong> ist kein Spiel um einen high score. Er soll Schülerinnen und Schülern der 1.4. Klasse Sekundarstufe I (ca. <strong>1014 Jahre</strong>) vermitteln, dass:</p> <p>Der <strong>Klimawächter</strong> ist kein Spiel um einen high score. Er soll Schülerinnen und Schülern der 1.4. Klasse Sekundarstufe I (ca. <strong>10 14 Jahre</strong>) vermitteln, dass:</p>
<ul> <ul>
<li>der Klimawandel träge ist — Folgen kommen verzögert, Entscheidungen wirken erst in Jahren</li> <li>der Klimawandel träge ist — Folgen kommen verzögert, Entscheidungen wirken erst in Jahren</li>
<li>es keine Wunderlösung gibt — man muss mehrere Strategien kombinieren</li> <li>es keine Wunderlösung gibt — man muss mehrere Strategien kombinieren</li>
@@ -202,7 +202,7 @@
<li>Deiche allein retten kurzfristig Häuser, aber nicht das Klima.</li> <li>Deiche allein retten kurzfristig Häuser, aber nicht das Klima.</li>
<li>Aufforsten ist wichtig, aber als Einzelmaßnahme zu langsam.</li> <li>Aufforsten ist wichtig, aber als Einzelmaßnahme zu langsam.</li>
<li>Wind ist stärker als Solar, aber wegen Wartungskosten nicht immer die beste Wahl.</li> <li>Wind ist stärker als Solar, aber wegen Wartungskosten nicht immer die beste Wahl.</li>
<li>Das Klima hat eine Trägheit von ca. 8 %/Jahr für Temperatur und 68 %/Jahr für Meeresspiegel.</li> <li>Das Klima hat eine Trägheit von ca. 8 %/Jahr für Temperatur und 6 8 %/Jahr für Meeresspiegel.</li>
</ul> </ul>
</div> </div>
@@ -226,7 +226,7 @@
<div class="card green"> <div class="card green">
<h3><span class="tag done">done</span> Bürger-Beschwerden mit Choices</h3> <h3><span class="tag done">done</span> Bürger-Beschwerden mit Choices</h3>
<p>Modaler Dialog mit Avatar, Message und 23 Choice-Karten. Jede Wahl hat echte Konsequenzen. Spiel pausiert automatisch bis der Spieler entscheidet. Bibliothek: 7 Events über die 75 Jahre verteilt (Fischerin, Forscherin, Bauer, Hotel, Jugend-Streik, Industrie, Bergdorf).</p> <p>Modaler Dialog mit Avatar, Message und 2 3 Choice-Karten. Jede Wahl hat echte Konsequenzen. Spiel pausiert automatisch bis der Spieler entscheidet. Bibliothek: 7 Events über die 75 Jahre verteilt (Fischerin, Forscherin, Bauer, Hotel, Jugend-Streik, Industrie, Bergdorf).</p>
</div> </div>
<div class="card green"> <div class="card green">
@@ -336,7 +336,7 @@
<div class="card sand"> <div class="card sand">
<h3><span class="tag todo">todo</span> Krisen-Mini-Spiele</h3> <h3><span class="tag todo">todo</span> Krisen-Mini-Spiele</h3>
<p>Bei Sturmflut, Hitzewelle, Murenabgang ein kurzes (1020 s) Klick-Mini-Spiel. Erfolg mildert den Schaden.</p> <p>Bei Sturmflut, Hitzewelle, Murenabgang ein kurzes (10 20 s) Klick-Mini-Spiel. Erfolg mildert den Schaden.</p>
</div> </div>
</section> </section>
@@ -394,9 +394,9 @@
<!-- ====================================================== --> <!-- ====================================================== -->
<section id="erklaerungen"> <section id="erklaerungen">
<h2>6. Erklärungen für Kinder 1014</h2> <h2>6. Erklärungen für Kinder 10 14</h2>
<p>Das neue <code>info-overlay.ts</code>-Modul bietet eine Bibliothek kindgerechter Erklärtexte. Jeder Graph und jedes wichtige Event hat einen (i)-Button, der ein Overlay öffnet. Zielgruppe: 1014 Jahre, einfache Sätze, max. 3 Absätze plus "Merksatz".</p> <p>Das neue <code>info-overlay.ts</code>-Modul bietet eine Bibliothek kindgerechter Erklärtexte. Jeder Graph und jedes wichtige Event hat einen (i)-Button, der ein Overlay öffnet. Zielgruppe: 10 14 Jahre, einfache Sätze, max. 3 Absätze plus "Merksatz".</p>
<h3>Bisher erklärte Begriffe</h3> <h3>Bisher erklärte Begriffe</h3>
<ul> <ul>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -491,7 +491,7 @@
<h2>Was den Alltag schwer macht</h2> <h2>Was den Alltag schwer macht</h2>
<h3>Stundenknappheit &amp; Stofffülle</h3> <h3>Stundenknappheit &amp; Stofffülle</h3>
<ul> <ul>
<li>12 Wo.-Std. — der ganze Lehrplan muss durchgenommen werden</li> <li>1 2 Wo.-Std. — der ganze Lehrplan muss durchgenommen werden</li>
<li>Tendenz zur Oberflächlichkeit, Verlust von Tiefe</li> <li>Tendenz zur Oberflächlichkeit, Verlust von Tiefe</li>
</ul> </ul>
<h3>Lehrkräftebildung</h3> <h3>Lehrkräftebildung</h3>
@@ -428,7 +428,7 @@
<li><strong>Lernende ↔ Lehrperson:</strong> Reflexion, Bedeutungsaushandlung, soziales Lernen</li> <li><strong>Lernende ↔ Lehrperson:</strong> Reflexion, Bedeutungsaushandlung, soziales Lernen</li>
<li><strong>Lehrperson ↔ Simulation:</strong> Auswahl, Vorbereitung, Didaktisierung — Inszenierung des Lernanlasses</li> <li><strong>Lehrperson ↔ Simulation:</strong> Auswahl, Vorbereitung, Didaktisierung — Inszenierung des Lernanlasses</li>
</ul> </ul>
<p>Studienbefund: Bei kompetenter Begleitung steigt die Effektstärke um den Faktor 1,52.</p> <p>Studienbefund: Bei kompetenter Begleitung steigt die Effektstärke um den Faktor 1,5 2.</p>
</section> </section>
<!-- KAPITEL 6 --> <!-- KAPITEL 6 -->
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More