Files
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/
- 12 code-workspace-Files
- STATUS-*.md
- viele M/D/R-Änderungen an bereits getrackten Files
- .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 02:27:02 +02:00

1039 lines
39 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Die große Wanderung — GeoGraSim</title>
<link rel="stylesheet" href="../../assets/css/design-system.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="">
<script src="../../assets/js/glossar-tooltip.js" defer></script>
<style>
/* =========================================================================
Wal-Sim · Sim-eigenes CSS
========================================================================= */
:root {
--wal-deep: #1a3949;
--wal-mid: #2e5b6e;
--wal-light: #5e8a9c;
--wal-sand: #e8e4d8;
--wal-sand-warm: #f4ede0;
--wal-coral: #b86840;
--wal-coral-dark: #8b4a2a;
--wal-moss: #4a7c4e;
--wal-honey: #e8c547;
--wal-ink: #1a2620;
--wal-ink-soft: #4a5550;
}
html, body { height: 100%; margin: 0; }
body {
font-family: 'Hanken Grotesk', system-ui, -apple-system, sans-serif;
background: var(--wal-sand);
color: var(--wal-ink);
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
#app { height: 100vh; display: flex; flex-direction: column; }
/* ---------- Era-Badge im Header ---------- */
#era-badge { background: var(--wal-coral); }
#era-badge.era-1850 { background: #8b4a2a; }
#era-badge.era-1990 { background: #4a7c8a; }
#era-badge.era-2026 { background: var(--wal-moss); }
/* ---------- Screens (Vollbild-Wechsel) ---------- */
.screen { flex: 1; display: none; flex-direction: column; min-height: 0; }
.screen.active { display: flex; }
/* =========================================================================
ERA PICKER
========================================================================= */
#screen-era {
background: linear-gradient(180deg, #2e5b6e 0%, #1a3949 100%);
color: var(--wal-sand-warm);
overflow-y: auto;
}
.era-head {
max-width: 880px; margin: 0 auto; padding: 36px 24px 16px; text-align: center;
}
.era-tag {
font-size: .72rem; letter-spacing: .22em; text-transform: uppercase;
color: var(--wal-honey); font-weight: 700; margin-bottom: 12px;
}
.era-head h1 {
font-size: clamp(1.6rem, 4vw, 2.4rem); font-weight: 800; margin: 0 0 10px;
letter-spacing: -.02em; line-height: 1.1;
}
.era-head p {
font-size: 1rem; line-height: 1.5; max-width: 640px; margin: 0 auto;
color: rgba(248, 244, 232, .85);
}
.era-grid {
max-width: 980px; margin: 0 auto; padding: 24px 16px 48px;
display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
}
.era-card {
background: var(--wal-sand-warm); color: var(--wal-ink);
border-radius: 10px; padding: 22px 22px 18px;
cursor: pointer; transition: transform .15s, box-shadow .15s;
border: 2px solid transparent;
display: flex; flex-direction: column; gap: 10px;
min-height: 280px;
}
.era-card:hover { transform: translateY(-3px); box-shadow: 0 12px 28px rgba(0,0,0,.25); }
.era-card:focus-visible { outline: 3px solid var(--wal-honey); outline-offset: 2px; }
.era-card-year {
font-size: 2.2rem; font-weight: 800; line-height: 1;
color: var(--wal-deep); letter-spacing: -.03em;
}
.era-card-sub {
font-size: .65rem; letter-spacing: .2em; text-transform: uppercase;
color: var(--wal-coral); font-weight: 700;
}
.era-card-lead { font-size: .92rem; line-height: 1.5; color: var(--wal-ink); flex: 1; }
.era-card-stats {
display: flex; gap: 12px; padding-top: 10px;
border-top: 1px solid rgba(0,0,0,.1); font-size: .78rem;
}
.era-card-stats span { color: var(--wal-ink-soft); }
.era-card-stats b { color: var(--wal-deep); }
.era-card-cta {
margin-top: auto; padding: 10px 14px;
background: var(--wal-coral); color: white;
border: 0; border-radius: 6px; font-weight: 700; cursor: pointer;
font-family: inherit; font-size: .9rem; letter-spacing: .04em;
}
.era-card-cta:hover { background: var(--wal-coral-dark); }
.era-foot {
max-width: 880px; margin: 0 auto; padding: 0 24px 32px;
font-size: .82rem; line-height: 1.55; color: rgba(248, 244, 232, .7);
text-align: center;
}
/* =========================================================================
GAME SCREEN — Stats-HUD + Map + Panel
========================================================================= */
#screen-game { background: var(--wal-sand); }
.hud {
background: var(--wal-deep); color: var(--wal-sand-warm);
padding: 8px 16px; display: flex; gap: 16px; align-items: center;
flex-wrap: wrap;
box-shadow: 0 2px 6px rgba(0,0,0,.15);
}
.hud-stage {
flex: 1; min-width: 200px;
font-weight: 700; font-size: .95rem;
}
.hud-stage small {
display: block; font-size: .68rem; letter-spacing: .15em;
text-transform: uppercase; color: rgba(248, 244, 232, .65);
font-weight: 600; margin-bottom: 2px;
}
.hud-day {
background: rgba(255,255,255,.1); padding: 4px 10px; border-radius: 99px;
font-size: .82rem; font-weight: 700; font-variant-numeric: tabular-nums;
}
.hud-stats { display: flex; gap: 8px; flex-wrap: wrap; }
.hud-stat {
min-width: 95px; background: rgba(255,255,255,.08);
padding: 5px 9px; border-radius: 6px;
}
.hud-stat-label {
font-size: .58rem; letter-spacing: .18em; text-transform: uppercase;
color: rgba(248, 244, 232, .7); font-weight: 700;
display: flex; justify-content: space-between;
}
.hud-stat-label .v {
font-variant-numeric: tabular-nums; color: var(--wal-sand-warm);
letter-spacing: .04em; font-size: .68rem;
}
.hud-stat-bar { height: 4px; background: rgba(0,0,0,.3); margin-top: 4px; border-radius: 2px; overflow: hidden; }
.hud-stat-fill { height: 100%; transition: width .25s linear, background .3s; border-radius: 2px; }
.hud-stat-fill.good { background: var(--wal-moss); }
.hud-stat-fill.ok { background: var(--wal-honey); }
.hud-stat-fill.bad { background: var(--wal-coral); }
.hud-calf {
font-size: 1.2rem; opacity: 0;
transition: opacity .35s;
}
.hud-calf.show { opacity: 1; }
#map-wrap {
flex: 1; min-height: 0; position: relative;
background: var(--wal-light);
}
#map { width: 100%; height: 100%; }
/* Leaflet whale marker (custom icon via class) */
.wal-marker {
font-size: 1.6rem; line-height: 1;
text-shadow: 0 1px 2px rgba(0,0,0,.4);
filter: drop-shadow(0 0 4px rgba(255,255,255,.6));
}
.panel {
background: var(--wal-sand-warm);
border-top: 3px solid var(--wal-coral);
padding: 14px 16px 18px; max-height: 38%; overflow-y: auto;
}
.panel-tag {
font-size: .65rem; letter-spacing: .22em; text-transform: uppercase;
color: var(--wal-coral); font-weight: 700; margin-bottom: 4px;
}
.panel-text { font-size: .95rem; line-height: 1.55; color: var(--wal-ink); margin: 0; }
.panel-text strong { color: var(--wal-deep); }
.panel-actions {
margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap;
}
/* =========================================================================
MODAL (Stage-Intro / Encounter / Tutorial)
========================================================================= */
.modal-back {
position: fixed; inset: 0; background: rgba(26, 38, 32, .72);
z-index: 9999; display: none; align-items: center; justify-content: center;
padding: 16px;
}
.modal-back.active { display: flex; }
.modal {
background: var(--wal-sand-warm); max-width: 560px; width: 100%;
max-height: 92vh; overflow-y: auto;
border-radius: 10px; display: flex; flex-direction: column;
box-shadow: 0 20px 40px rgba(0,0,0,.3);
}
.modal-head {
background: var(--wal-deep); color: var(--wal-sand-warm);
padding: 16px 22px;
border-radius: 10px 10px 0 0;
}
.modal-tag {
font-size: .65rem; letter-spacing: .25em; text-transform: uppercase;
color: var(--wal-honey); font-weight: 700; margin-bottom: 4px;
}
.modal-head h2 {
margin: 0; font-size: 1.4rem; font-weight: 800; line-height: 1.1;
letter-spacing: -.02em;
}
.modal-body {
padding: 20px 22px; display: flex; flex-direction: column; gap: 14px;
}
.modal-body p { margin: 0; font-size: .95rem; line-height: 1.55; }
.modal-body p strong { color: var(--wal-deep); }
.modal-foot {
display: flex; gap: 10px; padding: 16px 22px 18px;
border-top: 2px solid var(--wal-coral);
background: var(--wal-sand);
border-radius: 0 0 10px 10px;
position: sticky; bottom: 0;
}
.modal-foot .btn {
flex: 1; min-height: 50px; font-size: 1rem;
box-shadow: 0 2px 6px rgba(0,0,0,.12);
}
.choices { display: flex; flex-direction: column; gap: 8px; }
.choice {
text-align: left; padding: 12px 14px;
background: white; border: 2px solid #d9d3c4;
border-radius: 8px; cursor: pointer; font-family: inherit;
display: flex; flex-direction: column; gap: 3px;
transition: border-color .15s, background .15s;
min-height: 50px;
}
.choice:hover { border-color: var(--wal-coral); background: var(--wal-sand); }
.choice-label { font-weight: 700; color: var(--wal-deep); font-size: .94rem; }
.choice-hint { font-size: .8rem; color: var(--wal-ink-soft); }
.fact-box {
padding: 12px 14px; background: var(--wal-sand);
border-left: 4px solid var(--wal-moss);
border-radius: 0 6px 6px 0; font-size: .88rem; line-height: 1.55;
}
.fact-tag {
font-size: .58rem; letter-spacing: .25em; text-transform: uppercase;
color: var(--wal-moss); font-weight: 700; display: block; margin-bottom: 4px;
}
.effect-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.effect-chip {
font-size: .78rem; padding: 3px 8px; border-radius: 99px;
font-weight: 700; font-variant-numeric: tabular-nums;
}
.effect-chip.pos { background: rgba(74, 124, 78, .15); color: var(--wal-moss); }
.effect-chip.neg { background: rgba(184, 104, 64, .15); color: var(--wal-coral-dark); }
.effect-chip.time { background: rgba(46, 91, 110, .15); color: var(--wal-deep); }
/* =========================================================================
BUTTONS
========================================================================= */
.btn {
font-family: inherit; font-size: .95rem; font-weight: 700;
padding: 12px 20px; border: 0; border-radius: 6px; cursor: pointer;
background: var(--wal-coral); color: white;
letter-spacing: .03em; transition: background .15s, transform .08s;
display: inline-flex; align-items: center; justify-content: center;
min-height: 44px;
}
.btn:hover { background: var(--wal-coral-dark); }
.btn:active { transform: translateY(1px); }
.btn-secondary { background: var(--wal-deep); color: white; }
.btn-secondary:hover { background: var(--wal-ink); }
.btn-ghost {
background: white; color: var(--wal-deep);
border: 2px solid var(--wal-deep);
}
.btn-ghost:hover { background: var(--wal-sand); }
/* =========================================================================
END SCREEN
========================================================================= */
#screen-end {
background: linear-gradient(180deg, #1a3949 0%, #2e5b6e 100%);
color: var(--wal-sand-warm); overflow-y: auto;
padding: 36px 16px;
}
.end-inner { max-width: 640px; margin: 0 auto; }
.end-tag {
font-size: .68rem; letter-spacing: .25em; text-transform: uppercase;
color: var(--wal-honey); font-weight: 700; margin-bottom: 8px;
}
.end-inner h1 {
font-size: clamp(1.5rem, 4vw, 2.1rem); margin: 0 0 14px;
letter-spacing: -.02em; line-height: 1.1;
}
.end-text { font-size: 1.02rem; line-height: 1.55; margin: 0 0 20px; color: rgba(248,244,232,.92); }
.end-stats {
display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px; margin-bottom: 20px;
background: rgba(255,255,255,.08); border-radius: 8px; padding: 4px;
}
.end-stat { padding: 12px; text-align: center; }
.end-stat-label {
font-size: .58rem; letter-spacing: .18em; text-transform: uppercase;
color: rgba(248,244,232,.65); font-weight: 700; margin-bottom: 4px;
}
.end-stat-value {
font-size: 1.35rem; font-weight: 800; font-variant-numeric: tabular-nums;
color: var(--wal-honey);
}
/* =========================================================================
Mobile
========================================================================= */
@media (max-width: 720px) {
.hud { padding: 6px 10px; gap: 8px; }
.hud-stats { flex: 1; }
.hud-stat { min-width: 78px; flex: 1; padding: 4px 7px; }
.panel { max-height: 44%; padding: 12px 14px 14px; }
}
@media (max-width: 480px) {
.hud-stage { flex-basis: 100%; min-width: 0; }
.hud-stat-label .v { font-size: .62rem; }
}
</style>
</head>
<body>
<header class="ggs-header">
<a class="ggs-header-logo" id="cockpit-link" href="/schueler" title="Zurück zum Cockpit">
<img src="../../assets/img/bildLogo.png" alt="" class="ggs-header-logo-icon">
<img src="../../assets/img/textlogo_geograsim.svg" alt="GeoGraSim" class="ggs-header-logo-text">
</a>
<div class="ggs-header-separator"></div>
<span class="ggs-header-module">Die große Wanderung</span>
<span class="ggs-header-badge" id="era-badge"></span>
<div class="ggs-header-spacer"></div>
<div class="ggs-header-actions">
<button class="ggs-btn-ghost" id="btn-info"
title="Anleitung nochmal zeigen"
onclick="if(window.GGS_TUTORIAL&&typeof window.GGS_TUTORIAL.replay==='function')window.GGS_TUTORIAL.replay();"></button>
<a class="ggs-btn-ghost" id="ggs-lehrplan-link" href="/lehrplan#wal" target="_blank" rel="noopener" title="Lehrplan-Bezug">📚</a>
<button class="ggs-btn-ghost" id="btn-reset" title="Fortschritt zurücksetzen" onclick="resetWal()">🔄</button>
<a class="ggs-btn-ghost" id="cockpit-home" href="/schueler" title="Zurück zum Cockpit">🏠</a>
</div>
</header>
<main id="app">
<!-- ========= ERA PICKER ========= -->
<section class="screen" id="screen-era">
<div class="era-head">
<div class="era-tag">Welt-Auswahl</div>
<h1>Drei Welten derselbe Wal</h1>
<p>Du leitest einen Buckelwal auf der 6.000 km langen Wanderung von Alaska nach Hawaii. Du kannst wählen, in welcher Zeit deine Reise spielt. Die Geografie ist immer die gleiche aber die Welt um dich herum verändert sich.</p>
</div>
<div class="era-grid">
<div class="era-card" role="button" tabindex="0" data-era="1850">
<div class="era-card-sub">Walfangzeit</div>
<div class="era-card-year">1850</div>
<div class="era-card-lead">Walfänger-Schiffe segeln durch alle Meere. Fischschwärme sind so dicht wie nie wieder. Auf der Welt leben rund 125.000 Buckelwale aber die Zahl sinkt schnell.</div>
<div class="era-card-stats">
<span><b>~125.000</b> Wale</span>
<span><b>kein</b> Plastik</span>
<span><b>Harpune</b> als Gefahr</span>
</div>
<button class="era-card-cta" data-era-cta="1850">In dieser Welt starten →</button>
</div>
<div class="era-card" role="button" tabindex="0" data-era="1990">
<div class="era-card-sub">Erholung beginnt</div>
<div class="era-card-year">1990</div>
<div class="era-card-lead">Seit vier Jahren ist Walfang weltweit verboten. Nur noch rund 12.000 Buckelwale leben einen anderen zu treffen, ist seltenes Glück. Erste große Frachtschiffe queren den Pazifik.</div>
<div class="era-card-stats">
<span><b>~12.000</b> Wale</span>
<span><b>wenig</b> Plastik</span>
<span><b>Fischer-Netze</b> häufig</span>
</div>
<button class="era-card-cta" data-era-cta="1990">In dieser Welt starten →</button>
</div>
<div class="era-card" role="button" tabindex="0" data-era="2026">
<div class="era-card-sub">Heute</div>
<div class="era-card-year">2026</div>
<div class="era-card-lead">Rund 80.000 Buckelwale weltweit die Erholung läuft. Aber: dichter Schiffsverkehr, viel Plastik im Meer, militärisches Sonar, und das Wasser wird wärmer.</div>
<div class="era-card-stats">
<span><b>~80.000</b> Wale</span>
<span><b>viel</b> Plastik</span>
<span><b>Frachter</b> als Gefahr</span>
</div>
<button class="era-card-cta" data-era-cta="2026">In dieser Welt starten →</button>
</div>
</div>
<p class="era-foot">
Tipp für Lehrpersonen: Klasse in drei Gruppen aufteilen, jede Gruppe arbeitet in einer Welt. Der Vergleich in der Reflexion zeigt, was internationale Schutzabkommen bewegt haben und welche Probleme heute neu sind.
</p>
</section>
<!-- ========= GAME ========= -->
<section class="screen" id="screen-game">
<div class="hud">
<div class="hud-stage">
<small id="hudStageHead">Etappe 1 von 4</small>
<span id="hudStageName"></span>
</div>
<div class="hud-day">Tag <span id="hudDay">1</span></div>
<div class="hud-stats">
<div class="hud-stat">
<div class="hud-stat-label"><span>⚡ Energie</span><span class="v" id="hudEnergyV">100</span></div>
<div class="hud-stat-bar"><div class="hud-stat-fill good" id="hudEnergyB" style="width:100%"></div></div>
</div>
<div class="hud-stat">
<div class="hud-stat-label"><span>❤️ Gesund.</span><span class="v" id="hudHealthV">100</span></div>
<div class="hud-stat-bar"><div class="hud-stat-fill good" id="hudHealthB" style="width:100%"></div></div>
</div>
<div class="hud-stat">
<div class="hud-stat-label"><span>😖 Stress</span><span class="v" id="hudStressV">0</span></div>
<div class="hud-stat-bar"><div class="hud-stat-fill good" id="hudStressB" style="width:0%"></div></div>
</div>
</div>
<div class="hud-calf" id="hudCalf" title="Kalb geboren">🐳</div>
</div>
<div id="map-wrap">
<div id="map"></div>
</div>
<div class="panel" id="panel">
<div class="panel-tag" id="panelTag">Position</div>
<p class="panel-text" id="panelText"></p>
<div class="panel-actions" id="panelActions"></div>
</div>
</section>
<!-- ========= END SCREEN ========= -->
<section class="screen" id="screen-end">
<div class="end-inner">
<div class="end-tag" id="endTag">— Ende der Reise —</div>
<h1 id="endTitle"></h1>
<p class="end-text" id="endText"></p>
<div class="end-stats" id="endStats"></div>
<div class="fact-box" id="endFact" style="display:none">
<span class="fact-tag">Wusstest du?</span>
<span id="endFactText"></span>
</div>
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-top: 20px;">
<button class="btn" onclick="returnToEraPick()">Andere Welt erkunden</button>
<button class="btn btn-secondary" onclick="resetCurrentEra()">Dieselbe Welt nochmal</button>
</div>
</div>
</section>
</main>
<!-- ========= MODAL ========= -->
<div class="modal-back" id="modal">
<div class="modal">
<div class="modal-head">
<div class="modal-tag" id="modalTag"></div>
<h2 id="modalTitle"></h2>
</div>
<div class="modal-body" id="modalBody"></div>
<div class="modal-foot" id="modalFoot"></div>
</div>
</div>
<!-- Plattform-Wrapper-Injection (Atlas-Wrapper setzt diese Vars) -->
<script>/*__WAL_INJECTION__*/</script>
<!-- Leaflet -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Sim-Engine + UI -->
<script src="engine.js"></script>
<script>
// =============================================================================
// WAL · UI-Controller
// =============================================================================
(function () {
'use strict';
// ---- Shorthand ----
const $ = (id) => document.getElementById(id);
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const escapeHtml = (s) => String(s == null ? '' : s).replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'})[c]);
// ---- State ----
let state = null;
let map = null;
let walMarker = null;
let routeLine = null;
let booted = false;
// ---- Helpers ----
function lsKey(era) { return 'ggs-save-wal-' + era; }
function autoSave() {
if (!state) return;
try { localStorage.setItem(lsKey(state.era), JSON.stringify(state)); } catch (_) {}
}
function loadSave(era) {
try {
const raw = localStorage.getItem(lsKey(era));
if (!raw) return null;
const s = JSON.parse(raw);
return (s && typeof s === 'object' && s.era === era) ? s : null;
} catch (_) { return null; }
}
function clearSave(era) { try { localStorage.removeItem(lsKey(era)); } catch (_) {} }
function setBadge(era) {
const b = $('era-badge');
if (!era) { b.textContent = '—'; b.className = 'ggs-header-badge'; return; }
b.textContent = String(era);
b.className = 'ggs-header-badge era-' + era;
}
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
$(id).classList.add('active');
}
// ---- Stats-HUD ----
function updateHud() {
$('hudDay').textContent = state.day;
$('hudStageHead').textContent = 'Etappe ' + (state.stage + 1) + ' von ' + WalEngine.STAGE_COUNT;
const stage = WalEngine.getStage(state.era, state.stage);
$('hudStageName').textContent = stage ? stage.name : '—';
setStat('Energy', state.energy, 'high');
setStat('Health', state.health, 'high');
setStat('Stress', state.stress, 'low');
$('hudCalf').classList.toggle('show', !!state.hasCalf);
}
function setStat(key, val, goodWhen) {
const v = $('hud' + key + 'V');
const b = $('hud' + key + 'B');
v.textContent = Math.round(val);
b.style.width = val + '%';
const cls = (goodWhen === 'low')
? (val <= 33 ? 'good' : val <= 66 ? 'ok' : 'bad')
: (val >= 66 ? 'good' : val >= 33 ? 'ok' : 'bad');
b.className = 'hud-stat-fill ' + cls;
}
// ---- Modal ----
function showModal(tag, title, bodyHtml, footHtml) {
$('modalTag').textContent = tag;
$('modalTitle').textContent = title;
$('modalBody').innerHTML = bodyHtml;
$('modalFoot').innerHTML = footHtml || '';
$('modal').classList.add('active');
}
function closeModal() { $('modal').classList.remove('active'); }
// ---- Map ----
function initMap() {
if (map) return;
map = L.map('map', {
zoomControl: true,
attributionControl: true,
minZoom: 3,
maxZoom: 6,
scrollWheelZoom: true,
}).setView([40, -145], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
maxZoom: 12,
}).addTo(map);
routeLine = L.polyline(WalEngine.routePoints, {
color: '#b86840', weight: 3, opacity: .75, dashArray: '6,6',
}).addTo(map);
const startIcon = L.divIcon({
className: 'wal-marker', html: '🐋', iconSize: [32, 32], iconAnchor: [16, 16],
});
walMarker = L.marker(WalEngine.routePoints[0], { icon: startIcon }).addTo(map);
L.marker([57.0, -133.8]).bindPopup('Frederick Sound · Start').addTo(map);
L.marker([20.85, -156.5]).bindPopup('Maui · Ziel').addTo(map);
}
function moveWhale(routeIdx) {
if (!walMarker || !WalEngine.routePoints[routeIdx]) return;
const pt = WalEngine.routePoints[routeIdx];
walMarker.setLatLng(pt);
map.panTo(pt, { animate: true, duration: .8 });
}
// ---- Tutorial ----
const TUTORIAL_STEPS = [
{ tag: 'Anleitung', title: 'Du bist eine Buckelwal-Kuh',
body: '<p>Du steuerst einen großen Buckelwal ein Meeres-Säugetier, das jedes Jahr eine sehr weite Reise macht.</p><p>Vor dir liegt die längste Reise deines Lebens: 6.000 km von Alaska nach Hawaii. Du arbeitest dich durch <strong>4 Etappen</strong>. In jeder Etappe wartet eine Begegnung, bei der du entscheiden musst.</p>' },
{ tag: 'Anleitung', title: 'Drei Werte zeigen, wie es dir geht',
body: '<p><strong>⚡ Energie</strong> ist dein Speck fällt sie auf 0, schaffst du es nicht mehr nach Hawaii.</p><p><strong>❤️ Gesundheit</strong> sinkt bei Verletzungen fällt sie auf 0, ist die Reise vorbei.</p><p><strong>😖 Stress</strong> steigt bei bedrohlichen Situationen. Zu viel Stress kostet dich Energie über die Zeit.</p>' },
{ tag: 'Anleitung', title: 'Jede Entscheidung hat Folgen',
body: '<p>Bei jeder Begegnung wählst du zwischen zwei oder drei Wegen. Der sichere Weg kostet oft mehr Energie. Der riskante Weg kann gut ausgehen oder Gesundheit kosten.</p><p>Nach jeder Entscheidung siehst du, was passiert, und eine <strong>Faktbox</strong> erklärt den Hintergrund.</p>' },
{ tag: 'Anleitung', title: 'Dein Ziel: Maui erreichen',
body: '<p>Auf Hawaii wirst du im Winter dein Kalb zur Welt bringen. Ankunft mit Kalb ist das Hauptziel.</p><p>Die Reise dauert ungefähr <strong>90 Tage</strong>. Wörter mit gestrichelter Unterlinie kannst du antippen dann öffnet sich eine kurze Erklärung.</p>' },
];
let tutorialStep = 0;
let postTutorialCallback = null; // wird nach Tutorial-Close gefeuert
function startTutorial(onAfter) {
tutorialStep = 0;
postTutorialCallback = (typeof onAfter === 'function') ? onAfter : null;
renderTutorialStep();
}
function renderTutorialStep() {
const t = TUTORIAL_STEPS[tutorialStep];
const isLast = tutorialStep >= TUTORIAL_STEPS.length - 1;
const counter = '<div style="text-align:center;font-size:.7rem;color:var(--wal-ink-soft);letter-spacing:.15em;margin-top:8px">' + (tutorialStep+1) + ' / ' + TUTORIAL_STEPS.length + '</div>';
const foot = isLast
? '<button class="btn" onclick="closeTutorial()">Los geht\'s</button>'
: '<button class="btn btn-ghost" onclick="closeTutorial()">Überspringen</button>'
+ '<button class="btn" onclick="nextTutorialStep()">Weiter →</button>';
showModal(t.tag, t.title, t.body + counter, foot);
}
function nextTutorialStep() {
tutorialStep++;
if (tutorialStep >= TUTORIAL_STEPS.length) return closeTutorial();
renderTutorialStep();
}
function closeTutorial() {
closeModal();
try { localStorage.setItem('wal.tutorial.seen', '1'); } catch (_) {}
const cb = postTutorialCallback;
postTutorialCallback = null;
if (typeof cb === 'function') {
// Mini-Delay, damit Modal-Schließ-Animation nicht mit nächstem Modal kollidiert
setTimeout(cb, 60);
}
}
window.startTutorial = startTutorial;
window.closeTutorial = closeTutorial;
window.nextTutorialStep = nextTutorialStep;
// ---- Plattform-Convention: GGS_TUTORIAL ----
window.GGS_TUTORIAL = {
simId: 'wal',
title: '🐋 Die große Wanderung',
replay: function () { startTutorial(null); },
};
// ---- Era-Picker ----
function bindEraPicker() {
document.querySelectorAll('[data-era-cta]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
startEra(parseInt(btn.dataset.eraCta, 10));
});
});
document.querySelectorAll('.era-card[data-era]').forEach(card => {
card.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') return;
startEra(parseInt(card.dataset.era, 10));
});
card.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); startEra(parseInt(card.dataset.era, 10)); }
});
});
}
function startEra(era) {
setBadge(era);
// Existierenden Save laden?
const saved = loadSave(era);
if (saved && saved.status === 'playing' && (saved.stage > 0 || saved.eventIndex > 0)) {
if (confirm('Du hast in dieser Welt schon angefangen (Etappe ' + (saved.stage+1) + '). Weitermachen?\n\nAbbrechen = neu starten.')) {
state = saved;
enterGame();
return;
}
clearSave(era);
}
state = WalEngine.newState(era);
showScreen('screen-game');
setTimeout(() => {
initMap();
if (map) map.invalidateSize();
updateHud();
autoSave();
// Tutorial bei Erststart, sonst direkt Stage-Intro
const seen = (() => { try { return localStorage.getItem('wal.tutorial.seen') === '1'; } catch (_) { return false; } })();
if (!seen) {
// Pending-Callback feuert nach Tutorial-Close (Skip oder „Los geht's")
startTutorial(showStageIntro);
} else {
showStageIntro();
}
}, 80);
}
function enterGame() {
showScreen('screen-game');
setTimeout(() => {
initMap();
if (map) map.invalidateSize();
moveWhale(WalEngine.pointIndexFor(state.era, state.stage, state.eventIndex));
updateHud();
setPanel('Etappe ' + (state.stage+1) + ' · Begegnung ' + (state.eventIndex+1),
'Du setzt deine Reise fort.',
'<button class="btn" onclick="nextEvent()">Weiter →</button>');
}, 80);
}
// ---- Stage / Event Flow ----
function showStageIntro() {
const s = WalEngine.getStage(state.era, state.stage);
if (!s) return;
closeModal();
setBadge(state.era);
moveWhale(WalEngine.pointIndexFor(state.era, state.stage, 0) - 1);
showModal(
'Etappe ' + (state.stage + 1) + ' von ' + WalEngine.STAGE_COUNT + ' · ' + s.season,
s.name,
'<p class="panel-text">' + s.intro.text + '</p>'
+ '<div class="fact-box"><span class="fact-tag">Hintergrund</span>' + s.intro.fact + '</div>',
'<button class="btn" onclick="closeModal(); nextEvent();">Weiterreisen →</button>'
);
autoSave();
}
function setPanel(tag, text, actionsHtml) {
$('panelTag').textContent = tag;
$('panelText').innerHTML = text;
$('panelActions').innerHTML = actionsHtml || '';
}
function nextEvent() {
// Tages-Drift zwischen Begegnungen
state.energy = clamp(state.energy - 3, 0, 100);
state.stress = clamp(state.stress - 2, 0, 100);
state.day += 5 + Math.floor(Math.random() * 4);
updateHud();
autoSave();
if (state.energy <= 0) return endRun('starvation');
if (state.health <= 0) return endRun('injury');
const stage = WalEngine.getStage(state.era, state.stage);
// Etappenende?
if (state.eventIndex >= stage.events.length) {
state.stage++;
state.eventIndex = 0;
autoSave();
if (state.stage >= WalEngine.STAGE_COUNT) return endRun('arrival');
return showStageIntro();
}
const ev = stage.events[state.eventIndex];
moveWhale(WalEngine.pointIndexFor(state.era, state.stage, state.eventIndex));
setPanel(
'Etappe ' + (state.stage + 1) + ' · Begegnung ' + (state.eventIndex + 1),
'Du näherst dich einer neuen Situation: <strong>' + ev.title + '</strong>',
'<button class="btn" onclick="openEvent()">Hinsehen →</button>'
);
}
function openEvent() {
const stage = WalEngine.getStage(state.era, state.stage);
const ev = stage.events[state.eventIndex];
const choicesHtml = '<div class="choices">'
+ ev.choices.map((c, i) =>
'<button class="choice" onclick="chooseOption(' + i + ')">'
+ '<span class="choice-label">' + c.label + '</span>'
+ (c.hint ? '<span class="choice-hint">' + c.hint + '</span>' : '')
+ '</button>'
).join('')
+ '</div>';
showModal(
ev.tag, ev.title,
'<p class="panel-text">' + ev.text + '</p>' + choicesHtml,
''
);
}
window.openEvent = openEvent;
function chooseOption(i) {
const stage = WalEngine.getStage(state.era, state.stage);
const ev = stage.events[state.eventIndex];
const choice = ev.choices[i];
// Effekte anwenden
const eff = choice.effects || {};
if (typeof eff.energy === 'number') state.energy = clamp(state.energy + eff.energy, 0, 100);
if (typeof eff.health === 'number') state.health = clamp(state.health + eff.health, 0, 100);
if (typeof eff.stress === 'number') state.stress = clamp(state.stress + eff.stress, 0, 100);
if (choice.sideEffect && typeof choice.sideEffect.day === 'number') state.day += choice.sideEffect.day;
if (choice.calf) state.hasCalf = true;
state.stressPeak = Math.max(state.stressPeak, state.stress);
state.encountersDone++;
state.choicesLog.push({stage: state.stage, event_id: ev.id || ev.title, choice_label: choice.label});
if (ev.fact) { state.factsReadList.push(ev.id || ev.title); state.factsRead++; }
updateHud();
moveWhale(WalEngine.pointIndexFor(state.era, state.stage, state.eventIndex) + 1);
autoSave();
// Auflösung + Fakt
const factHtml = ev.fact
? '<div class="fact-box"><span class="fact-tag">Wusstest du?</span>' + ev.fact + '</div>'
: '';
showModal(
ev.tag, ev.title,
'<p class="panel-text">' + choice.result + '</p>'
+ buildEffectChips(eff, choice.sideEffect)
+ factHtml,
'<button class="btn" onclick="afterChoice()">Weiter →</button>'
);
}
window.chooseOption = chooseOption;
function buildEffectChips(eff, sideEffect) {
const chips = [];
if (typeof eff.energy === 'number' && eff.energy !== 0) chips.push(['energy', eff.energy, eff.energy>0?'pos':'neg']);
if (typeof eff.health === 'number' && eff.health !== 0) chips.push(['health', eff.health, eff.health>0?'pos':'neg']);
if (typeof eff.stress === 'number' && eff.stress !== 0) chips.push(['stress', eff.stress, eff.stress<0?'pos':'neg']);
if (sideEffect && typeof sideEffect.day === 'number' && sideEffect.day !== 0) chips.push(['day', sideEffect.day, 'time']);
if (!chips.length) return '';
const map = {energy:'⚡', health:'❤️', stress:'😖', day:'🗓'};
return '<div class="effect-chips">' + chips.map(([k, v, cls]) =>
'<span class="effect-chip ' + cls + '">' + map[k] + ' ' + (v>0?'+':'') + v + (k==='day'?' Tage':'') + '</span>'
).join('') + '</div>';
}
function afterChoice() {
state.eventIndex++;
closeModal();
autoSave();
if (state.energy <= 0) return endRun('starvation');
if (state.health <= 0) return endRun('injury');
nextEvent();
}
window.afterChoice = afterChoice;
window.nextEvent = nextEvent;
window.closeModal = closeModal;
// ---- Reise-Ende ----
function endRun(reason) {
state.status = (reason === 'arrival') ? 'won' : 'lost';
state.endReason = reason;
autoSave();
closeModal();
showScreen('screen-end');
const meta = WalEngine.endMeta(state, reason);
$('endTag').textContent = meta.tag;
$('endTitle').textContent = meta.title;
$('endText').textContent = meta.text;
$('endFactText').textContent = meta.fact || '';
$('endFact').style.display = meta.fact ? 'block' : 'none';
$('endStats').innerHTML = ''
+ statBlock('Welt', state.era)
+ statBlock('Tage', state.day)
+ statBlock('Etappen', state.stage)
+ statBlock('Energie', Math.round(state.energy))
+ statBlock('Gesund.', Math.round(state.health))
+ statBlock('Stress-Max', Math.round(state.stressPeak));
submitAssessment(reason);
}
function statBlock(label, val) {
return '<div class="end-stat"><div class="end-stat-label">' + label + '</div><div class="end-stat-value">' + val + '</div></div>';
}
// ---- Submit (Plattform-Endpoint) ----
function submitAssessment(reason) {
if (!window.WAL_SESSION_ID || !window.WAL_API_BASE) return; // Direkt-URL / Gast → kein Submit
const outcome = (reason === 'arrival')
? (state.hasCalf ? 'arrival_with_calf' : 'arrival_no_calf')
: reason; // starvation | injury | whaling
const level = (state.era === 1850) ? 1 : (state.era === 1990) ? 2 : 3;
const stars = computeStars(reason);
const score = computeScore(reason);
fetch(window.WAL_API_BASE + '/progress.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'same-origin',
body: JSON.stringify({
sim_id: 'wal',
action: 'submit_assessment',
data: {
level,
stars,
score,
duration_ms: (window.__GGS_LIVE__ && window.__GGS_LIVE__.activeMs) || 0,
completed: outcome.startsWith('arrival'),
results: {
era: state.era,
days_total: state.day,
outcome,
energy_end: Math.round(state.energy),
health_end: Math.round(state.health),
stress_peak: Math.round(state.stressPeak),
stages_completed: state.stage,
encounters_total: state.encountersDone,
choices_log: state.choicesLog,
facts_seen: state.factsRead,
},
},
}),
}).catch(() => {});
}
function computeStars(reason) {
if (reason === 'arrival' && state.hasCalf) return 5;
if (reason === 'arrival') return 4;
if (state.stage >= 2) return 2;
return 1;
}
function computeScore(reason) {
const outcome = (reason === 'arrival') ? (state.hasCalf ? 'arrival_with_calf' : 'arrival_no_calf') : reason;
const base = outcome === 'arrival_with_calf' ? 70
: outcome === 'arrival_no_calf' ? 50
: 0;
const bonus = Math.round(state.health * 0.12) + Math.round(state.energy * 0.08);
const malus = Math.round(state.stressPeak * 0.05);
const timePen = Math.max(0, state.day - 90) * 0.3;
return clamp(0, 100, Math.round(base + bonus - malus - timePen));
}
// ---- Live-Snapshot fürs Cockpit (Plattform-Convention) ----
window.GGS_LIVE_STATE = function () {
if (!state || !booted) return null;
const stage = WalEngine.getStage(state.era, state.stage);
const total = WalEngine.totalEvents(state.era);
const seen = WalEngine.eventsSeenBefore(state.era, state.stage) + state.eventIndex;
const pct = Math.round((seen / Math.max(1, total)) * 100);
const healthStatus = (state.energy + state.health) / 2 >= 60 ? 'good'
: (state.energy + state.health) / 2 >= 30 ? 'ok'
: 'struggle';
return {
simId: 'wal',
era: state.era,
stage: state.stage + 1,
stageName: stage ? stage.name : '—',
day: state.day,
energy: Math.round(state.energy),
health: Math.round(state.health),
stress: Math.round(state.stress),
stressPeak: Math.round(state.stressPeak),
hasCalf: !!state.hasCalf,
progressPct: pct,
health_status: healthStatus,
encountersDone: state.encountersDone,
};
};
// ---- Reset + Back to Era Picker ----
function resetWal() {
if (!state) { showScreen('screen-era'); setBadge(null); return; }
if (!confirm('Aktuelle Reise abbrechen und neu starten?\nDer Fortschritt in dieser Welt geht verloren.')) return;
clearSave(state.era);
const era = state.era;
state = WalEngine.newState(era);
closeModal();
showScreen('screen-game');
setTimeout(() => {
if (map) map.invalidateSize();
moveWhale(0);
updateHud();
showStageIntro();
}, 60);
}
window.resetWal = resetWal;
function resetCurrentEra() {
const era = state.era;
clearSave(era);
state = WalEngine.newState(era);
showScreen('screen-game');
setTimeout(() => {
if (map) map.invalidateSize();
moveWhale(0);
updateHud();
showStageIntro();
}, 60);
}
window.resetCurrentEra = resetCurrentEra;
function returnToEraPick() {
state = null;
setBadge(null);
showScreen('screen-era');
}
window.returnToEraPick = returnToEraPick;
// =============================================================================
// BOOT
// =============================================================================
function boot() {
// Glossar-Tooltip mit lokalen Fallback-Snippets initialisieren.
// Sobald in DB Wal-Begriffe angelegt sind, greift die API automatisch.
if (window.GlossarTooltip) {
window.GlossarTooltip.init({
apiBase: (window.WAL_API_BASE || '/php/api'),
moduleId: 'wal',
fallback: WalEngine.glossarFallback || {},
});
}
bindEraPicker();
booted = true;
// Falls Lehrperson Era erzwingt → direkt los
const forcedEra = window.WAL_FORCED_ERA;
if (forcedEra && [1850, 1990, 2026].includes(forcedEra)) {
startEra(forcedEra);
return;
}
// Sonst Era-Picker zeigen
showScreen('screen-era');
setBadge(null);
}
window.addEventListener('DOMContentLoaded', boot);
})();
</script>
</body>
</html>