Compare commits
250 Commits
1e51ef7def
...
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 | |||
| 0a2ebf46b4 | |||
| eb565d4c01 |
@@ -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}`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# Tourismustal — Lehrplan-Recherche D-A-CH (Stand 2026-07-08)
|
||||||
|
|
||||||
|
Primärquellengestützt. AT-Belege aus **BGBl. II Nr. 1/2023** (RIS) bzw. layoutierte
|
||||||
|
Fassung V11 auf gwb.schule.at. Basis für die SQL-Anker + LEHRPLAN.md.
|
||||||
|
|
||||||
|
## AT — GW-Lehrplan 2023, Sek I
|
||||||
|
|
||||||
|
**Kompetenzmodell = 3 Kompetenzen** (nicht das dt. 6-Dimensionen-Modell!):
|
||||||
|
Orientierungs-, Urteils-, Handlungskompetenz. Die vielen „Kompetenzbereiche" sind
|
||||||
|
klassen-/themenbezogene Teilbereiche, keine Modellsäulen. Anforderungsbereiche
|
||||||
|
(kognitive Stufen): Reproduktion, Transfer, Reflexion, Problemlösung.
|
||||||
|
|
||||||
|
Zentrale fachliche Konzepte: Gemeinsamkeiten/Unterschiede · Veränderung/Wandel ·
|
||||||
|
Leistungserstellung & Nachhaltigkeit · Kooperation/Konkurrenz · Vernetzung & Märkte ·
|
||||||
|
Ökonomische Prinzipien & Entscheidungsfindung.
|
||||||
|
|
||||||
|
**Kern-Deskriptoren 3. Klasse (7. Schulstufe) „Leben und Wirtschaften in Österreich":**
|
||||||
|
- **3.17** (WICHTIGSTE Fundstelle) — Raumnutzungen für Wohnen, Arbeit, Verkehr,
|
||||||
|
Freizeit und **Tourismus** vergleichen + **Nutzungskonflikte** über **Raumplanung** lösen.
|
||||||
|
- **3.6** — Industrie-, **Tourismusregionen**, landw. Regionen charakterisieren/verorten.
|
||||||
|
- **3.5** — Wandel von **Standortfaktoren/-entscheidungen** und Folgen erörtern.
|
||||||
|
- **3.7** — Folgen der **Ansiedelung eines Unternehmens** (Infrastruktur, Demografie).
|
||||||
|
- **3.8** — **Preisbildung: Angebot & Nachfrage** + Produktionskosten.
|
||||||
|
- **3.16** — **Lebensqualität** zentraler/peripherer Räume vergleichen/bewerten.
|
||||||
|
- **3.13** — wirtsch. Kenngrößen: Wachstum/BIP, Beschäftigung, alt. Wohlstandsindikatoren.
|
||||||
|
- **3.11/3.12** — Bedeutung von Unternehmen / unternehmerischer Innovation.
|
||||||
|
|
||||||
|
**Klima-Schicht 4. Klasse „globalisierte Welt":**
|
||||||
|
- **4.2** — Folgen des **Klimawandels** aufs Leben und Wirtschaften.
|
||||||
|
- **4.10** — nachhaltige Wirtschaftsaktivität bei Klimawandel/Ressourcenknappheit.
|
||||||
|
- **4.5** — EU-Politikfeld **Regionalentwicklung**.
|
||||||
|
|
||||||
|
**Wirtschaftliche Bildung / Entrepreneurship:** 2.7 (Angebot/Nachfrage→Preis),
|
||||||
|
2.8 (**Projektidee** unter Knappheit/Preisgestaltung → Entrepreneurship Education,
|
||||||
|
„Unternehmerisches Denken und Handeln"). EE verankert über übergreifendes Thema Nr. 2,
|
||||||
|
TRIO-Modell (Aff/Lindner 2005): Core / Entrepreneurial Culture („Trans") / Civic.
|
||||||
|
|
||||||
|
**Übergreifende Themen (13 Stück, Allgemeiner Teil):** von der Sim abgedeckt:
|
||||||
|
- Kern: **13** Wirtschafts-, Finanz- und Verbraucher/innenbildung · **2** Entrepreneurship
|
||||||
|
Education · **11** Umweltbildung für nachhaltige Entwicklung · **12** Verkehrs- und
|
||||||
|
Mobilitätsbildung.
|
||||||
|
- Mittel: **7** Politische Bildung · **1** Berufsorientierung · **5** Interkulturelle Bildung.
|
||||||
|
- Nr. 11 nennt „Rollen-, Plan- sowie **Simulationsspiele**" ausdrücklich als Methode.
|
||||||
|
|
||||||
|
**Vorbehalt:** „Alpen/alpin" steht im Sek-I-GW-Text NICHT wörtlich → im Modul über
|
||||||
|
„Tourismusregion in Österreich" framen, nicht als Zitat „alpin" ausgeben.
|
||||||
|
|
||||||
|
## DE — Deutschland (Fach Geographie/Erdkunde, Kl. 7/8)
|
||||||
|
- **NRW** Kernlehrplan Erdkunde: eigenes Inhaltsfeld Tourismus; wörtlich „**Zielkonflikt
|
||||||
|
zwischen wirtschaftlichem Wachstum durch Tourismus und nachhaltiger Entwicklung**" +
|
||||||
|
„Raumnutzungskonflikte … Gefährdung des Naturraums".
|
||||||
|
- **Bayern** LehrplanPLUS Geo Jg. 7: **Alpen** explizit — naturräumliches Potenzial +
|
||||||
|
touristische Nutzung, Naturgefahren/Schutz.
|
||||||
|
- **DGfG-Bildungsstandards** (MSA 2020): 6 Kompetenzbereiche (Fachwissen · Räuml.
|
||||||
|
Orientierung · Erkenntnisgewinnung/Methoden · Kommunikation · Beurteilung/Bewertung ·
|
||||||
|
Handlung); Leitkonzept **Mensch-Umwelt-System**.
|
||||||
|
|
||||||
|
## CH — Schweiz (Lehrplan 21, RZG, Zyklus 3)
|
||||||
|
- **RZG.2.5 „Die Bedeutung des Tourismus einschätzen"** (Alpenraum, wirtschaftliche
|
||||||
|
Bedeutung, Einfluss auf Gebirgsregionen, **nachhaltiger Tourismus**) — zentralster Anker.
|
||||||
|
- **RZG.3.3** Raumplanung, **Raumnutzungskonflikte**, nachhaltige Raumentwicklung.
|
||||||
|
- **RZG.3.1** natürliche Systeme + Nutzung (Gebirge; Tourismus/Siedlung/Nutzungswandel).
|
||||||
|
- **RZG.3.2** wirtsch. Prozesse/Globalisierung · **RZG.1.4** Ressourcen/Energie.
|
||||||
|
|
||||||
|
## MAPPING Sim-Mechanik → AT-Anker
|
||||||
|
| Mechanik | Anker | Kompetenz |
|
||||||
|
|---|---|---|
|
||||||
|
| Skipass-Preisregler | 3.8 (Angebot/Nachfrage) | Orientierung+Urteil |
|
||||||
|
| Personalhaus/Wohnraum | 3.7, 3.16 | Urteil+Handlung |
|
||||||
|
| Bürgerinitiative/Naturwert | 3.17 (Nutzungskonflikt) | Urteil+Handlung |
|
||||||
|
| Beschneiung/Schneegrenze | 4.2, 4.10 | Orientierung+Urteil |
|
||||||
|
| Übertourismus-Event | 3.6, 3.17, 3.13 | Urteil+Handlung |
|
||||||
|
| Standortaufbau/Investition | 3.5, 3.11/3.12, 2.8 | Handlung |
|
||||||
|
| Ganzjahrestourismus-Umbau | 4.10, 3.5, 4.5 | Urteil+Handlung |
|
||||||
|
| Endabrechnung/KPI | 3.13 | Urteil |
|
||||||
|
|
||||||
|
**Ankersatz:** Primär 3. Klasse/7. Schulstufe, Kern **3.17**, gestützt durch
|
||||||
|
3.5/3.6/3.7/3.8/3.13/3.16; Klima-Schicht 4.2/4.10. Fördert alle drei Kompetenzen;
|
||||||
|
übergreifende Themen 13, 2, 11, 12, 7.
|
||||||
|
|
||||||
|
Referenz-Konvention für SQL: `reference = "BMBWF GW-Lehrplan Sek I 2023, Kompetenz 3.17"`,
|
||||||
|
`reference_url = https://www.ris.bka.gv.at/eli/bgbl/II/2023/1`.
|
||||||
@@ -23,3 +23,4 @@ secret_keys.txt
|
|||||||
**/.humaninput/
|
**/.humaninput/
|
||||||
**/.env
|
**/.env
|
||||||
!**/.env.example
|
!**/.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 Sound Effects API — nur für generate-sounds.py (Dev-Zeit)
|
||||||
# ELEVENLABS_API_KEY=...
|
# 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
|
RewriteEngine On
|
||||||
# Kein RewriteBase — Apache nutzt den Request-Pfad automatisch.
|
# Kein RewriteBase — Apache nutzt den Request-Pfad automatisch.
|
||||||
# Funktioniert fuer alle Deploy-Ziele:
|
# 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,147 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Tourismustal: Glossar-Begriffe + Modul-Zuordnung + Quellen
|
||||||
|
-- Stand: 2026-07-11 · Atlas
|
||||||
|
-- 13 neue Begriffe (mit Leichter Sprache), Zuordnung auch für die
|
||||||
|
-- bestehenden Begriffe 'tourismus' und 'nachhaltigkeit'.
|
||||||
|
-- Quellen: siehe App/sims/tourismustal/QUELLEN.md (Recherche 2026-07-10).
|
||||||
|
-- Idempotent: Begriffe per ON DUPLICATE, Quellen per DELETE+INSERT.
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO glossar (key_slug, title, short, text, short_easy, text_easy, category, sort_order) VALUES
|
||||||
|
('standortfaktor', 'Standortfaktor',
|
||||||
|
'Eigenschaften eines Ortes, die ihn für eine Nutzung geeignet machen – z. B. Höhenlage, Erreichbarkeit, Landschaft.',
|
||||||
|
'Standortfaktoren sind die Eigenschaften eines Ortes, die darüber entscheiden, ob sich dort etwas ansiedeln kann: Verkehrsanbindung, Gelände, Klima, Arbeitskräfte, Nähe zu Kunden. Im Alpentourismus zählen z. B. Höhenlage (Schneesicherheit), Erreichbarkeit (Straße, Bus) und Landschaftsbild. Man unterscheidet harte (messbare) und weiche Faktoren (z. B. Image, Sicherheit).',
|
||||||
|
'Was einen Ort gut für etwas macht.',
|
||||||
|
'Ein Standort-Faktor sagt: Ist dieser Ort gut für ein Geschäft oder Hotel? Wichtig sind zum Beispiel: Kommt man leicht hin? Liegt genug Schnee? Ist die Landschaft schön?',
|
||||||
|
'wirtschaft', 200),
|
||||||
|
('angebot-nachfrage', 'Angebot und Nachfrage',
|
||||||
|
'Das Zusammenspiel von Anbietern und Käufern bestimmt den Preis: Viel Nachfrage bei wenig Angebot macht teuer – und umgekehrt.',
|
||||||
|
'Auf Märkten treffen Angebot (was verkauft wird) und Nachfrage (was Menschen kaufen wollen) aufeinander – daraus entsteht der Preis. Steigt der Preis, kaufen weniger Menschen; sinkt er, kommen mehr. Im Tourismus sichtbar am Skipass: Ein hoher Preis bringt mehr Geld pro Gast, aber weniger Gäste kommen. Der GW-Lehrplan nennt das Preisbildung auf Märkten.',
|
||||||
|
'Was viele wollen und selten ist, wird teuer.',
|
||||||
|
'Angebot ist: Was es zu kaufen gibt. Nachfrage ist: Was die Menschen kaufen wollen. Beides zusammen macht den Preis. Ist der Ski-Pass sehr teuer, kommen weniger Gäste.',
|
||||||
|
'wirtschaft', 201),
|
||||||
|
('wertschoepfung', 'Wertschöpfung',
|
||||||
|
'Der wirtschaftliche Wert, der vor Ort entsteht – im Tourismus alles, was Gäste in der Region ausgeben.',
|
||||||
|
'Wertschöpfung ist der Wert, der durch Arbeit und Leistungen in einer Region entsteht. Im Tourismus: Übernachtung, Essen, Tickets, Verleih – alles, was Gäste vor Ort ausgeben und was Löhne und Einkommen im Tal sichert. Wichtig: Ein Wintergast gibt in Österreich im Schnitt 193 € pro Tag aus, ein Sommergast 156 € – gleich viele Gäste bedeuten also nicht gleich viel Wertschöpfung.',
|
||||||
|
'Das Geld, das Gäste in einem Ort ausgeben und lassen.',
|
||||||
|
'Wert-Schöpfung heißt: In einem Ort wird Geld verdient. Gäste zahlen für Schlafen, Essen und Tickets. Davon leben die Menschen im Ort.',
|
||||||
|
'wirtschaft', 202),
|
||||||
|
('infrastruktur', 'Infrastruktur',
|
||||||
|
'Die Grundausstattung eines Ortes: Straßen, Bahnen, Wasser, Strom – im Tourismus auch Lifte, Wege und Unterkünfte.',
|
||||||
|
'Infrastruktur ist alles, was ein Ort braucht, damit Leben und Wirtschaft funktionieren: Verkehrswege, Versorgung, Kommunikation. Touristische Infrastruktur sind Bahnen, Pisten, Wege, Hotels. Sie ist teuer im Bau UND im Betrieb – eine Seilbahn kostet auch dann Geld, wenn niemand fährt. Deshalb versuchen Regionen, ihre Infrastruktur ganzjährig auszulasten.',
|
||||||
|
'Alles, was ein Ort zum Funktionieren braucht.',
|
||||||
|
'Infra-Struktur ist die Grund-Ausstattung: Straßen, Strom, Wasser. Im Urlaubs-Ort auch: Lifte, Wege und Hotels. Das alles kostet Geld, auch wenn gerade keine Gäste da sind.',
|
||||||
|
'wirtschaft', 203),
|
||||||
|
('saisonalitaet', 'Saisonalität',
|
||||||
|
'Starke Schwankung von Gästezahlen im Jahresverlauf – Hochsaison, Nebensaison, Zwischensaison.',
|
||||||
|
'Tourismus schwankt mit den Jahreszeiten: In Skiorten ist der Winter Hochsaison, dazwischen liegen schwache Monate. Saisonalität ist teuer, weil Betten, Bahnen und Personal auch in schwachen Zeiten Geld kosten. Je gleichmäßiger sich die Saisonen füllen, desto wirtschaftlicher arbeitet eine Region – deshalb ist Ganzjahrestourismus das große Ziel vieler Orte.',
|
||||||
|
'Mal kommen viele Gäste, mal fast keine – je nach Jahreszeit.',
|
||||||
|
'Saison heißt: die Zeit mit vielen Gästen. In Ski-Orten ist das der Winter. In anderen Monaten ist wenig los. Das ist ein Problem, weil Hotels und Lifte immer Geld kosten.',
|
||||||
|
'wirtschaft', 204),
|
||||||
|
('sommerloch', 'Sommerloch (Nebensaison)',
|
||||||
|
'Die gästearme Zeit in Wintersport-Orten: Betten leer, Bahnen still – z. B. hat Lech am Arlberg nur 16 % seiner Nächtigungen im Sommer.',
|
||||||
|
'In winterlastigen Skiorten heißt die schwache Sommerzeit umgangssprachlich „Sommerloch". Extrembeispiel Lech am Arlberg: 84 % der Übernachtungen im Winter, nur 16 % im Sommer (Statistik Austria, 2023). Andere Orte zeigen das Gegenmodell: Am Wilden Kaiser kommen Sommer und Winter etwa gleich viele Gäste, und in ganz Österreich hat der Sommer den Winter bei den Nächtigungen überholt (83 zu 72 Mio., 2025).',
|
||||||
|
'Die Zeit im Sommer, wenn in Ski-Orten fast keine Gäste kommen.',
|
||||||
|
'Viele Ski-Orte leben nur vom Winter. Im Sommer sind die Betten leer. Das nennt man Sommer-Loch. Kluge Orte bauen darum auch Sommer-Angebote: Badesee, Wander-Wege, Rad-Strecken.',
|
||||||
|
'wirtschaft', 205),
|
||||||
|
('naechtigung', 'Nächtigung',
|
||||||
|
'Eine Übernachtung eines Gastes – die Maßeinheit der Tourismusstatistik.',
|
||||||
|
'Die Tourismusstatistik zählt Nächtigungen (Übernachtungen) und Ankünfte (angereiste Gäste). Österreich zählte 2025 rund 83 Mio. Sommer- und 72 Mio. Winter-Nächtigungen – der Sommer liegt bei den Nächtigungen vorn, beim Umsatz aber weiter der Winter, weil Wintergäste pro Tag mehr ausgeben. Wer Zeitungsberichte über Tourismus verstehen will, muss diese Zahl lesen können.',
|
||||||
|
'Eine Nacht, die ein Gast in einem Ort schläft.',
|
||||||
|
'Eine Nächtigung ist: Ein Gast schläft eine Nacht im Hotel. Damit zählt man, wie gut der Tourismus läuft. Österreich hat im Jahr über 150 Millionen Nächtigungen.',
|
||||||
|
'wirtschaft', 206),
|
||||||
|
('ganzjahrestourismus', 'Ganzjahrestourismus',
|
||||||
|
'Die Strategie, Gäste in allen Jahreszeiten anzusprechen – statt nur von einer Saison zu leben.',
|
||||||
|
'Ganzjahrestourismus verteilt das Geschäft auf alle Jahreszeiten: Ski im Winter, Wandern, Bike und Familienangebote im Sommer. Vorbilder: Serfaus-Fiss-Ladis steigerte die Sommernächtigungen in zehn Jahren um bis zu 95 %, Zell am See-Kaprun und der Wilde Kaiser erreichen fast 1:1. Wichtig: Die Umstellung dauert Jahre und ersetzt die Winter-Wertschöpfung nur teilweise – Bergbahnen brauchen 1,3 bis 4,2 Sommergäste pro fehlendem Wintergast.',
|
||||||
|
'Ein Ort will das ganze Jahr Gäste haben, nicht nur im Winter.',
|
||||||
|
'Ganz-Jahres-Tourismus heißt: Gäste kommen im Winter UND im Sommer. Dafür braucht ein Ort Sommer-Angebote wie Badesee oder Rad-Wege. Das dauert viele Jahre.',
|
||||||
|
'wirtschaft', 207),
|
||||||
|
('uebertourismus', 'Übertourismus',
|
||||||
|
'Wenn zu viele Gäste einen Ort überlasten: verstopfte Straßen, gestresste Einheimische, leidende Natur.',
|
||||||
|
'Übertourismus (Overtourism) entsteht, wenn Besucherzahlen die Belastungsgrenzen eines Ortes übersteigen: Verkehr, Lärm, Preise steigen, Lebensqualität und Naturwert sinken – und am Ende bleiben genau die Gäste aus, die wegen der Ruhe kamen. Gegenmittel sind Besucherlenkung, Obergrenzen und Qualitäts- statt Massenstrategie.',
|
||||||
|
'Zu viele Gäste auf einmal – der Ort wird überlastet.',
|
||||||
|
'Über-Tourismus heißt: Es kommen zu viele Gäste. Die Straßen sind voll. Die Natur leidet. Die Menschen im Ort sind genervt. Dann ist Urlaub dort nicht mehr schön.',
|
||||||
|
'gesellschaft', 208),
|
||||||
|
('raumnutzungskonflikt', 'Raumnutzungskonflikt',
|
||||||
|
'Wenn verschiedene Nutzungen um dieselbe Fläche konkurrieren – z. B. Skigebiet gegen Naturschutz.',
|
||||||
|
'Fläche ist knapp: Wohnen, Verkehr, Freizeit, Tourismus und Naturschutz wollen oft dasselbe Stück Land. Ein neuer Lift bedeutet Rodung, ein Hotel verbraucht Wiese. Raumplanung versucht, solche Konflikte fair zu lösen – mit Zonen, Schutzgebieten und Beteiligung der Bevölkerung (z. B. Bürgerinitiativen). Der GW-Lehrplan 2023 nennt genau das in Kompetenz 3.17.',
|
||||||
|
'Streit darüber, wofür ein Stück Land genutzt wird.',
|
||||||
|
'Ein Raum-Nutzungs-Konflikt ist ein Streit um Land: Die einen wollen einen Lift bauen. Die anderen wollen den Wald schützen. Die Raum-Planung sucht eine faire Lösung.',
|
||||||
|
'gesellschaft', 209),
|
||||||
|
('beschneiung', 'Beschneiung',
|
||||||
|
'Künstliche Schneeerzeugung mit Wasser und Druckluft – Anpassung an wärmere Winter, aber ressourcenintensiv.',
|
||||||
|
'Schneekanonen zerstäuben Wasser bei Minusgraden zu Schnee. Sie sichern die Saison, wenn Naturschnee fehlt – brauchen aber viel Wasser und Energie und funktionieren nur bei Kälte. Hintergrund: Die Schneesaison in den Alpen ist seit 1970 rund einen Monat kürzer geworden (5–9 Tage pro Jahrzehnt), unterhalb von ca. 2.000 m trifft es Skigebiete am härtesten (GeoSphere Austria, SLF).',
|
||||||
|
'Maschinen machen Schnee, wenn zu wenig echter Schnee liegt.',
|
||||||
|
'Schnee-Kanonen machen aus Wasser Kunst-Schnee. Das hilft, wenn der Winter zu warm ist. Aber es braucht sehr viel Wasser und Strom. Und es geht nur, wenn es kalt genug ist.',
|
||||||
|
'klima', 210),
|
||||||
|
('diversifizierung', 'Diversifizierung',
|
||||||
|
'Auf mehrere Standbeine setzen statt auf eines – im Tourismus: Sommer UND Winter, mehrere Zielgruppen.',
|
||||||
|
'Wer nur ein Produkt hat, ist verletzlich – fällt es aus, bricht alles zusammen. Diversifizierung verteilt das Risiko auf mehrere Angebote: Ein Skiort mit Bikepark, Badesee und Wanderwegen übersteht schneearme Winter besser. Das ist unternehmerisches Denken im Sinn der Entrepreneurship Education: Chancen erkennen, Risiken streuen.',
|
||||||
|
'Nicht alles auf eine Karte setzen – mehrere Angebote haben.',
|
||||||
|
'Diversifizierung heißt: viele verschiedene Angebote haben. Ein Ski-Ort mit Badesee und Rad-Wegen hat auch dann Gäste, wenn kein Schnee liegt.',
|
||||||
|
'wirtschaft', 211),
|
||||||
|
('tagesausgaben', 'Tagesausgaben pro Gast',
|
||||||
|
'Wie viel ein Gast pro Urlaubstag ausgibt – Wintergäste in Österreich ~193 €, Sommergäste ~156 €.',
|
||||||
|
'Die Tagesausgaben umfassen Unterkunft, Essen, Tickets und Einkäufe. In Österreich gibt ein Wintergast im Schnitt 193 € pro Tag aus, ein Sommergast 156 € (Österreich Werbung/T-MONA) – der Sommergast bringt also nur rund 80 % der Wertschöpfung. Darum gilt: Gleich viele Sommergäste ersetzen den Winterumsatz nicht 1:1 – für Bergbahnen braucht es sogar 1,3 bis 4,2 Sommergäste pro Wintergast.',
|
||||||
|
'Wie viel Geld ein Gast an einem Urlaubs-Tag ausgibt.',
|
||||||
|
'Ein Winter-Gast gibt in Österreich etwa 193 Euro am Tag aus. Ein Sommer-Gast etwa 156 Euro. Winter-Gäste bringen also mehr Geld.',
|
||||||
|
'wirtschaft', 212),
|
||||||
|
('sanfter-tourismus', 'Sanfter Tourismus',
|
||||||
|
'Tourismus mit möglichst kleinem Fußabdruck: Natur erleben, ohne sie zu verbrauchen.',
|
||||||
|
'Sanfter Tourismus setzt auf Wandern, Rad, Naturerlebnis und öffentliche Anreise statt auf immer mehr Erschließung. Er schont Landschaft und Klima – genau die Ressourcen, von denen der Alpentourismus lebt. Beispiele: Panoramawege, Hochseilgärten (nutzen den Wald, ohne ihn zu roden), Bergsteigerdörfer. Gegenstück zum harten, infrastrukturintensiven Massentourismus.',
|
||||||
|
'Urlaub, der die Natur wenig belastet.',
|
||||||
|
'Sanfter Tourismus schont die Natur: wandern, Rad fahren, mit dem Bus anreisen. So bleibt die Landschaft schön – auch für die Gäste von morgen.',
|
||||||
|
'gesellschaft', 213)
|
||||||
|
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);
|
||||||
|
|
||||||
|
-- ---------- Modul-Zuordnung ----------
|
||||||
|
INSERT INTO glossar_modules (glossar_id, module_id)
|
||||||
|
SELECT id, 'tourismustal' FROM glossar WHERE key_slug IN (
|
||||||
|
'standortfaktor','angebot-nachfrage','wertschoepfung','infrastruktur',
|
||||||
|
'saisonalitaet','sommerloch','naechtigung','ganzjahrestourismus',
|
||||||
|
'uebertourismus','raumnutzungskonflikt','beschneiung','diversifizierung',
|
||||||
|
'tagesausgaben','sanfter-tourismus',
|
||||||
|
'tourismus','nachhaltigkeit'
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE module_id=VALUES(module_id);
|
||||||
|
|
||||||
|
-- ---------- Quellen (DELETE+INSERT = idempotent) ----------
|
||||||
|
DELETE gs FROM glossar_sources gs
|
||||||
|
JOIN glossar g ON g.id = gs.glossar_id
|
||||||
|
WHERE g.key_slug IN ('sommerloch','naechtigung','ganzjahrestourismus','beschneiung','tagesausgaben');
|
||||||
|
|
||||||
|
INSERT INTO glossar_sources (glossar_id, ref_num, label, url, access_date)
|
||||||
|
SELECT id, 1, 'Statistik Austria: Ein Blick auf die Gemeinde Lech (2024)',
|
||||||
|
'https://www.statistik.at/blickgem/G0804/g80113.pdf', '2026-07-10'
|
||||||
|
FROM glossar WHERE key_slug='sommerloch';
|
||||||
|
INSERT INTO glossar_sources (glossar_id, ref_num, label, url, access_date)
|
||||||
|
SELECT id, 2, 'Statistik Austria: Ankünfte & Nächtigungen (2025/26)',
|
||||||
|
'https://www.statistik.at/statistiken/tourismus-und-verkehr/tourismus/beherbergung/ankuenfte-naechtigungen', '2026-07-10'
|
||||||
|
FROM glossar WHERE key_slug='sommerloch';
|
||||||
|
INSERT INTO glossar_sources (glossar_id, ref_num, label, url, access_date)
|
||||||
|
SELECT id, 1, 'Statistik Austria: Ankünfte & Nächtigungen (2025/26)',
|
||||||
|
'https://www.statistik.at/statistiken/tourismus-und-verkehr/tourismus/beherbergung/ankuenfte-naechtigungen', '2026-07-10'
|
||||||
|
FROM glossar WHERE key_slug='naechtigung';
|
||||||
|
INSERT INTO glossar_sources (glossar_id, ref_num, label, url, access_date)
|
||||||
|
SELECT id, 1, 'brand-logic: Fallstudie Serfaus-Fiss-Ladis',
|
||||||
|
'https://www.brand-logic.com/en/spectrum/gekommen-um-zu-bleiben/', '2026-07-10'
|
||||||
|
FROM glossar WHERE key_slug='ganzjahrestourismus';
|
||||||
|
INSERT INTO glossar_sources (glossar_id, ref_num, label, url, access_date)
|
||||||
|
SELECT id, 2, 'Seilbahnen Schweiz via SRF: Bergbahnen machen immer mehr Umsatz im Sommer (2023)',
|
||||||
|
'https://www.srf.ch/news/schweiz/tourismus-in-der-schweiz-bergbahnen-machen-immer-mehr-umsatz-in-den-sommermonaten', '2026-07-10'
|
||||||
|
FROM glossar WHERE key_slug='ganzjahrestourismus';
|
||||||
|
INSERT INTO glossar_sources (glossar_id, ref_num, label, url, access_date)
|
||||||
|
SELECT id, 1, 'GeoSphere Austria: Klimaportal Schnee',
|
||||||
|
'https://klimaportal.geosphere.at/informationsportal-klimawandel/schnee.html', '2026-07-10'
|
||||||
|
FROM glossar WHERE key_slug='beschneiung';
|
||||||
|
INSERT INTO glossar_sources (glossar_id, ref_num, label, url, access_date)
|
||||||
|
SELECT id, 2, 'Klein et al. (Uni Neuchâtel/SLF) via SRF: Schneesaison fast 40 Tage kürzer',
|
||||||
|
'https://www.srf.ch/news/schweiz/schneesaison-ist-fast-40-tage-kuerzer-als-frueher', '2026-07-10'
|
||||||
|
FROM glossar WHERE key_slug='beschneiung';
|
||||||
|
INSERT INTO glossar_sources (glossar_id, ref_num, label, url, access_date)
|
||||||
|
SELECT id, 1, 'Österreich Werbung/T-MONA: Studie zum Winterurlaub – Tagesausgaben (2023)',
|
||||||
|
'https://www.tourismuspresse.at/presseaussendung/TPT_20231005_TPT0006/studie-zum-winterurlaub-tagesausgaben-gestiegen-skifahren-anhaltend-beliebt', '2026-07-10'
|
||||||
|
FROM glossar WHERE key_slug='tagesausgaben';
|
||||||