Compare commits
248 Commits
0a2ebf46b4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dba841830 | |||
| ff889865e1 | |||
| eea5688c76 | |||
| 50c72fa8d8 | |||
| 96a73e22b2 | |||
| 7691649f37 | |||
| bb4d3b4cb6 | |||
| b109ebb099 | |||
| 93c742b381 | |||
| ff91c489f7 | |||
| 085b0b6aa8 | |||
| 637c058507 | |||
| 5b6c25bcd9 | |||
| 77a3e2f16c | |||
| 3f29ad0533 | |||
| 248a46f829 | |||
| 2e9ad6a83f | |||
| 6009eabca1 | |||
| 41731254b1 | |||
| 1dffef8200 | |||
| 496fc59c7b | |||
| 8f89613eee | |||
| 6135dd2648 | |||
| aab9b35781 | |||
| 014eb418a6 | |||
| e901f5addb | |||
| abc8bf5149 | |||
| 54772180a2 | |||
| 4cbdb6bbb1 | |||
| b835475ef7 | |||
| ec746f250f | |||
| 6c2990aa5e | |||
| 7b1bdb8d1b | |||
| c913522a77 | |||
| 502f271f36 | |||
| 362aaba883 | |||
| 88e3c09e5c | |||
| 539f6b012e | |||
| 7e1a7c5228 | |||
| ce7bff3184 | |||
| 89d4d8587e | |||
| a4e57d7aa6 | |||
| 3bc0f418be | |||
| 0a6fa2172f | |||
| c79ef691ad | |||
| bf579310ed | |||
| 691387c4d8 | |||
| 62e903a5ec | |||
| 11c48817de | |||
| 8ec005f889 | |||
| 7de71b0e83 | |||
| b77f4ed912 | |||
| 868cdda276 | |||
| 9a1ee0fae1 | |||
| 1e1c1238bd | |||
| de104e915a | |||
| 193c6dcc8f | |||
| 74736a33aa | |||
| f7bf0f0901 | |||
| cc83493055 | |||
| abbc0580c5 | |||
| be7d9fce1a | |||
| da1e6e6e87 | |||
| bc468272ba | |||
| 4976582400 | |||
| bf5b026b46 | |||
| 77023ff7a3 | |||
| aaa40e9f1a | |||
| 0d9481cae2 | |||
| 0d765961af | |||
| 7986bb2eb6 | |||
| 6dfcf4d57f | |||
| 0532327e6a | |||
| 5d79fa3c3a | |||
| c9eb0e7764 | |||
| 3c28794012 | |||
| 5633d3f9f9 | |||
| e4b877de4b | |||
| 550a35eae8 | |||
| 47adf0497d | |||
| 48971b4588 | |||
| 2386756a20 | |||
| 0c4fb84606 | |||
| 2160a99896 | |||
| 8cc48b1bc6 | |||
| eb67cbc79f | |||
| 3c856ff5ce | |||
| 64a9a62157 | |||
| ad8c6b7d7e | |||
| 71226581fa | |||
| 540b27a2bd | |||
| 5635fa26b4 | |||
| 4e38b27c64 | |||
| 85ee58c56d | |||
| fecd8bb9c9 | |||
| 9c99359d9c | |||
| b32097b617 | |||
| 257b5e5099 | |||
| 532b5471ec | |||
| ff751f3a05 | |||
| 714abc026a | |||
| 8c316f22e8 | |||
| 8ffc43d7b2 | |||
| 9bc3ca7b26 | |||
| 13f14c26a8 | |||
| d7dd3f9de5 | |||
| 8977901e89 | |||
| f934a7f9e4 | |||
| 0c2225594b | |||
| 7f997bff84 | |||
| 423490260d | |||
| b6b6c6ccce | |||
| a68c58ac9e | |||
| c3e61f4da7 | |||
| b353f25b07 | |||
| 5fbbf506b5 | |||
| d4c7fd5009 | |||
| bf2a4cd0d1 | |||
| 4c0183f2e0 | |||
| 0c4064bb58 | |||
| 5d3cd1bdcf | |||
| 5b0505b140 | |||
| c950f954d1 | |||
| a568f81b0a | |||
| 182a87978e | |||
| 5b91a5bf84 | |||
| 0555a2d245 | |||
| 5f603cb193 | |||
| 913f161502 | |||
| 665b43e069 | |||
| d9f9f901a1 | |||
| e23ebd960e | |||
| c1f90e255d | |||
| 37f6238d05 | |||
| 5d94572569 | |||
| db4b75c5e6 | |||
| 9937fbf62a | |||
| 5655af81bf | |||
| 4bfb6539cb | |||
| f6df2eb79f | |||
| ec102b7c36 | |||
| 9ec7ad67f1 | |||
| b9bcbf9198 | |||
| 2bdaa0f6cc | |||
| b0fe3fdf86 | |||
| ad627331f0 | |||
| 75511630c8 | |||
| 81ba268ea8 | |||
| 1950542ae5 | |||
| eba9e54c70 | |||
| 8b1088a070 | |||
| fb44ec7afe | |||
| 2984d93e7e | |||
| a3c5753cd7 | |||
| 031ca87daf | |||
| 405fcc2b69 | |||
| 7f774c7b2a | |||
| bbef4856d0 | |||
| 3a2e65ebd9 | |||
| 97144495a1 | |||
| 5f6abeeb42 | |||
| ebdd1ce438 | |||
| 07010f2302 | |||
| 05ace93318 | |||
| 25faca0819 | |||
| 532956fd7d | |||
| 52e9fafd92 | |||
| e750402799 | |||
| 56d8a3217b | |||
| bfc90b78de | |||
| ff285e9af3 | |||
| d1a01915af | |||
| 5bf8f0fe03 | |||
| c83da66325 | |||
| 2db99c37ee | |||
| 4cd6302f0a | |||
| 1f00d83232 | |||
| 84386e93d0 | |||
| 504b0098a6 | |||
| 65ca8692cb | |||
| 3d1b8e49a3 | |||
| 1c2fa2e1b4 | |||
| 15ac35a7ad | |||
| 69888445c7 | |||
| cb16c3742e | |||
| 7915512195 | |||
| e7453d9072 | |||
| 1c5356422c | |||
| 7d3b3693db | |||
| 305e3a62f8 | |||
| 0357cd188e | |||
| 6e3de69b3c | |||
| c64cfda66d | |||
| e65e938b7f | |||
| c5307017e4 | |||
| 685b4700f8 | |||
| 283780994b | |||
| 108053d5da | |||
| 954461c1b9 | |||
| f8e77910ea | |||
| f9a5c84ab9 | |||
| 6e23265dbc | |||
| e06681d522 | |||
| c00b535b1f | |||
| 237c3c4d82 | |||
| 1f9d52bd2b | |||
| 2e50aaf49f | |||
| aae0c82c64 | |||
| 0febe51685 | |||
| c4276083a2 | |||
| 78a1cdca24 | |||
| f91f3f26de | |||
| a3d81b8983 | |||
| 4e3ff6447f | |||
| f282c79a25 | |||
| e9870843e8 | |||
| e7e0f922c7 | |||
| dbfb5be5a0 | |||
| 31f1744730 | |||
| 7d4e97c0bc | |||
| 3d85b7a13c | |||
| cf0afc5c02 | |||
| 6ca643f628 | |||
| d60bc2caff | |||
| 8047778a5b | |||
| 778634fab6 | |||
| 41ecb4d5fe | |||
| ecc70a333d | |||
| acde652ee7 | |||
| 24ad893dd5 | |||
| cff7c11853 | |||
| 6d00a762bc | |||
| b90abb31a4 | |||
| 20974b8b3b | |||
| 250532f9a8 | |||
| 272c5a7169 | |||
| cfdfa4bb28 | |||
| d195b085d9 | |||
| 328d542fdc | |||
| 55a83d5daf | |||
| cb60a8c16d | |||
| 32793d0bca | |||
| 7e3158aede | |||
| 2591f37191 | |||
| 727e82c8fe | |||
| 16c562ed1d | |||
| 2b4a92e614 | |||
| 7453bebbc3 |
@@ -0,0 +1,42 @@
|
||||
# Relevante Infos für Lehrpersonen — Zusammenfassung
|
||||
|
||||
Stand 2026-08-19. Konsolidiert, was wir zum Lehrer-Cockpit festgehalten und gebaut haben.
|
||||
|
||||
## Die Kernfrage der Lehrperson
|
||||
> „Wer ist gerade **engagiert und macht es gut** — und wer **blödelt herum oder hängt fest**?"
|
||||
|
||||
Alles im Cockpit dient dieser Frage. Kein Zahlen-Dump, sondern wenige aussagekräftige Signale.
|
||||
|
||||
## Die 6 relevanten Signale
|
||||
| Signal | Was es beantwortet | Woher |
|
||||
|---|---|---|
|
||||
| **Fortschritt** | Wie weit durch die Sim? (%) | `progressPct` / abgeleitet je Sim |
|
||||
| **Qualität** | Blödeln vs. gut — Verhältnis **richtige ↔ falsche** Klicks (grafischer Balken) | `correctClicks`/`wrongClicks` (bisher Weltküche; andere ziehen nach) |
|
||||
| **Fleiß** | Wie viel im **gewählten Zeitraum** geschafft? (z. B. Gerichte, gelieferte Aufträge) | assessments im Intervall |
|
||||
| **Lage** | Klartext: **Stark / Solide / Viele Fehler** (kein „Kritisch"-Jargon) | `_liveMood` je Sim |
|
||||
| **Aktivität** | aktiv / idle / **pausiert** — war das Kind die ganze Stunde dabei? | Heartbeat `__activeMs`/`__idleMs`/`__paused` |
|
||||
| **Hilfe-Bedarf** | „🆘 braucht evtl. Hilfe" | `_liveNeedsHelp` je Sim (z. B. Budget < 0, viele Fehlversuche) |
|
||||
|
||||
## Die Ansichten (Menü „Ergebnisse")
|
||||
- **📅 Tag** — Zeitstrahl aller Sessions des Tages je Kind, eingefärbt nach Erfolg. Balken tragen **Sim-Kürzel/Wort** (LOG, Logistik …), Klick öffnet Detail.
|
||||
- **🔴 Aktuell** — Live-Tabelle je Sim mit **kuratierten** Spalten (LIVE_PRIMARY_FIELDS), Qualitäts-Balken, Fortschritt, Lage. Aktualisiert alle 4 s.
|
||||
- **📊 Vergleich** — aktive Kinder EINER Sim nebeneinander (Fortschritt + Qualität) → direkter „wer gut / wer nicht".
|
||||
- **📚 Gesamt** — Aggregat (Fleiß-Zeit, Modul-Karten, Matrix) über den gewählten Zeitraum.
|
||||
|
||||
## Design-Prinzipien (verbindlich)
|
||||
1. **Kuratierte Spalten je Sim** — nie Roh-State dumpen, nie interne `__`-Heartbeat-Keys zeigen. Fehlt ein Schema → gefilterter Fallback (ohne Rausch-Felder).
|
||||
2. **Klartext statt Jargon** — „Stark/Solide/Viele Fehler" statt „Kritisch/OK"; „Gerade dabei" statt „Phase".
|
||||
3. **Ehrliche Zeit** — Aktivzeit zählt **nicht** bei Spiel-Pause und **nicht** bei Tab-weg/idle (>5 min). Sonst wirkt ein pausiertes Kind fälschlich „fleißig".
|
||||
4. **Ein Wort + 3-Buchstaben-Kürzel je Sim** (SIM_LABELS) — enge Balken nutzen das Kürzel, breite das Wort.
|
||||
5. **Datenschutz** — nur was die Lehrperson pädagogisch braucht; verdeckte Antworten (z. B. Kofferdetektiv-Ziel) bleiben verdeckt, solange das Kind sucht.
|
||||
|
||||
## Was jede Sim liefern sollte (GGS_LIVE_STATE)
|
||||
- `progressPct` (0–100), ein Fortschritts-Zähler (z. B. `completed`/`total`),
|
||||
- **`correctClicks`/`wrongClicks`** (für die Qualitäts-Kennzahl — Ziel: alle Sims),
|
||||
- optional `health` (good/ok/struggling) für die Lage,
|
||||
- Heartbeat-Felder (`__activeMs` etc.) kommen automatisch aus live-client.js; bei Pause `window.__GGS_LIVE__.setPaused(true)` rufen.
|
||||
|
||||
## Offen (siehe project_backlog_2026-08-18 / project_lehrer_observability_overhaul)
|
||||
- **Aktivitäts-/Qualitätsstreifen** pro Kind über die Stunde (grün→rot, aktiv→inaktiv & bullshit→ernsthaft) — braucht Zeitreihen-Aktivitätsdaten.
|
||||
- Weltküche-**Gesamt-Kachel** (Gerichte · Ø Punkte · Fehlquote).
|
||||
- **Andere Sims** mit `correctClicks`/`wrongClicks` nachrüsten (bisher nur Weltküche).
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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 & 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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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();
|
||||
})();
|
||||
@@ -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,48 @@
|
||||
# Schulbuch-Konzept (Auftrag Thomas, 2026-07-12)
|
||||
|
||||
Hinter jeder Sim entsteht ein Schulbuch-Kapitel: Sachinhalte lehrplankonform,
|
||||
mit Links (Glossar-Tooltips, Sim, Quellen), präsentierbar vor/zwischen/nach dem
|
||||
Arbeiten mit der Simulation. Zwei Versionen aus EINER Quelle.
|
||||
|
||||
## Architektur
|
||||
|
||||
- **Routen:** `/buch` (Übersicht aller Kapitel) · `/buch-<sim>` (Kapitel).
|
||||
Lehrer-Version über `?lehrer=1` (Toggle-Link auf der Seite).
|
||||
TODO später: Lehrer-Version hinter Teacher-Session (wie Marktplatz-Plan).
|
||||
- **Dateien:** `App/pages/buch.php` + `App/pages/buch-<sim>.php`.
|
||||
Erste Iteration self-contained (eigenes CSS im File, Schulbuch-Look),
|
||||
bei Kapitel 3+ ggf. gemeinsames Partial extrahieren.
|
||||
- **Fotos (echte Bilder von Thomas):** Slots via PHP-Helper `buchBild($slug,
|
||||
$vorschlag)`. Sucht `App/assets/img/buch/<sim>/<slug>.{jpg,webp,png}` —
|
||||
Datei da → Bild erscheint automatisch; sonst Platzhalter-Kasten
|
||||
„📷 FOTO GESUCHT: <konkreter Vorschlag>". Thomas legt Dateien einfach ab.
|
||||
- **Glossar:** `data-glossar`-Spans + plattformweite glossar-tooltip.js.
|
||||
- **Druck:** @media print → A4, Seitenumbrüche an Kapitel-Seiten, Nav/Toggle raus.
|
||||
- **Quellen:** Jede Daten-Box nummeriert [1]…, Quellenverzeichnis am Ende
|
||||
(aus QUELLEN.md der Sim übernommen).
|
||||
|
||||
## Baukasten (Inhaltselemente)
|
||||
|
||||
- Kapitel-Opener: großes Bild, Leitfrage, Lernziele-Box, Lehrplan-Chips (D-A-CH)
|
||||
- „Seite"-Sektionen: Sachtext (~250–400 Wörter), Merke-Kasten, Daten-Kasten,
|
||||
Begriffs-Chips (Glossar), Foto
|
||||
- 🎮 Sim-Kasten: „So ist es in der Simulation" — erklärt die Mechanik & ihren
|
||||
Realitätsbezug (Schülerversion = vollständige Mechanik-Referenz)
|
||||
- ✏️ Aufgaben-Kasten: vor / während / nach dem Arbeiten, gestuft
|
||||
- 💡 Lehrer-Kästen (nur ?lehrer=1): Verlaufsvorschlag (Doppelstunde),
|
||||
didaktischer Kommentar je Seite, Stolpersteine (z. B. „Panzer-Rush endet in
|
||||
Bank-Liquidation — bewusst so"), Differenzierung, Erwartungshorizont,
|
||||
Cockpit-Tipps (welche Live-Spalten/Ergebnisse ansehen)
|
||||
|
||||
## Kapitel-Fahrplan
|
||||
|
||||
1. **Tourismustal** (gebaut 2026-07-12, beide Versionen) — Pilot
|
||||
2. Tourismusregion (Besucherlenkung/Mobilität)
|
||||
3. danach je Bestands-Sim (Busfahrt, Heli, Klima, …) nach Priorität Thomas
|
||||
|
||||
## Foto-Workflow
|
||||
|
||||
Kapitel nennt pro Slot einen konkreten Vorschlag. Thomas fotografiert real
|
||||
(Vorarlberg/Arlberg-Umfeld vorhanden), legt ab unter
|
||||
`App/assets/img/buch/<sim>/<slug>.jpg` → erscheint automatisch. Credits-Zeile
|
||||
optional via Dateiname egal; Bildunterschrift steht im Kapitel.
|
||||
@@ -0,0 +1,73 @@
|
||||
// Headless-Balance-Messung (temporär). node sim-harness.mjs
|
||||
import { createState, place, upgrade, callNextWave, tick, GOALS } from '../../App/sims/tourismusregion/js/model.js';
|
||||
import { BUILDINGS, GAME_LEN } from '../../App/sims/tourismusregion/js/data.js';
|
||||
|
||||
const ALL_TYPES = Object.keys(BUILDINGS);
|
||||
function upgradeAllMax(s) {
|
||||
for (const b of s.buildings) {
|
||||
const def = BUILDINGS[b.type];
|
||||
let guard = 0;
|
||||
while (b.level < def.cost.length && s.budget >= def.cost[b.level] && guard++ < 6) {
|
||||
const r = upgrade(s, b);
|
||||
if (!r || !r.ok) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function run(strategy, mapId = 'talschleife') {
|
||||
const s = createState({ mapId, seed: 12345 });
|
||||
const dt = 0.5;
|
||||
let t = 0;
|
||||
|
||||
function tryBuildAll() {
|
||||
for (let i = 0; i < s.slots.length; i++) {
|
||||
const slot = s.slots[i];
|
||||
if (slot.building || slot.blocked) continue;
|
||||
for (const type of ALL_TYPES) {
|
||||
const def = BUILDINGS[type];
|
||||
if (!def || s.budget < def.cost[0]) continue;
|
||||
const r = place(s, type, i);
|
||||
if (r && r.ok) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
function buildOneCafe() {
|
||||
// Minimal-Strategie: genau EIN Café (Thomas' „5-Sterne-Café macht alles grün")
|
||||
if (s.buildings.length) return;
|
||||
for (let i = 0; i < s.slots.length; i++) {
|
||||
if (s.slots[i].building || s.slots[i].blocked) continue;
|
||||
const r = place(s, 'cafe', i);
|
||||
if (r && r.ok) return;
|
||||
}
|
||||
}
|
||||
|
||||
let guard = 0;
|
||||
while (t < GAME_LEN && s.phase < 8 && guard++ < 2000) {
|
||||
if (strategy === 'buildAll' || strategy === 'maxedAll') tryBuildAll();
|
||||
if (strategy === 'maxedAll') upgradeAllMax(s);
|
||||
if (strategy === 'oneCafe') buildOneCafe();
|
||||
try { callNextWave(s); } catch (_) {}
|
||||
for (let k = 0; k < 40 && t < GAME_LEN; k++) { tick(s, dt); t += dt; }
|
||||
}
|
||||
if (strategy === 'maxedAll') upgradeAllMax(s);
|
||||
// Restzeit auslaufen lassen
|
||||
while (t < GAME_LEN && guard++ < 6000) { tick(s, dt); t += dt; }
|
||||
|
||||
const stars = s.stats.all.count ? s.stats.all.stars / s.stats.all.count : 0;
|
||||
return {
|
||||
strategy,
|
||||
env: Math.round(s.env),
|
||||
avgStars: +stars.toFixed(2),
|
||||
visitorsExited: s.stats.all.count,
|
||||
budget: Math.round(s.budget),
|
||||
buildings: s.buildings.length,
|
||||
traffic: Math.round(s.traffic || 0),
|
||||
goalsMet: GOALS.filter(g => g.check(s)).length + '/' + GOALS.length,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('=== Tourismusregion Balance-Messung (talschleife) ===');
|
||||
for (const strat of ['maxedAll', 'buildAll', 'oneCafe']) {
|
||||
const r = run(strat);
|
||||
console.log(`${strat.padEnd(9)} → env ${String(r.env).padStart(3)} · Ø${r.avgStars}★ · ${r.buildings} Geb. · Verkehr ${r.traffic} · Ziele ${r.goalsMet} · Budget ${r.budget}`);
|
||||
}
|
||||
@@ -23,3 +23,4 @@ secret_keys.txt
|
||||
**/.humaninput/
|
||||
**/.env
|
||||
!**/.env.example
|
||||
App/uploads/
|
||||
|
||||
@@ -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 & 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');
|
||||
})();
|
||||
|
After Width: | Height: | Size: 369 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 301 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 721 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 714 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 595 KiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 601 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 500 KiB |
|
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 & 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 & Admin"></section>
|
||||
|
||||
<!-- ================= LEHRER-COCKPIT ================= -->
|
||||
<section data-sec="Lehrer-Cockpit (teacher.html)"></section>
|
||||
|
||||
<!-- ================= TOURISMUSREGION ================= -->
|
||||
<section data-sec="Tourismusregion"></section>
|
||||
|
||||
<!-- ================= LOGISTIK & PLATTFORM ================= -->
|
||||
<section data-sec="Logistik & Plattform"></section>
|
||||
|
||||
<!-- ================= ADMIN & AUTH ================= -->
|
||||
<section data-sec="Admin & 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 & 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 & 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 & 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>
|
||||
@@ -14,3 +14,18 @@ DB_PORT=3306
|
||||
|
||||
# ElevenLabs Sound Effects API — nur für generate-sounds.py (Dev-Zeit)
|
||||
# ELEVENLABS_API_KEY=...
|
||||
|
||||
# ── Auth-Backend (JWT + Redis) ──────────────────────────────────────────
|
||||
# 'session' = Legacy PHP-Datei-Sessions (Default, ändert nichts).
|
||||
# 'jwt' = JWT-Access-Token (HttpOnly-Cookie) + Redis-Session/Refresh.
|
||||
AUTH_BACKEND=session
|
||||
|
||||
# Signaturschlüssel für JWT (HS256). PFLICHT sobald AUTH_BACKEND=jwt.
|
||||
# Erzeugen mit: openssl rand -hex 32
|
||||
# Pro Umgebung UNTERSCHIEDLICH (dev ≠ prod). NIEMALS committen.
|
||||
# JWT_SECRET=
|
||||
|
||||
# Redis (Session-/Refresh-Store). Lokal: docker-compose.dev.yml → 127.0.0.1:6379
|
||||
REDIS_URL=tcp://127.0.0.1:6379
|
||||
# JWT_TTL=900 # Access-Token-Lebensdauer in Sekunden (Default 15 min)
|
||||
# JWT_REFRESH_TTL=2592000 # Session/Refresh in Sekunden (Default 30 Tage)
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
# Sensible Dateien niemals ausliefern (Defense-in-Depth), auch wenn sie im
|
||||
# Docroot liegen und die -f-Regel unten sie sonst direkt serviert.
|
||||
<FilesMatch "^\.env">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
<FilesMatch "\.(sh|sql|bak)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
# Backend-Editor-UI (statisch) nicht direkt ausliefern — nur über die
|
||||
# admin-geschützte Route /stadt-editor (pages/stadt-editor.php) erreichbar.
|
||||
<Files "stadt-editor.html">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
RewriteEngine On
|
||||
# Kein RewriteBase — Apache nutzt den Request-Pfad automatisch.
|
||||
# Funktioniert fuer alle Deploy-Ziele:
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- ============================================================
|
||||
-- class_modules.audio_info: Lehrer-Schalter für den Vorlese-Modus
|
||||
-- (Audio-Info-Modus in Sims, derzeit Tourismustal). Default AN.
|
||||
-- Stand: 2026-07-11 · Atlas
|
||||
-- ACHTUNG: Prod ist echtes MySQL (kein MariaDB) → kein "IF NOT EXISTS"
|
||||
-- bei ADD COLUMN. Guard über information_schema, damit idempotent.
|
||||
-- ============================================================
|
||||
SET @exists := (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'class_modules'
|
||||
AND COLUMN_NAME = 'audio_info');
|
||||
SET @ddl := IF(@exists = 0,
|
||||
'ALTER TABLE class_modules ADD COLUMN audio_info TINYINT(1) NOT NULL DEFAULT 1',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -0,0 +1,13 @@
|
||||
-- ============================================================
|
||||
-- Tourismustal: Glossar-Bilder (image_path) für die 14 neuen Begriffe
|
||||
-- Stand: 2026-07-11 · Atlas · Bilder: gpt-image-1 im Flat-Scandinavian-Stil,
|
||||
-- 1600×914 webp (Konvention der bestehenden 164 Einträge). Idempotent.
|
||||
-- ============================================================
|
||||
|
||||
UPDATE glossar SET image_path = CONCAT('assets/img/glossar/', key_slug, '.webp')
|
||||
WHERE key_slug IN (
|
||||
'standortfaktor','angebot-nachfrage','wertschoepfung','infrastruktur',
|
||||
'saisonalitaet','sommerloch','naechtigung','ganzjahrestourismus',
|
||||
'uebertourismus','raumnutzungskonflikt','beschneiung','diversifizierung',
|
||||
'tagesausgaben','sanfter-tourismus'
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
-- ============================================================
|
||||
-- Tourismustal: Glossar-Begriff "Soziale Verantwortung" (+ Zuordnung)
|
||||
-- Stand: 2026-07-11 · Atlas · idempotent
|
||||
-- Mechanik in der Sim: Personalhaus-Ausbau → Wohnqualität → Personal-
|
||||
-- zufriedenheit → freundlicherer Service → Gäste-Zufriedenheit (Win-win).
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO glossar (key_slug, title, short, text, short_easy, text_easy, category, sort_order, image_path) VALUES
|
||||
('soziale-verantwortung', 'Soziale Verantwortung',
|
||||
'Unternehmen tragen Verantwortung für ihre Beschäftigten – gute Arbeits- und Wohnbedingungen zahlen sich doppelt aus.',
|
||||
'Soziale Verantwortung (Corporate Social Responsibility) heißt: Ein Betrieb kümmert sich nicht nur um Gewinn, sondern auch um Menschen und Umwelt. Im Alpentourismus betrifft das besonders die Saisonarbeitskräfte – real wohnen sie oft in engen, schlechten Personalunterkünften. Wer in gute Personalhäuser, faire Löhne und gutes Arbeitsklima investiert, bekommt zufriedenes, loyales Personal – und Gäste spüren freundlichen Service sofort. Soziale Verantwortung ist damit kein Kostenfaktor, sondern eine Investition: ein Win-win für Beschäftigte, Gäste und Betrieb.',
|
||||
'Ein Betrieb kümmert sich gut um seine Mitarbeiter:innen.',
|
||||
'Soziale Verantwortung heißt: Ein Betrieb schaut auch auf seine Leute, nicht nur aufs Geld. Zum Beispiel: gute Wohnungen für das Personal. Zufriedene Mitarbeiter sind freundlich. Das freut auch die Gäste. Alle gewinnen.',
|
||||
'gesellschaft', 214,
|
||||
'assets/img/glossar/soziale-verantwortung.webp')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
title=VALUES(title), short=VALUES(short), text=VALUES(text),
|
||||
short_easy=VALUES(short_easy), text_easy=VALUES(text_easy),
|
||||
category=VALUES(category), sort_order=VALUES(sort_order), image_path=VALUES(image_path);
|
||||
|
||||
INSERT INTO glossar_modules (glossar_id, module_id)
|
||||
SELECT id, 'tourismustal' FROM glossar WHERE key_slug = 'soziale-verantwortung'
|
||||
ON DUPLICATE KEY UPDATE module_id=VALUES(module_id);
|
||||
@@ -0,0 +1,75 @@
|
||||
-- ============================================================
|
||||
-- Tourismusregion: Komplett-Integration (module_info + Glossar + Lehrplan)
|
||||
-- Stand: 2026-07-12 · Atlas · idempotent
|
||||
-- Schwester-Sim von Tourismustal (strikt getrennt): wellenbasierte
|
||||
-- Routen-Sim — Gäste ziehen über Wegkarten, Infrastruktur wirkt nur in
|
||||
-- Reichweite. Kern-Didaktik: Besucherlenkung, Mobilität, Standort am Weg.
|
||||
-- ============================================================
|
||||
|
||||
-- 1) Modul
|
||||
INSERT INTO module_info (
|
||||
module_id, title, subtitle, short_desc, long_desc, learning_goals,
|
||||
short_desc_easy, long_desc_easy, learning_goals_easy,
|
||||
duration_min, age_min, age_max, icon, card_image, page_url, play_url,
|
||||
status, sort_order
|
||||
) VALUES (
|
||||
'tourismusregion',
|
||||
'Tourismusregion',
|
||||
'Gäste lenken auf den Wegen deiner Region',
|
||||
'Gästegruppen ziehen über die Wege deiner Region – baue Wirtshäuser, Bahnen und Attraktionen in Reichweite, lenke Besucherströme und halte Umwelt und Budget im Lot.',
|
||||
'Acht Gästewellen ziehen über die Wegkarte deiner Region: Familien, Wandergruppen, Radfahrer:innen, Bus-Reisegruppen – jede mit eigenen Wünschen, eigenem Budget und eigenem Verkehrsmittel. Deine Gebäude wirken nur auf Gäste in ihrer Reichweite: Der Standort am Weg entscheidet. Zwischen den Wellen planst du in Baupausen, Ereignisse und A/B-Entscheidungen fordern dein Urteil. Fünf Wegkarten (Talschleife bis Passstraße), vier Saisonen und ein Endbericht mit Regionsprofil zeigen, ob du zufriedene Gäste, regionale Wertschöpfung, intakte Umwelt UND kluge Besucherlenkung geschafft hast.',
|
||||
'Besucherströme und Besucherlenkung verstehen · Standortwahl an Verkehrswegen begründen · Zielgruppen und ihre Mobilität unterscheiden (zu Fuß, Rad, Bus, Auto) · Wertschöpfung entlang einer Route erfassen · Zielkonflikte Umwelt/Verkehr/Wirtschaft abwägen (GW-Lehrplan 2023: 3.5–3.8, 3.17; übergreifend Verkehrs- und Mobilitätsbildung)',
|
||||
'Gäste wandern über die Wege. Baue Häuser an den Weg und lenke die Gäste gut.',
|
||||
'In deiner Region gibt es Wege. Auf den Wegen kommen Gäste: Familien, Wanderer, Rad-Fahrer, Bus-Gruppen. Baue Gast-Häuser und Attraktionen an die Wege. Nur Gäste in der Nähe nutzen sie. Nach 8 Gäste-Wellen siehst du: War deine Region gut?',
|
||||
'Du lernst: Wo baut man am besten? Wie lenkt man viele Besucher? Welche Gäste kommen wie an – zu Fuß, mit Rad oder Bus?',
|
||||
25, 9, 15,
|
||||
0xF09F97BAEFB88F,
|
||||
'card-tourismusregion.webp',
|
||||
'modul-tourismusregion',
|
||||
'tourismusregion',
|
||||
'beta', 95
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
title=VALUES(title), subtitle=VALUES(subtitle), short_desc=VALUES(short_desc),
|
||||
long_desc=VALUES(long_desc), learning_goals=VALUES(learning_goals),
|
||||
short_desc_easy=VALUES(short_desc_easy), long_desc_easy=VALUES(long_desc_easy),
|
||||
learning_goals_easy=VALUES(learning_goals_easy),
|
||||
duration_min=VALUES(duration_min), age_min=VALUES(age_min), age_max=VALUES(age_max),
|
||||
icon=VALUES(icon), card_image=VALUES(card_image), page_url=VALUES(page_url),
|
||||
play_url=VALUES(play_url), status=VALUES(status), sort_order=VALUES(sort_order);
|
||||
|
||||
-- 2) Glossar: neuer Kernbegriff Besucherlenkung
|
||||
INSERT INTO glossar (key_slug, title, short, text, short_easy, text_easy, category, sort_order, image_path) VALUES
|
||||
('besucherlenkung', 'Besucherlenkung',
|
||||
'Besucherströme gezielt steuern – durch Wege, Angebote, Beschilderung und Mobilität – damit weder Natur noch Orte überlastet werden.',
|
||||
'Besucherlenkung heißt: Eine Region steuert, WO und WIE sich Gäste bewegen – über Wegenetze, Attraktions-Standorte, Beschilderung, Parkraum und öffentliche Anreise (Bus statt Auto). Gute Lenkung verteilt Ströme, schützt sensible Naturräume und entlastet Ortskerne; schlechte Lenkung erzeugt Staus, Trampelpfade und Übertourismus-Hotspots. Werkzeuge reichen von Bushaltestellen und attraktiven Alternativrouten bis zu Obergrenzen an Ausflugszielen.',
|
||||
'Man lenkt, wohin die Gäste gehen – damit es nirgends zu voll wird.',
|
||||
'Viele Gäste an einem Ort sind ein Problem. Besucher-Lenkung heißt: Man verteilt die Gäste. Zum Beispiel mit Wegen, Schildern und Bussen. So bleibt die Natur heil und kein Ort ist zu voll.',
|
||||
'gesellschaft', 215,
|
||||
'assets/img/glossar/besucherlenkung.webp')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
title=VALUES(title), short=VALUES(short), text=VALUES(text),
|
||||
short_easy=VALUES(short_easy), text_easy=VALUES(text_easy),
|
||||
category=VALUES(category), sort_order=VALUES(sort_order), image_path=VALUES(image_path);
|
||||
|
||||
-- Modul-Zuordnung: neuer Begriff + passende bestehende Begriffe
|
||||
INSERT INTO glossar_modules (glossar_id, module_id)
|
||||
SELECT id, 'tourismusregion' FROM glossar WHERE key_slug IN (
|
||||
'besucherlenkung',
|
||||
'standortfaktor','angebot-nachfrage','wertschoepfung','infrastruktur',
|
||||
'saisonalitaet','uebertourismus','raumnutzungskonflikt','diversifizierung',
|
||||
'sanfter-tourismus','tagesausgaben','tourismus','nachhaltigkeit'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE module_id=VALUES(module_id);
|
||||
|
||||
-- 3) Lehrplan: Modul an bestehende Kompetenzen hängen
|
||||
-- (Anker hängen an den Kompetenzen und gelten damit automatisch mit.)
|
||||
INSERT INTO kompetenz_modules (kompetenz_id, module_id, level, strength)
|
||||
SELECT id, 'tourismusregion', 0, 'primaer' FROM kompetenzen WHERE key_slug='tourismus-raumentwicklung'
|
||||
ON DUPLICATE KEY UPDATE strength=VALUES(strength);
|
||||
INSERT INTO kompetenz_modules (kompetenz_id, module_id, level, strength)
|
||||
SELECT id, 'tourismusregion', 0, 'sekundaer' FROM kompetenzen WHERE key_slug='verkehrserziehung-sicherheit'
|
||||
ON DUPLICATE KEY UPDATE strength=VALUES(strength);
|
||||
INSERT INTO kompetenz_modules (kompetenz_id, module_id, level, strength)
|
||||
SELECT id, 'tourismusregion', 0, 'sekundaer' FROM kompetenzen WHERE key_slug='systemisches-denken'
|
||||
ON DUPLICATE KEY UPDATE strength=VALUES(strength);
|
||||
@@ -0,0 +1,57 @@
|
||||
-- ============================================================
|
||||
-- Buch-Kapitel Busfahrt <-> Glossar (2026-07-15)
|
||||
-- 5 neue Glossar-Eintraege + Modul-Mapping fuer alle im Kapitel
|
||||
-- verwendeten g()-Slugs. Idempotent (INSERT IGNORE), mehrfach
|
||||
-- ausfuehrbar, MySQL-kompatibel. Encoding: UTF-8.
|
||||
-- Anwenden: mysql --default-character-set=utf8mb4 -u root geograsim < diese Datei
|
||||
-- ============================================================
|
||||
|
||||
-- ---------- Neue Glossar-Eintraege ----------
|
||||
|
||||
INSERT IGNORE INTO glossar (key_slug, title, short, text, category, short_easy, text_easy) VALUES
|
||||
('fernbus', 'Fernbus',
|
||||
'Linienbus für lange Strecken zwischen Städten – meist günstig und klimafreundlich.',
|
||||
'Ein Fernbus verbindet Städte über große Entfernungen im Linienverkehr – anders als der Stadtbus, der nur innerhalb eines Ortes unterwegs ist. In Deutschland wurde der Fernbusmarkt 2013 geöffnet; seither ist ein dichtes Netz entstanden, Marktführer Flix verbindet über 8.000 Ziele in mehr als 40 Ländern. Typische Fahrgäste sind junge Leute, Familien und Schulklassen, denn die Tickets sind meist deutlich billiger als Bahn oder Flugzeug. Weil sich viele Menschen ein Fahrzeug teilen, verursacht ein Fernbus nur rund 30 g Treibhausgase pro [Personenkilometer](glossar:personenkilometer) – fast so wenig wie der Fernzug und nur ein Bruchteil von Auto oder Flugzeug.',
|
||||
'verkehr',
|
||||
'Ein Bus für weite Fahrten von Stadt zu Stadt.',
|
||||
'Ein Fern-Bus fährt von einer Stadt in eine andere Stadt.\nDie Fahrt ist oft sehr weit.\nDie Fahr-Karte kostet wenig Geld.\nViele Menschen teilen sich einen Bus.\nDarum ist der Fern-Bus gut für die Umwelt.'),
|
||||
|
||||
('transitverkehr', 'Transitverkehr',
|
||||
'Verkehr, der ein Land nur durchquert – Start und Ziel liegen in anderen Ländern.',
|
||||
'Transitverkehr durchquert ein Land oder eine Region, ohne dort zu starten oder anzukommen. Österreich ist wegen seiner Lage in der Mitte Europas ein typisches Transitland: Ein Lkw von München nach Verona fährt über den [Brenner](glossar:brennerkorridor) quer durch Tirol. 2025 rollten rund 2,42 Millionen Lkw über den Brenner – fast dreimal so viele wie über alle Schweizer Alpenübergänge zusammen. Die Menschen an der Strecke tragen Lärm, Stau und Abgase, der Nutzen entsteht aber vor allem anderswo. Deshalb wird politisch gestritten, wie viel Transit eine Region verträgt – etwa mit Lkw-Fahrverboten oder dem Bau des Brennerbasistunnels für die Bahn.',
|
||||
'verkehr',
|
||||
'Verkehr, der nur durch ein Land hindurchfährt.',
|
||||
'Viele Last-Wagen fahren nur durch Österreich hindurch.\nSie kommen aus einem anderen Land.\nSie fahren in ein anderes Land.\nDas heißt Transit-Verkehr.\nDer Transit-Verkehr macht Lärm und schlechte Luft.\nDarum gibt es oft Streit darüber.'),
|
||||
|
||||
('personenkilometer', 'Personenkilometer',
|
||||
'Maßeinheit der Verkehrsleistung: eine Person legt einen Kilometer zurück.',
|
||||
'Ein Personenkilometer (Pkm) bedeutet: Eine Person reist einen Kilometer weit. Fahren 50 Personen 100 km weit mit dem Bus, sind das 50 · 100 = 5.000 Personenkilometer. Mit dieser Einheit lassen sich Verkehrsmittel fair vergleichen, denn die [Emissionen](glossar:emissionen) eines Fahrzeugs werden auf alle Mitfahrenden aufgeteilt. Ein gut besetzter Reisebus kommt so auf nur rund 30 g Treibhausgase pro Personenkilometer, ein durchschnittlich besetztes Auto auf 164 g. Entscheidend ist die Auslastung: Ein fast leerer Bus kann pro Kopf schlechter abschneiden als ein voll besetztes Auto.',
|
||||
'einheit',
|
||||
'Ein Mensch fährt einen Kilometer weit.',
|
||||
'Ein Personen-Kilometer heißt:\nEin Mensch fährt einen Kilometer weit.\n50 Menschen im Bus auf 1 Kilometer sind 50 Personen-Kilometer.\nSo kann man Bus, Auto und Flugzeug gut vergleichen.\nEin voller Bus ist gut für die Umwelt.'),
|
||||
|
||||
('zollunion', 'Zollunion',
|
||||
'Staatenverbund ohne Zölle untereinander und mit gemeinsamen Zöllen nach außen.',
|
||||
'In einer Zollunion erheben die Mitgliedsländer untereinander keine Zölle mehr und wenden an ihren Außengrenzen gemeinsame Zollsätze an. Die EU-Zollunion besteht seit 1968: Waren können zwischen den EU-Staaten ohne Warenkontrollen und Zollpapiere gehandelt werden. Sie ist nicht dasselbe wie der [Schengen-Raum](glossar:schengen), der die Personenkontrollen abschafft: Die Schweiz gehört zu Schengen, aber nicht zur EU-Zollunion – deshalb kontrolliert der [Zoll](glossar:zoll) an der Schweizer Grenze Waren, obwohl niemand den Pass zeigen muss. Das Vereinigte Königreich war umgekehrt lange Mitglied der Zollunion, aber nie Teil von Schengen.',
|
||||
'recht',
|
||||
'Länder-Gruppe ohne Zoll an den Grenzen untereinander.',
|
||||
'Die EU-Länder sind eine Zoll-Union.\nDas heißt: Waren kosten an der Grenze keinen Zoll.\nNiemand kontrolliert die Waren im Bus oder Last-Wagen.\nDie Schweiz ist nicht in der Zoll-Union.\nAn der Schweizer Grenze werden Waren kontrolliert.'),
|
||||
|
||||
('binnengrenze', 'Binnengrenze',
|
||||
'Grenze zwischen zwei Mitgliedstaaten innerhalb eines Verbunds wie dem Schengen-Raum.',
|
||||
'Eine Binnengrenze ist eine Grenze zwischen zwei Staaten, die beide demselben Verbund angehören – im Schengen-Raum etwa die Grenze zwischen Österreich und Deutschland. An den Binnengrenzen des [Schengen-Raums](glossar:schengen) gibt es keine systematischen Personenkontrollen mehr: Busse, Autos und Züge fahren einfach durch, nur ein Schild zeigt den Staatswechsel an. Das Gegenstück ist die [EU-Außengrenze](glossar:eu-aussengrenze), an der weiterhin streng kontrolliert wird. In Ausnahmefällen – etwa bei Großereignissen oder starker Migration – dürfen Staaten vorübergehend wieder Kontrollen an Binnengrenzen einführen, wie es Österreich und Deutschland mehrfach getan haben.',
|
||||
'geografie',
|
||||
'Eine Grenze zwischen zwei Ländern im Schengen-Raum.',
|
||||
'Eine Binnen-Grenze liegt zwischen zwei Schengen-Ländern.\nZum Beispiel zwischen Österreich und Deutschland.\nDort gibt es keine Pass-Kontrolle.\nDer Bus fährt einfach durch.\nNur ein Schild zeigt: Jetzt bist du in einem anderen Land.');
|
||||
|
||||
-- ---------- Modul-Mapping: alle im Buch-Kapitel verwendeten Slugs ----------
|
||||
|
||||
INSERT IGNORE INTO glossar_modules (glossar_id, module_id)
|
||||
SELECT id, 'busfahrt' FROM glossar WHERE key_slug IN (
|
||||
'hauptstadt', 'wahrzeichen', 'topografie', 'auftrag', 'umweg',
|
||||
'massstab', 'luftlinie', 'tarif', 'marge', 'maut', 'vignette',
|
||||
'go-box', 'lenkzeit', 'tachograph', 'pauschalreise', 'emissionen',
|
||||
'klimawandel', 'tourismus', 'kinetose', 'schengen', 'eu-aussengrenze',
|
||||
'zoll', 'brennerkorridor', 'alpenpass',
|
||||
'fernbus', 'transitverkehr', 'personenkilometer', 'zollunion', 'binnengrenze'
|
||||
);
|
||||