Files
Adminator 502f271f36 Einheitliche Sim-Kopfzeile (Entwurf B) als Komponente + Lizenz-Datenmodell
- ggs-header.css/.js: schwebende Pillen-Kopfzeile aus vorhandenem
  .ggs-header-Markup (Kreis-Logo/Titel/Level als eine Pille, eigene
  SVG-Icons, Tempo-Panel links + Zustandsbutton, Musik-Panel unten,
  30% transparente Untermenues, Glossar target=_blank, Lehrplan raus).
  Enhance-Ansatz: Spiel-Logik (IDs/Klassen, data-speed) bleibt unberuehrt.
- Eingebaut in Klima 2D/3D, Fluss, Logistik (Auto-Tempo), Energiemanager,
  Kofferdetektiv 3D.
- admin-licenses.html: Datenmodell-Sektion (licenses-Tabelle, Lebenszyklus
  Generiert->Eingeloest->Zugewiesen, Beziehungen, Jahr->Schuljahr->Code).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-25 14:06:41 +02:00

2172 lines
93 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>GeoGraSim — Flussmanagement</title>
<link rel="icon" type="image/svg+xml" href="../../favicon.svg">
<link rel="stylesheet" href="../../assets/fonts/inter.css">
<link rel="stylesheet" href="../../assets/css/design-system.css">
<link rel="stylesheet" href="../../assets/css/ggs-header.css">
<script src="../../assets/js/ggs-header.js"></script>
<style>
/* ==========================================================
FLUSSMANAGEMENT — modul-spezifisches CSS
========================================================== */
/* Sim-Zone als Flex-Column, damit Canvas-Viewport die volle Resthoehe
unter der Topbar bekommt (analog Klima). */
.ggs-zone-sim {
display: flex;
flex-direction: column;
}
/* Canvas-Viewport waechst mit — nicht bei min-height:320px kleben bleiben. */
#sim-viewport {
flex: 1 1 auto;
min-height: 0; /* laesst flex-children wirklich schrumpfen/wachsen */
}
/* Canvas nimmt die volle Zone ein. touch-action:none verhindert iPad-Scroll
beim Bauen auf dem Grid. */
#fluss-canvas {
width: 100%;
height: 100%;
display: block;
background: #dde6d4;
touch-action: none;
cursor: crosshair;
border-radius: 10px;
}
/* Werkzeug-Cards im rechten Panel. .disabled-progression zeigt Werkzeuge,
die in diesem Level noch nicht freigeschaltet sind. */
.ggs-card.disabled-progression {
opacity: .35;
filter: grayscale(90%);
pointer-events: none;
}
.ggs-card.disabled-progression::after {
content: "🔒 ab Level 2";
font-size: 10px;
color: var(--ggs-text-muted);
display: block;
margin-top: 4px;
}
.ggs-card.disabled-progression[data-unlock="3"]::after {
content: "🔒 ab Level 3";
}
/* Aktives Werkzeug — deutliche Hervorhebung */
.ggs-card.tool-active {
border-color: var(--ggs-fjord);
background: var(--ggs-fjord-light);
box-shadow: 0 0 0 2px var(--ggs-fjord) inset;
}
/* Werkzeug aktuell nicht leistbar — ausgegraut, aber noch lesbar.
Klicks werden blockiert, Cursor zeigt "nicht erlaubt". */
.ggs-card.too-expensive {
opacity: .45;
filter: grayscale(70%);
cursor: not-allowed;
}
.ggs-card.too-expensive .ggs-badge.cost {
background: var(--ggs-coral);
color: #fff;
}
/* Nur die "aktuelle Stufe"-Hervorhebung ergaenzt das jetzt im
Design-System stehende .ggs-level-card (Atlas 22:40). */
.ggs-level-card.current {
background: var(--ggs-fjord-light);
border-color: var(--ggs-fjord);
}
/* Event-Toasts oben im Canvas: volle Breite, Zeilenumbruch erlauben,
nur die Rundung bleibt am Rand sichtbar. Ohne das bricht der Text
oft rechts aus dem Kasten heraus. */
.ggs-event-viewport {
top: 10px;
left: 12px;
right: 12px;
transform: none;
align-items: stretch;
}
.ggs-event-notification {
max-width: none;
white-space: normal;
text-align: center;
padding: 10px 16px;
}
</style>
</head>
<body>
<!-- ============================================================
LOADING SCREEN
============================================================ -->
<div class="ggs-loading" id="loading-screen">
<img src="../../assets/img/bildLogo.png" alt="" class="ggs-loading-logo">
<div class="ggs-loading-text">Fluss wird geladen ...</div>
</div>
<!-- ============================================================
HEADER
============================================================ -->
<header class="ggs-header" data-ggs-sim="fluss">
<a href="/schueler" class="ggs-header-logo">
<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" id="module-title">Flussmanagement</span>
<span class="ggs-header-badge" id="level-badge">Level 1</span>
<div class="ggs-header-spacer"></div>
<div class="ggs-header-actions">
<!-- Speed Control -->
<div class="ggs-speed">
<button class="ggs-speed-btn" data-speed="0" title="Pause (P)"></button>
<button class="ggs-speed-btn active" data-speed="1" title="Normal (1)">1x</button>
<button class="ggs-speed-btn" data-speed="2" title="Schnell (2)">2x</button>
<button class="ggs-speed-btn" data-speed="4" title="Turbo (3)">4x</button>
</div>
<!-- Music Player -->
<div class="ggs-music" id="ggs-music" hidden>
<select class="ggs-music-sel" id="ggs-music-sel" title="Hintergrundmusik"></select>
<button class="ggs-music-btn" id="ggs-music-toggle" title="Play / Pause"></button>
<input class="ggs-music-vol" id="ggs-music-vol" type="range" min="0" max="100" value="35" title="Lautstärke">
</div>
<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="#" target="_blank" rel="noopener" title="Lehrplan-Bezug">📚</a>
<button class="ggs-btn-ghost" id="btn-mute" title="Ton aus/an">🔊</button>
<button class="ggs-btn-ghost" id="btn-reset" title="Fortschritt zurücksetzen">🔄</button>
<a href="/schueler" class="ggs-btn-ghost" title="Zurück zum Cockpit">🏠</a>
</div>
</header>
<!-- ============================================================
SIMULATION LAYOUT
============================================================ -->
<main class="ggs-sim-layout">
<!-- ─── LINKS: Status ─── -->
<aside class="ggs-zone-status">
<div class="ggs-panel">
<div class="ggs-panel-title">Zustand</div>
<div class="ggs-param" data-param="population">
<span class="ggs-param-icon">👥</span>
<span class="ggs-param-label">Bevölkerung</span>
<span class="ggs-param-value" id="p-population">200</span>
<span class="ggs-trend" id="t-population"></span>
</div>
<div class="ggs-param-bar"><div class="ggs-param-fill good" id="b-population" style="width:40%"></div></div>
<div class="ggs-param" data-param="food">
<span class="ggs-param-icon">🌾</span>
<span class="ggs-param-label">Nahrung</span>
<span class="ggs-param-value" id="p-food">50</span>
<span class="ggs-trend" id="t-food"></span>
</div>
<div class="ggs-param-bar"><div class="ggs-param-fill warning" id="b-food" style="width:50%"></div></div>
<div class="ggs-param" data-param="biodiv">
<span class="ggs-param-icon">🐟</span>
<span class="ggs-param-label" data-glossar="biodiversitaet">Biodiversität</span>
<span class="ggs-param-value" id="p-biodiv">80</span>
<span class="ggs-trend" id="t-biodiv"></span>
</div>
<div class="ggs-param-bar"><div class="ggs-param-fill good" id="b-biodiv" style="width:80%"></div></div>
<div class="ggs-param" data-param="flood">
<span class="ggs-param-icon">🛡️</span>
<span class="ggs-param-label" data-glossar="hochwasserschutz">Hochwasserschutz</span>
<span class="ggs-param-value" id="p-flood">30</span>
<span class="ggs-trend" id="t-flood"></span>
</div>
<div class="ggs-param-bar"><div class="ggs-param-fill warning" id="b-flood" style="width:30%"></div></div>
<div class="ggs-param" data-param="economy">
<span class="ggs-param-icon">💼</span>
<span class="ggs-param-label">Wirtschaft</span>
<span class="ggs-param-value" id="p-economy">50</span>
<span class="ggs-trend" id="t-economy"></span>
</div>
<div class="ggs-param-bar"><div class="ggs-param-fill warning" id="b-economy" style="width:50%"></div></div>
</div>
<div class="ggs-panel">
<div class="ggs-panel-title" data-glossar="biodiversitaet">Tierarten im Gebiet</div>
<div id="species-list" style="display:flex;flex-wrap:wrap;gap:4px;font-size:20px;min-height:28px;line-height:1.1">
<!-- dynamisch befuellt: gesichtete Arten voll sichtbar, fehlende ausgegraut -->
</div>
<div style="font-size:10px;color:var(--ggs-text-muted);margin-top:4px;line-height:1.3">
Mehr Biodiversität zieht mehr Arten an.
</div>
</div>
<div class="ggs-panel">
<div class="ggs-panel-title">Ziele</div>
<div class="ggs-param">
<span class="ggs-param-icon">🎯</span>
<span class="ggs-param-label">Biodiv. &gt; 60</span>
<span class="ggs-param-value" id="g-biodiv"></span>
</div>
<div class="ggs-param">
<span class="ggs-param-icon">🎯</span>
<span class="ggs-param-label">Keine Flut-Verluste</span>
<span class="ggs-param-value" id="g-flood"></span>
</div>
<div class="ggs-param">
<span class="ggs-param-icon">🎯</span>
<span class="ggs-param-label">Wirtschaft &gt; 50</span>
<span class="ggs-param-value" id="g-economy"></span>
</div>
</div>
<div class="ggs-panel">
<div class="ggs-panel-title">Erfolge</div>
<div id="badges-earned" style="display:flex;gap:6px;flex-wrap:wrap;min-height:36px">
<!-- wird dynamisch befuellt -->
</div>
</div>
</aside>
<!-- ─── MITTE: Simulation ─── -->
<section class="ggs-zone-sim">
<div class="ggs-topbar">
<div class="ggs-topbar-stat">
<span class="icon">📅</span>
<span class="val">Jahr <span id="t-year">0</span> / <span id="t-year-max">100</span></span>
</div>
<div class="ggs-topbar-stat">
<span class="icon">💰</span>
<span class="val" id="t-budget">60 Mio</span>
</div>
<div class="ggs-topbar-stat" id="t-weather-wrap">
<span class="icon" id="t-weather-icon"></span>
<span class="val" id="t-weather-txt">Normal</span>
</div>
<div class="ggs-header-spacer"></div>
<div class="ggs-topbar-stat">
<span class="icon">🏷️</span>
<span class="val" id="t-level-label">Lernen</span>
</div>
</div>
<!-- Canvas-Viewport fuellt die verbleibende Hoehe (flex:1) -->
<div id="sim-viewport" style="position:relative">
<canvas id="fluss-canvas"></canvas>
<div id="canvas-info" style="position:absolute;top:10px;left:10px;font-size:11px;color:var(--ggs-text-muted);background:rgba(255,255,255,.8);padding:4px 8px;border-radius:6px;pointer-events:none">
Maus über die Karte bewegen
</div>
</div>
<!-- Event-Notifications -->
<div class="ggs-event-viewport" id="event-viewport"></div>
<!-- Event-Stapel -->
<div class="ggs-event-stack" id="event-stack">
<div class="ggs-event-stack-list" id="event-stack-list"></div>
<button class="ggs-event-stack-toggle" onclick="toggleEventStack()">
📋 Ereignisse <span class="ggs-event-stack-count" id="event-count">0</span>
</button>
</div>
</section>
<!-- ─── RECHTS: Werkzeuge ─── -->
<aside class="ggs-zone-actions">
<div class="ggs-panel">
<div class="ggs-panel-title">Werkzeug</div>
<div id="tools-list">
<div class="ggs-card tool-active" data-tool="inspect">
<div class="ggs-card-header">
<span class="ggs-card-icon">🔍</span>
<span class="ggs-card-title">Ansehen</span>
</div>
<div class="ggs-card-desc">Klicken, um ein Feld zu untersuchen.</div>
</div>
<div class="ggs-card" data-tool="deich">
<div class="ggs-card-header">
<span class="ggs-card-icon">🛡️</span>
<span class="ggs-card-title" data-glossar="hochwasserschutz">Deich bauen</span>
</div>
<div class="ggs-card-desc">Schützt angrenzende Felder vor Hochwasser.</div>
<div class="ggs-card-badges">
<span class="ggs-badge cost">💰 8 Mio</span>
<span class="ggs-badge protect">🛡️ +Schutz</span>
</div>
</div>
<div class="ggs-card" data-tool="feld">
<div class="ggs-card-header">
<span class="ggs-card-icon">🌾</span>
<span class="ggs-card-title">Feld anlegen</span>
</div>
<div class="ggs-card-desc">Mehr Nahrung, aber weniger Biodiversität.</div>
<div class="ggs-card-badges">
<span class="ggs-badge cost">💰 4 Mio</span>
<span class="ggs-badge effect">🌾 +Nahrung</span>
</div>
</div>
<div class="ggs-card" data-tool="siedlung">
<div class="ggs-card-header">
<span class="ggs-card-icon">🏘️</span>
<span class="ggs-card-title">Siedlung bauen</span>
</div>
<div class="ggs-card-desc">Mehr Bevölkerung und Wirtschaft. Ungeschützt am Fluss riskant!</div>
<div class="ggs-card-badges">
<span class="ggs-badge cost">💰 12 Mio</span>
<span class="ggs-badge effect">💼 +Wirtschaft</span>
</div>
</div>
<div class="ggs-card disabled-progression" data-tool="wald" data-unlock="2">
<div class="ggs-card-header">
<span class="ggs-card-icon">🌳</span>
<span class="ggs-card-title" data-glossar="ufervegetation">Wald pflanzen</span>
</div>
<div class="ggs-card-desc">Stärkt Biodiversität und Retention am Ufer.</div>
<div class="ggs-card-badges">
<span class="ggs-badge cost">💰 6 Mio</span>
<span class="ggs-badge effect">🐟 +Biodiv.</span>
</div>
</div>
<div class="ggs-card disabled-progression" data-tool="abriss" data-unlock="2">
<div class="ggs-card-header">
<span class="ggs-card-icon">🧨</span>
<span class="ggs-card-title">Abriss</span>
</div>
<div class="ggs-card-desc">Entfernt ein gebautes Feld, gibt etwas Budget zurück.</div>
<div class="ggs-card-badges">
<span class="ggs-badge cost">💰 2 Mio</span>
</div>
</div>
<div class="ggs-card disabled-progression" data-tool="begradigen" data-unlock="3">
<div class="ggs-card-header">
<span class="ggs-card-icon">📏</span>
<span class="ggs-card-title" data-glossar="flussbegradigung">Fluss begradigen</span>
</div>
<div class="ggs-card-desc">Gewinnt Bauland, aber schadet Biodiversität flussabwärts.</div>
<div class="ggs-card-badges">
<span class="ggs-badge cost">💰 15 Mio</span>
<span class="ggs-badge effect">⚠️ Folgen</span>
</div>
</div>
</div>
</div>
<div class="ggs-panel">
<div class="ggs-panel-title">Info</div>
<div id="tool-info" style="font-size:12px;color:var(--ggs-text-2);line-height:1.45">
Wähle rechts ein Werkzeug und klicke dann auf ein Feld.
</div>
</div>
</aside>
<!-- ─── UNTEN: Graphen ─── -->
<section class="ggs-zone-graphs">
<div class="ggs-graph-card" data-graph="biodiv">
<div class="ggs-graph-label" data-glossar="biodiversitaet">Biodiversität</div>
<div class="ggs-graph-value" id="g-value-biodiv" style="color:var(--ggs-moss);">80</div>
<svg class="ggs-graph-svg" viewBox="0 0 200 70" preserveAspectRatio="none">
<rect class="ggs-graph-zone-danger" x="0" y="50" width="200" height="20"/>
<rect class="ggs-graph-zone-warning" x="0" y="25" width="200" height="25"/>
<rect class="ggs-graph-zone-safe" x="0" y="0" width="200" height="25"/>
<polyline class="ggs-graph-line" stroke="var(--ggs-moss)" points="0,15"/>
</svg>
</div>
<div class="ggs-graph-card" data-graph="flood">
<div class="ggs-graph-label" data-glossar="hochwasserschutz">Hochwasserschutz</div>
<div class="ggs-graph-value" id="g-value-flood" style="color:var(--ggs-fjord);">30</div>
<svg class="ggs-graph-svg" viewBox="0 0 200 70" preserveAspectRatio="none">
<rect class="ggs-graph-zone-danger" x="0" y="50" width="200" height="20"/>
<rect class="ggs-graph-zone-warning" x="0" y="25" width="200" height="25"/>
<rect class="ggs-graph-zone-safe" x="0" y="0" width="200" height="25"/>
<polyline class="ggs-graph-line" stroke="var(--ggs-fjord)" points="0,50"/>
</svg>
</div>
<div class="ggs-graph-card" data-graph="economy">
<div class="ggs-graph-label">Wirtschaft</div>
<div class="ggs-graph-value" id="g-value-economy" style="color:var(--ggs-sand);">50</div>
<svg class="ggs-graph-svg" viewBox="0 0 200 70" preserveAspectRatio="none">
<rect class="ggs-graph-zone-safe" x="0" y="0" width="200" height="45"/>
<rect class="ggs-graph-zone-danger" x="0" y="45" width="200" height="25"/>
<polyline class="ggs-graph-line" stroke="var(--ggs-sand)" points="0,35"/>
</svg>
</div>
<div class="ggs-graph-card" data-graph="budget">
<div class="ggs-graph-label">Budget</div>
<div class="ggs-graph-value" id="g-value-budget" style="color:var(--ggs-moss);">60 Mio</div>
<svg class="ggs-graph-svg" viewBox="0 0 200 70" preserveAspectRatio="none">
<rect class="ggs-graph-zone-safe" x="0" y="0" width="200" height="45"/>
<rect class="ggs-graph-zone-danger" x="0" y="45" width="200" height="25"/>
<polyline class="ggs-graph-line" stroke="var(--ggs-moss)" points="0,20"/>
</svg>
</div>
</section>
</main>
<!-- ============================================================
ACHIEVEMENT TOAST
============================================================ -->
<div class="ggs-toast-container"></div>
<!-- ============================================================
INITIALER LEVEL-PICKER — Erststart ohne ?level=
============================================================ -->
<div class="ggs-overlay" id="level-select-overlay" style="display:none">
<div class="ggs-overlay-card" style="max-width:560px">
<div style="font-size:42px">🏞️</div>
<h2 style="font-size:22px;font-weight:900;color:var(--ggs-fjord-dark);margin:8px 0 4px">Flussmanagement</h2>
<p style="color:var(--ggs-text-muted);font-size:13px;margin-bottom:6px">
Du bist <b>verantwortlich für ein Flusstal</b>. 100 Jahre lang lenkst du Siedlungen,
Felder, Wälder und Hochwasserschutz — der Fluss verändert jedes Jahr seinen Lauf.
Wähle deinen Schwierigkeitsgrad.
</p>
<div id="level-select-grid">
<!-- dynamisch via renderLevelGrid() befuellt -->
</div>
</div>
</div>
<!-- ============================================================
TUTORIAL OVERLAY — 4 Karten (Phase 2/3 wird befuellt)
============================================================ -->
<div class="ggs-overlay" id="tutorial-overlay" style="display:none">
<div class="ggs-overlay-card ggs-tutorial">
<div class="ggs-tutorial-dots" id="tutorial-dots">
<!-- dynamisch befuellt passend zur Anzahl Karten -->
</div>
<div class="ggs-tutorial-title" id="tutorial-title">Willkommen am Fluss</div>
<div class="ggs-tutorial-text" id="tutorial-text">
Du arbeitest 100 Jahre mit diesem Fluss. Er verändert jedes Jahr seinen Lauf.
</div>
<div style="display:flex;gap:8px;justify-content:center;margin-top:14px">
<button class="ggs-btn ggs-btn-ghost" id="tutorial-skip">Überspringen</button>
<button class="ggs-btn ggs-btn-primary" id="tutorial-next">Weiter →</button>
</div>
</div>
</div>
<!-- ============================================================
BETWEEN-LEVEL SCREEN
============================================================ -->
<div class="ggs-overlay" id="level-overlay" style="display:none">
<div class="ggs-overlay-card ggs-level-screen">
<div style="font-size:48px">🎉</div>
<h2 style="font-size:20px;font-weight:800;color:var(--ggs-fjord-dark);margin:12px 0 4px" id="level-title">Level geschafft!</h2>
<div class="ggs-stars" id="level-stars">
<span class="ggs-star"></span><span class="ggs-star"></span><span class="ggs-star"></span><span class="ggs-star"></span><span class="ggs-star"></span>
</div>
<div class="ggs-badges-earned" id="level-badges"></div>
<div class="ggs-reflection">
<div class="ggs-reflection-q" id="reflection-q">Was war am schwierigsten?</div>
<div class="ggs-reflection-options" id="reflection-options">
<!-- dynamisch -->
</div>
</div>
<button class="ggs-btn ggs-btn-primary ggs-mt-md" id="btn-next-level">Weiter →</button>
</div>
</div>
<!-- ============================================================
EVENT-INFO-OVERLAY — didaktische Langfassung zu einem Ereignis
============================================================ -->
<div class="ggs-overlay" id="topic-overlay" style="display:none">
<div class="ggs-overlay-card" style="max-width:480px;text-align:left">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
<div id="topic-icon" style="font-size:32px"></div>
<div id="topic-title" style="font-size:18px;font-weight:800;color:var(--ggs-fjord-dark)"></div>
</div>
<div id="topic-body" style="font-size:14px;line-height:1.55;color:var(--ggs-text-2);margin-bottom:14px"></div>
<div style="display:flex;gap:8px;justify-content:space-between;align-items:center">
<a id="topic-link" href="#" target="_blank" rel="noopener" class="ggs-btn ggs-btn-ghost" style="font-size:12px">Mehr auf Wikipedia ↗</a>
<button class="ggs-btn ggs-btn-primary" onclick="hideOverlay('topic-overlay')">Schließen</button>
</div>
</div>
</div>
<!-- ============================================================
RESET-OVERLAY — freie Session darf die Stufe beim Neustart waehlen
============================================================ -->
<div class="ggs-overlay" id="reset-overlay" style="display:none">
<div class="ggs-overlay-card ggs-results" style="max-width:420px">
<div style="font-size:40px">🔄</div>
<div class="ggs-results-title" style="margin-top:8px">Neu beginnen?</div>
<div class="ggs-results-subtitle" style="margin-bottom:16px">
Dein aktueller Fortschritt wird gelöscht.
Wähle die Stufe für den nächsten Durchgang.
</div>
<div id="reset-actions" style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap"></div>
</div>
</div>
<!-- ============================================================
ENDSCREEN
============================================================ -->
<div class="ggs-overlay" id="end-overlay" style="display:none">
<div class="ggs-overlay-card ggs-results">
<!-- ===== FELD 1: Rückmeldung ===== -->
<div id="end-step1">
<div class="ggs-results-icon">🏆</div>
<div class="ggs-results-title" id="end-title">Simulation abgeschlossen!</div>
<div class="ggs-results-subtitle" id="end-subtitle">Du hast den Fluss 100 Jahre lang gemanagt.</div>
<div class="ggs-stars" id="end-stars">
<span class="ggs-star"></span><span class="ggs-star"></span><span class="ggs-star"></span><span class="ggs-star"></span><span class="ggs-star"></span>
</div>
<div class="ggs-results-stats" id="end-stats"></div>
<!-- Verdiente Erfolgs-Badges (nebeneinander, leer = ausgegraut) -->
<div class="ggs-badges-earned" id="end-badges" style="display:flex;flex-wrap:wrap;justify-content:center;gap:6px;margin:10px 0"></div>
<!-- Reflexionsfrage — Pflicht vor dem Weiter-Button -->
<div class="ggs-reflection" id="end-reflection" style="margin:10px 0 14px">
<div class="ggs-reflection-q" id="end-reflection-q">Was war in diesem Durchgang deine wirksamste Entscheidung?</div>
<div id="end-reflection-options"><!-- dynamisch befuellt --></div>
</div>
<div style="display:flex;justify-content:center">
<button id="end-weiter" class="ggs-btn ggs-btn-primary" disabled
onclick="document.getElementById('end-step1').style.display='none';document.getElementById('end-step2').style.display=''">Weiter →</button>
</div>
</div>
<!-- ===== FELD 2: Wie geht's weiter? ===== -->
<div id="end-step2" style="display:none">
<div class="ggs-results-icon">🚀</div>
<div class="ggs-results-title">Wie geht's weiter?</div>
<!-- Aktions-Leiste (Stufen-Auswahl bzw. Erneut versuchen + Zurück zur Startseite) -->
<div id="end-actions" style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap"></div>
</div>
</div>
</div>
<!-- ============================================================
GLOSSAR — temporaerer Fallback, bis Glossar-API greift.
Glossar-Instanz pflegt die echten Texte in der DB; bis dahin zeigen
die data-glossar-Attribute aus diesem Objekt Fallback-Popups.
============================================================ -->
<script>
var GLOSSAR = {
biodiversitaet: {
title: 'Biodiversität',
text: 'Die Vielfalt von Tieren, Pflanzen und Lebensräumen. Je mehr verschiedene Arten in einem Fluss und an seinen Ufern leben, desto stabiler ist das Ökosystem.',
link: 'https://de.wikipedia.org/wiki/Biodiversität'
},
hochwasserschutz: {
title: 'Hochwasserschutz',
text: 'Maßnahmen, die verhindern, dass ein Fluss Siedlungen und Felder überflutet: Deiche, Rückhalteflächen, Renaturierung. Reine Deiche helfen lokal, verlagern das Problem aber flussabwärts.',
link: 'https://de.wikipedia.org/wiki/Hochwasserschutz'
},
flussbegradigung: {
title: 'Flussbegradigung',
text: 'Mäander (Schleifen) werden künstlich begradigt, um Bauland zu gewinnen. Das Wasser fließt schneller — flussabwärts steigt die Hochwassergefahr, und wichtige Lebensräume gehen verloren.',
link: 'https://de.wikipedia.org/wiki/Flussbegradigung'
},
ufervegetation: {
title: 'Ufervegetation',
text: 'Pflanzen am Flussufer (Gräser, Sträucher, Bäume). Sie halten Erde fest, bremsen Hochwasser und bieten vielen Tieren Lebensraum.',
link: 'https://de.wikipedia.org/wiki/Auwald'
}
};
</script>
<!-- ============================================================
SPIELLOGIK — separate Engine fuer Single-Source-of-Truth.
game.html kuemmert sich nur um Darstellung und User-Input.
============================================================ -->
<script src="engine.js"></script>
<script src="audio.js"></script>
<!-- ============================================================
JAVASCRIPT — DOM-Anbindung, Canvas-Render, Event-Handling
============================================================ -->
<script>
/* ------------------------------------------------------------
Basis-Kontext aus PHP — __GGS__ wird erst am Seitenende injiziert,
also lazy per getCtx() lesen, nicht synchron beim Script-Load.
------------------------------------------------------------ */
var FALLBACK_CTX = {
sessionId: '', studentName: 'Gast', classId: null,
simId: 'fluss', simName: 'Flussmanagement', level: 1,
levelConfig: {
difficulty: 'easy', label: 'Lernen', timeLimit: 100, startBudget: 60,
startPopulation: 200, incomePerYear: 6, eventFrequency: 0.25,
toolsAvailable: ['inspect','deich','feld','siedlung']
},
baseUrl: '', basePath: '', apiUrl: ''
};
function getCtx() { return window.__GGS__ || FALLBACK_CTX; }
/* ------------------------------------------------------------
Speed Control
------------------------------------------------------------ */
document.querySelectorAll('.ggs-speed-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
document.querySelectorAll('.ggs-speed-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
var speed = parseInt(btn.dataset.speed);
if (typeof onSpeedChange === 'function') onSpeedChange(speed);
});
});
document.addEventListener('keydown', function(e) {
var t = e.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA')) return;
if (e.key === 'p' || e.key === 'P') document.querySelector('[data-speed="0"]').click();
if (e.key === '1') document.querySelector('[data-speed="1"]').click();
if (e.key === '2') document.querySelector('[data-speed="2"]').click();
if (e.key === '3') document.querySelector('[data-speed="4"]').click();
});
/* ------------------------------------------------------------
Overlay Show/Hide
------------------------------------------------------------ */
function showOverlay(id) { var el = document.getElementById(id); if (!el) return; el.style.display = ''; el.classList.add('visible'); }
function hideOverlay(id) { var el = document.getElementById(id); if (!el) return; el.classList.remove('visible'); setTimeout(function(){ el.style.display = 'none'; }, 300); }
/* ------------------------------------------------------------
Achievement Toast
------------------------------------------------------------ */
function showAchievement(icon, title, subtitle) {
var container = document.querySelector('.ggs-toast-container');
var toast = document.createElement('div');
toast.className = 'ggs-toast';
toast.innerHTML = '<span class="ggs-toast-icon">' + icon + '</span><div><div class="ggs-toast-text">' + title + '</div><div class="ggs-toast-sub">' + subtitle + '</div></div>';
container.appendChild(toast);
setTimeout(function() { toast.remove(); }, 4500);
}
/* ------------------------------------------------------------
In-Canvas Events
------------------------------------------------------------ */
var _eventList = [];
function showEvent(icon, text, type, topic) {
type = type || 'neutral';
var vp = document.getElementById('event-viewport');
var n = document.createElement('div');
n.className = 'ggs-event-notification ' + type;
if (topic) n.setAttribute('data-topic', topic);
n.innerHTML = '<span class="ev-icon">' + icon + '</span> ' + text;
if (topic && TOPIC_INFO[topic]) {
n.style.cursor = 'pointer';
n.addEventListener('click', function() { openTopicInfo(topic); });
}
vp.appendChild(n);
setTimeout(function() { n.remove(); }, 3800);
_eventList.unshift({ icon: icon, text: text, type: type, topic: topic || null,
year: (window.FLUSS && FLUSS.year) || 0 });
if (_eventList.length > 30) _eventList.pop();
renderEventStack();
}
function renderEventStack() {
var list = document.getElementById('event-stack-list');
var countEl = document.getElementById('event-count');
countEl.textContent = _eventList.length;
list.innerHTML = _eventList.map(function(e, idx) {
var clickable = e.topic && TOPIC_INFO[e.topic] ? ' data-topic="' + e.topic + '" style="cursor:pointer"' : '';
return '<div class="ggs-event-stack-item ' + e.type + '"' + clickable + ' data-idx="' + idx + '">' + e.icon + ' ' + e.text + '</div>';
}).join('');
// Event-Delegation: Klick auf Topic-Eintrag oeffnet Info-Overlay.
list.querySelectorAll('[data-topic]').forEach(function(el) {
el.addEventListener('click', function() { openTopicInfo(el.dataset.topic); });
});
}
function toggleEventStack() { document.getElementById('event-stack').classList.toggle('open'); }
/* ------------------------------------------------------------
Event-Info-Topics (Phase 3d) — didaktische Langfassungen
------------------------------------------------------------ */
var TOPIC_INFO = {
'flood-settlement': {
icon: '🌊',
title: 'Hochwasser trifft Siedlung',
body: 'Wenn ein Fluss keinen natürlichen Platz zum Wandern hat oder eine Starkflut kommt, bricht er über die Ufer. Alles, was im alten Flussbett oder in der nächsten Auenzone gebaut ist, wird überspült oder fortgerissen. Kurzfristig hilft ein Deich — langfristig braucht es Überschwemmungsgebiete und Rückhalteflächen, damit der Fluss Raum hat, ohne Häuser zu zerstören.',
link: 'https://de.wikipedia.org/wiki/Hochwasserschutz',
},
'flood-field': {
icon: '🌾',
title: 'Feld unter Wasser',
body: 'Ein Feld direkt am Fluss ist besonders fruchtbar — Sedimente bringen Mineralien. Bleibt das Wasser aber länger als zwei Jahre stehen, ersticken die Wurzeln, das Feld stirbt und wird wieder zur Wiese. Didaktisch: Ertrag und Risiko liegen nah beieinander. Die Lösung ist selten "Deich bauen", sondern oft "weiter weg bauen".',
link: 'https://de.wikipedia.org/wiki/Aue_(Gewässer)',
},
'levee-hold': {
icon: '🛡️',
title: 'Deich hält',
body: 'Der Deich drängt das Wasser zurück und schützt die Felder und Siedlung dahinter. Achtung: Der Fluss verlagert sich mit der Zeit um den Deich herum — und flussabwärts kommt mehr Wasser an. Deiche schützen lokal, verlagern das Problem aber oft nur.',
link: 'https://de.wikipedia.org/wiki/Deich',
},
'levee-break': {
icon: '🧱',
title: 'Deich weggespült',
body: 'Bei langer Überflutung (Dauerflut) weicht der Deich auf. Irgendwann reißt die Wasserwand durch — schlagartig steht alles hinter dem Deich unter Wasser. Deichbruch ist oft schlimmer als gar kein Deich, weil sich die Bewohner darauf verlassen haben.',
link: 'https://de.wikipedia.org/wiki/Deichbruch',
},
'tree-flood-die': {
icon: '🌳',
title: 'Wald im Dauerhochwasser abgestorben',
body: 'Auwald hält zeitweise nasse Füße aus — aber nicht dauerhaft. Wenn ein Baum Jahr für Jahr unter Wasser steht, sterben seine Wurzeln. Das Ufer verliert seinen natürlichen Schutz; Erosion und Hochwasser-Schäden nehmen zu.',
link: 'https://de.wikipedia.org/wiki/Auwald',
},
'tree-straighten-die': {
icon: '🌳',
title: 'Baum neben Begradigung vertrocknet',
body: 'Ein begradigter Fluss fließt schneller, tieft sich ein und der Grundwasserspiegel sinkt. Bäume am Ufer verlieren die Feuchtigkeit und sterben ab. Besonders sichtbar: Weiden, Erlen und andere typische Auwald-Arten verschwinden als Erstes.',
link: 'https://de.wikipedia.org/wiki/Flussbegradigung',
},
'begradigen': {
icon: '📏',
title: 'Fluss begradigt',
body: 'Die Mäander sind weg, der Fluss fließt gerade durch das Tal. Bauland entsteht, aber der Fluss tieft sich ein, Ufer-Lebensräume verschwinden, Fischbestände brechen ein und flussabwärts gibt es mehr Hochwasser. Historisch wurden viele Alpen-Flüsse im 19./20. Jh. begradigt; seit den 1990ern werden viele davon renaturiert.',
link: 'https://de.wikipedia.org/wiki/Flussbegradigung',
},
'renaturierung': {
icon: '🦋',
title: 'Renaturierung gelungen',
body: 'Die Begradigung ist aufgelöst, der Fluss darf wieder mäandrieren. Das braucht Platz (und damit oft: zurückgegebenes Bauland), belebt aber Biodiversität und Hochwasserschutz gleichzeitig. Die Natur macht einen Teil der Arbeit kostenlos, die du sonst mit Deichen bezahlst.',
link: 'https://de.wikipedia.org/wiki/Renaturierung_(Flüsse)',
},
};
function openTopicInfo(topic) {
var info = TOPIC_INFO[topic];
if (!info) return;
var overlay = document.getElementById('topic-overlay');
document.getElementById('topic-icon').textContent = info.icon;
document.getElementById('topic-title').textContent = info.title;
document.getElementById('topic-body').textContent = info.body;
var link = document.getElementById('topic-link');
if (info.link) {
link.href = info.link;
link.style.display = '';
} else {
link.style.display = 'none';
}
showOverlay('topic-overlay');
}
/* ------------------------------------------------------------
Graph-Zoom on Click
------------------------------------------------------------ */
document.querySelectorAll('.ggs-graph-card').forEach(function(card) {
card.addEventListener('click', function() {
var label = card.querySelector('.ggs-graph-label');
var value = card.querySelector('.ggs-graph-value');
var svg = card.querySelector('.ggs-graph-svg');
if (!svg) return;
var overlay = document.createElement('div');
overlay.className = 'ggs-graph-zoom-overlay';
overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
var zoomCard = document.createElement('div');
zoomCard.className = 'ggs-graph-zoom-card';
var header = document.createElement('div');
header.className = 'ggs-graph-zoom-header';
header.innerHTML = '<div class="ggs-graph-label">' + (label ? label.textContent : '') + '</div>'
+ '<div class="ggs-graph-value" style="' + (value ? value.getAttribute('style') || '' : '') + '">' + (value ? value.textContent : '') + '</div>';
var bigSvg = svg.cloneNode(true);
bigSvg.style.height = '100%'; bigSvg.style.flex = '1';
bigSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
zoomCard.appendChild(header); zoomCard.appendChild(bigSvg);
overlay.appendChild(zoomCard);
document.body.appendChild(overlay);
document.addEventListener('keydown', function close(e) {
if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', close); }
});
});
});
/* ------------------------------------------------------------
Glossar-Tooltips (Fallback bis DB-Anbindung greift)
------------------------------------------------------------ */
var _glossarPopup = null;
document.addEventListener('click', function(e) {
if (_glossarPopup && !_glossarPopup.contains(e.target)) {
_glossarPopup.remove();
_glossarPopup = null;
}
var el = e.target.closest('[data-glossar]');
if (!el) return;
var key = el.dataset.glossar;
var entry = GLOSSAR[key];
if (!entry) return;
if (_glossarPopup) { _glossarPopup.remove(); _glossarPopup = null; }
var popup = document.createElement('div');
popup.className = 'ggs-glossar-popup';
var html = '<div class="ggs-glossar-title">' + entry.title + '</div>';
html += '<div class="ggs-glossar-text">' + entry.text + '</div>';
if (entry.unit) html += '<div class="ggs-glossar-unit">' + entry.unit + '</div>';
if (entry.link) html += '<br><a href="' + entry.link + '" target="_blank" class="ggs-glossar-link">Mehr erfahren →</a>';
popup.innerHTML = html;
document.body.appendChild(popup);
_glossarPopup = popup;
var rect = el.getBoundingClientRect();
var pw = popup.offsetWidth;
var left = Math.max(8, Math.min(rect.left, window.innerWidth - pw - 8));
popup.style.left = left + 'px';
popup.style.top = (rect.bottom + 8) + 'px';
});
/* ------------------------------------------------------------
Init bei DOMContentLoaded — lauft NACH der PHP-Injection von __GGS__.
Hier Header befuellen, Tools freischalten, Lehrplan-Link setzen,
Music-Player starten.
------------------------------------------------------------ */
document.addEventListener('DOMContentLoaded', function() {
var ctx = getCtx();
var lvl = ctx.level || 1;
var lc = ctx.levelConfig || {};
document.getElementById('level-badge').textContent = 'Level ' + lvl;
document.getElementById('t-level-label').textContent = lc.label || '';
document.getElementById('t-year-max').textContent = (lc.timeLimit || 100);
document.getElementById('t-budget').textContent = (lc.startBudget || 60) + ' Mio';
// Werkzeug-Freigabe pro Level
var available = (lc.toolsAvailable || []).reduce(function(o,k){ o[k]=true; return o; }, {});
document.querySelectorAll('#tools-list .ggs-card').forEach(function(card) {
var tool = card.dataset.tool;
if (available[tool]) card.classList.remove('disabled-progression');
});
// Lehrplan-Link
var link = document.getElementById('ggs-lehrplan-link');
if (link) {
link.href = (ctx.baseUrl || '') + '/modul-' + (ctx.simId || 'fluss') + '#lehrplan-bezug';
}
// Music-Player
ggsMusicSetup([
{ key: 'rain', label: '🌧 Porcelain Rain', file: 'assets/music/porcelain-rain.mp3' }
]);
});
/* ------------------------------------------------------------
Loading Screen
------------------------------------------------------------ */
window.addEventListener('load', function() {
var ls = document.getElementById('loading-screen');
if (ls) {
setTimeout(function() {
ls.classList.add('fade-out');
setTimeout(function() { ls.remove(); }, 600);
}, 400);
}
});
/* ------------------------------------------------------------
Music Player
------------------------------------------------------------ */
window.ggsMusicSetup = function(tracks) {
var el = document.getElementById('ggs-music');
var sel = document.getElementById('ggs-music-sel');
var btn = document.getElementById('ggs-music-toggle');
var vol = document.getElementById('ggs-music-vol');
if (!el || !tracks || !tracks.length) return;
el.hidden = false;
var audio = new Audio();
audio.loop = true;
var simId = (getCtx().simId) || 'fluss';
var lsKey = 'ggs-music-' + simId;
var state = {};
try { state = JSON.parse(localStorage.getItem(lsKey) || '{}'); } catch {}
sel.innerHTML = tracks.map(function(t) {
return '<option value="'+t.key+'" data-file="'+t.file+'">'+t.label+'</option>';
}).join('');
if (state.track) sel.value = state.track;
audio.src = sel.options[sel.selectedIndex].dataset.file;
audio.volume = (state.volume != null ? state.volume : 35) / 100;
vol.value = state.volume != null ? state.volume : 35;
function save() {
localStorage.setItem(lsKey, JSON.stringify({ track: sel.value, volume: +vol.value, playing: !audio.paused }));
}
sel.addEventListener('change', function() {
var wasPlaying = !audio.paused;
audio.src = sel.options[sel.selectedIndex].dataset.file;
if (wasPlaying) audio.play().catch(function(){});
save();
});
btn.addEventListener('click', function() {
if (audio.paused) {
audio.play().then(function() { btn.textContent = '⏸'; btn.classList.add('playing'); save(); }).catch(function(){});
} else {
audio.pause(); btn.textContent = '▶'; btn.classList.remove('playing'); save();
}
});
vol.addEventListener('input', function() { audio.volume = vol.value / 100; save(); });
if (state.playing) {
var resume = function() {
audio.play().then(function() { btn.textContent = '⏸'; btn.classList.add('playing'); }).catch(function(){});
document.removeEventListener('click', resume);
document.removeEventListener('keydown', resume);
};
document.addEventListener('click', resume, { once: true });
document.addEventListener('keydown', resume, { once: true });
}
};
/* ------------------------------------------------------------
API — Fortschritt / Assessment / Reflexion
------------------------------------------------------------ */
function reportProgress(data) {
var c = getCtx(); if (!c.sessionId) return;
fetch((c.baseUrl || '') + '/php/api/progress.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: c.sessionId, sim_id: c.simId || 'fluss', action: 'update', data: data })
}).catch(function(){});
}
function submitAssessment(results) {
var c = getCtx(); if (!c.sessionId) return;
return fetch((c.baseUrl || '') + '/php/api/progress.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: c.sessionId, sim_id: c.simId || 'fluss', action: 'submit_assessment', data: results })
}).catch(function(){});
}
function submitReflection(levelNum, question, answer) {
var c = getCtx(); if (!c.sessionId) return;
fetch((c.baseUrl || '') + '/php/api/progress.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: c.sessionId, sim_id: c.simId || 'fluss', action: 'reflection',
data: { level: levelNum, question: question, answer: answer } })
}).catch(function(){});
}
/* ------------------------------------------------------------
Reset-Button: bootFluss registriert den echten Handler (muss das
beforeunload-Autosave abschalten, sonst schreibt es den geloeschten
Stand direkt wieder in localStorage).
------------------------------------------------------------ */
/* ------------------------------------------------------------
SPIEL-STATE + RENDER-LOOP — verbindet Engine mit DOM/Canvas
------------------------------------------------------------ */
(function bootFluss() {
var E = window.FlussEngine;
if (!E) { console.error('FlussEngine nicht geladen'); return; }
var game = null;
var selectedTool = 'inspect';
var speed = 1;
var MS_PER_TICK = 10000; // 1× = 10 s pro Jahr (ruhig); 2× = 5 s; 4× = 2.5 s
var tickAccum = 0;
var lastFrame = 0;
var animT = 0;
var canvas = document.getElementById('fluss-canvas');
var ctxCv = canvas.getContext('2d');
var W = 0, H = 0, tw = 0, th = 0;
function init() {
var ctx = getCtx();
// Autosave laden oder frisches Spiel anlegen
var saveKey = 'ggs-save-fluss-' + (ctx.level || 1);
var saved = null;
try { saved = localStorage.getItem(saveKey); } catch {}
var hasSave = false;
if (saved) {
var restored = E.deserialize(saved);
if (restored) {
game = restored;
hasSave = true;
showEvent('💾', 'Arbeitsstand geladen — weiter bei Jahr ' + game.year, 'neutral');
}
}
if (!game) {
game = E.createGame({ level: ctx.level || 1, levelConfig: ctx.levelConfig || {} });
pumpEvents();
if (window.flAudio) flAudio.play('sim-start');
}
window.FLUSS = game; // Debug-Zugriff
resize();
renderUI();
requestAnimationFrame(loop);
// Initialer Level-Picker: nur wenn URL keinen ?level= hat UND kein Save existiert
// (Lehrperson-Direktlinks mit ?level=N ueberspringen den Picker).
var hasLevelInUrl = /[?&]level=\d+/i.test(location.search);
if (!hasSave && !hasLevelInUrl) {
renderLevelGrid(document.getElementById('level-select-grid'));
speedBeforeTutorial = speed;
speed = 0;
showOverlay('level-select-overlay');
return;
}
// Tutorial nur beim allerersten Durchgang zeigen — weder nach Reload mit
// Save noch nach einem abgeschlossenen Durchlauf.
if (!hasSave && !tutorialSeen()) {
setTimeout(startTutorial, 600);
}
}
/* ---------- Tutorial-Flow ---------- */
var TUTORIAL = [
{
title: 'Willkommen am Fluss',
text: 'Du arbeitest 100 Jahre mit diesem Fluss. Er verändert jedes Jahr seinen Lauf — plane voraus.',
},
{
title: 'Fünf Zustände im Blick',
text: 'Links siehst du Bevölkerung, Nahrung, Biodiversität, Hochwasserschutz und Wirtschaft. Deine Arbeit ist, alle fünf im Gleichgewicht zu halten.',
},
{
title: 'Werkzeuge rechts',
text: 'Wähle ein Werkzeug und klicke dann auf ein Feld. Felder und Siedlungen nah am Fluss bringen mehr — sind aber hochwassergefährdet.',
},
{
title: 'Trade-off erkennen',
text: 'Felder bringen Nahrung, belasten aber die Natur. Wald und Renaturierung stärken die Biodiversität. Deiche schützen, können aber brechen.',
},
{
title: 'Los gehts',
text: 'Tempo-Regler oben: ⏸ Pause, 1×/2×/4× beschleunigt. Tastatur: P pausiert, 1/2/3 wechselt Tempo. Viel Erfolg!',
},
];
var tutorialStep = 0;
function tutorialSeen() {
try { return localStorage.getItem('ggs-fluss-tutorial-seen') === '1'; } catch { return false; }
}
function markTutorialSeen() {
try { localStorage.setItem('ggs-fluss-tutorial-seen', '1'); } catch {}
}
function renderTutorialStep() {
document.getElementById('tutorial-title').textContent = TUTORIAL[tutorialStep].title;
document.getElementById('tutorial-text').textContent = TUTORIAL[tutorialStep].text;
var dots = document.getElementById('tutorial-dots');
dots.innerHTML = '';
for (var i = 0; i < TUTORIAL.length; i++) {
var d = document.createElement('div');
d.className = 'ggs-tutorial-dot' + (i === tutorialStep ? ' active' : '');
dots.appendChild(d);
}
var nextBtn = document.getElementById('tutorial-next');
nextBtn.textContent = (tutorialStep === TUTORIAL.length - 1) ? 'Start →' : 'Weiter →';
}
function startTutorial() {
tutorialStep = 0;
renderTutorialStep();
showOverlay('tutorial-overlay');
speedBeforeTutorial = speed;
speed = 0; // Spielmechanik pausieren, ohne Button-State zu veraendern
}
// Plattform-Convention: -Button im Header (siehe live-client.js GGS_TUTORIAL).
window.GGS_TUTORIAL = { simId: 'fluss', title: 'Flussmanagement', replay: startTutorial };
function advanceTutorial() {
if (tutorialStep < TUTORIAL.length - 1) {
tutorialStep++;
renderTutorialStep();
} else {
closeTutorial();
}
}
function closeTutorial() {
markTutorialSeen();
hideOverlay('tutorial-overlay');
speed = speedBeforeTutorial || 1;
}
var speedBeforeTutorial = 1;
document.getElementById('tutorial-next').addEventListener('click', advanceTutorial);
document.getElementById('tutorial-skip').addEventListener('click', closeTutorial);
/* ---------- Resize + Render-Loop ---------- */
function resize() {
var rect = canvas.parentElement.getBoundingClientRect();
var dpr = window.devicePixelRatio || 1;
var w = Math.max(100, rect.width);
var h = Math.max(100, rect.height);
canvas.width = w * dpr;
canvas.height = h * dpr;
ctxCv.setTransform(dpr, 0, 0, dpr, 0, 0);
W = w;
H = h;
tw = W / E.COLS;
th = H / E.ROWS;
}
window.addEventListener('resize', resize);
function loop(ts) {
if (!lastFrame) lastFrame = ts;
var dt = ts - lastFrame;
lastFrame = ts;
if (speed > 0 && !game.gameOver) {
tickAccum += dt * speed;
while (tickAccum >= MS_PER_TICK) {
tickAccum -= MS_PER_TICK;
// Status vor und nach dem Tick festhalten — erlaubt Trend-Pfeile
// und Feedback-Flashes in der UI.
var preStatus = E.getStatus(game);
var preYield = sumYield();
E.tick(game);
var postYield = sumYield();
var postStatus = E.getStatus(game);
showYearChanges(preStatus, postStatus);
pumpEvents();
if (window.flAudio) {
flAudio.play('year-tick');
if (postYield > 0 && postYield !== preYield) flAudio.play('harvest');
}
// Ernte-Animation: pro erntendem Feld fliegt ein 🌾 zur Nahrungs-Zeile.
if (postYield > 0) flyHarvestToFood();
// Fang-Animation: Fluss-Fische fliegen zu Siedlungen, die dieses
// Jahr laut Engine wirklich Fisch gefangen haben.
enqueueCatches();
autoSave();
renderUI();
}
}
// Smooth-Fluss auch pro Frame weiter interpolieren (fuer Animation)
for (var i = 0; i < E.ROWS; i++) {
game.riverColSmooth[i] += (game.riverCol[i] - game.riverColSmooth[i]) * 0.06;
}
draw();
requestAnimationFrame(loop);
}
/* ---------- Canvas zeichnen (portiert aus V1) ---------- */
function draw() {
animT += 0.012;
var s = Math.min(tw, th) * 0.3;
ctxCv.fillStyle = '#c8d8b0';
ctxCv.fillRect(0, 0, W, H);
// Tiles
for (var r = 0; r < E.ROWS; r++) for (var c = 0; c < E.COLS; c++) {
var x = c * tw, y = r * th;
var tile = game.land[r][c];
var dist = E.distToRiver(game, r, c);
var fi = game.floodIntensity[r][c] || 0;
// Feuchte nahe am Fluss nur noch sehr dezent (erkennt man kaum,
// stoert aber auch nicht). Richtiges Wasser kommt als sichtbare
// Welle durch drawFloodOverlay().
var moisture = Math.max(0, 1 - dist / 5) * 0.07;
var bR = 155, bG = 185, bB = 120; // meadow
if (tile === 'forest') { bR = 55; bG = 115; bB = 45; }
if (tile === 'field') { bR = 185; bG = 170; bB = 75; }
if (tile === 'settlement') { bR = 175; bG = 150; bB = 115; }
if (tile === 'levee') { bR = 135; bG = 110; bB = 80; }
if (tile === 'ruin') { bR = 125; bG = 110; bB = 95; }
if (tile === 'dead_tree') { bR = 105; bG = 125; bB = 75; }
// Leichter Feuchte-Shift am Ufer — nur als Kontext, nicht als Warnung
var fR = bR * (1 - moisture) + 145 * moisture | 0;
var fG = bG * (1 - moisture * 0.5) + 170 * moisture * 0.5 | 0;
var fB = bB * (1 - moisture) + 190 * moisture | 0;
ctxCv.fillStyle = 'rgb(' + fR + ',' + fG + ',' + fB + ')';
ctxCv.fillRect(x, y, tw + 0.5, th + 0.5);
// Hochwasser-Overlay: sichtbare Wellenschicht, je nach Intensitaet.
if (fi > 0.08 && tile !== 'levee') {
drawFloodOverlay(x, y, fi, r, c);
}
var cx = x + tw / 2, cy = y + th / 2;
if (tile === 'forest') {
ctxCv.fillStyle = '#4a3020';
ctxCv.fillRect(cx - s * 0.1, cy - s * 0.2, s * 0.2, s * 0.5);
ctxCv.fillStyle = r < E.ROWS * 0.3 ? '#1a5a2a' : '#2a7a2a';
ctxCv.beginPath(); ctxCv.arc(cx, cy - s * 0.4, s * 0.45, 0, Math.PI * 2); ctxCv.fill();
}
if (tile === 'settlement') {
// 3x3 Emoji-Raster: Haus in der Mitte, 8 Bewohner-Koepfe aussen.
// Gesichter spiegeln Feuchtigkeits-Stress: fi > 0.35 krank, 0.15-0.35
// teilweise, sonst gesund.
var pop = game.tilePop[r][c] || 0;
var efs = Math.max(7, Math.min(13, tw * 0.28));
var gx = tw * 0.32, gy = th * 0.32;
ctxCv.font = efs + 'px serif';
ctxCv.textAlign = 'center'; ctxCv.textBaseline = 'middle';
var healthy = ['👩','👨','👧','👦','👩‍🦱','👨‍🦰','👱‍♀️','🧑'];
var sick = ['😰','🤒','🤢','😷','🤒','😰','🤢','😷'];
for (var gr_ = 0; gr_ < 3; gr_++) for (var gc_ = 0; gc_ < 3; gc_++) {
var idx = gr_ * 3 + gc_;
if (idx === 4) { ctxCv.fillText('🏘️', cx, cy); continue; }
var slotIdx = idx > 4 ? idx - 1 : idx;
if (slotIdx >= pop) continue;
var emoji;
if (fi > 0.35) emoji = sick[slotIdx % 8];
else if (fi > 0.15) emoji = (slotIdx % 2 === 0) ? sick[slotIdx % 8] : healthy[slotIdx % 8];
else emoji = healthy[slotIdx % 8];
ctxCv.fillText(emoji, cx + (gc_ - 1) * gx, cy + (gr_ - 1) * gy);
}
ctxCv.textBaseline = 'alphabetic';
}
if (tile === 'field') {
// Stabiles Bild zwischen Ticks: Weizen-Slots bleiben sichtbar,
// Wasser-Slots uebernehmen bei Feuchte von unten. Kein Slot-fuer-Slot
// Nachwachsen, kein Leerphasen-Flackern. Der Zeitverlauf wird ueber
// den Sound + den Fortschritts-Graph rhythmisiert.
var yi = game.tileYield[r][c] || 0;
var waterSlots = fi > 0.15 ? Math.min(8, Math.round(fi * 12)) : 0;
var wheatSlots = Math.max(0, Math.min(8, yi) - waterSlots);
// Zentrum-Status (Feuchte-Hinweis), stabil zwischen Ticks
var status;
if (fi > 0.25) status = '💧';
else if (yi > 4) status = '👍';
else if (yi > 0) status = '🌱';
else status = '🥵';
var efs2 = Math.max(7, Math.min(13, tw * 0.28));
var gx2 = tw * 0.32, gy2 = th * 0.32;
ctxCv.font = efs2 + 'px serif';
ctxCv.textAlign = 'center'; ctxCv.textBaseline = 'middle';
for (var p = 0; p < 9; p++) {
if (p === 4) { ctxCv.fillText(status, cx, cy); continue; }
var slotIdx2 = p > 4 ? p - 1 : p;
var gr2 = Math.floor(p / 3), gc2 = p % 3;
var px2 = cx + (gc2 - 1) * gx2, py2 = cy + (gr2 - 1) * gy2;
var ch = '';
if (slotIdx2 < wheatSlots) ch = '🌾';
else if (slotIdx2 < wheatSlots + waterSlots) ch = '💧';
if (ch) ctxCv.fillText(ch, px2, py2);
}
ctxCv.textBaseline = 'alphabetic';
}
if (tile === 'levee') {
var w = s * 0.8 * (1 - fi * 0.5);
ctxCv.fillStyle = fi > 0.3 ? 'rgba(140,115,85,0.5)' : 'rgba(140,115,85,0.9)';
ctxCv.fillRect(cx - w, cy - s * 0.1, w * 2, s * 0.25);
}
if (tile === 'ruin') {
ctxCv.fillStyle = 'rgba(100,85,70,0.5)';
ctxCv.fillRect(cx - s * 0.3, cy - s * 0.15, s * 0.6, s * 0.3);
}
if (tile === 'dead_tree') {
ctxCv.strokeStyle = '#2a2a2a';
ctxCv.lineWidth = Math.max(0.5, s * 0.1);
ctxCv.beginPath();
ctxCv.moveTo(cx, cy + s * 0.2); ctxCv.lineTo(cx + s * 0.05, cy - s * 0.3);
ctxCv.moveTo(cx + s * 0.03, cy - s * 0.15); ctxCv.lineTo(cx - s * 0.2, cy - s * 0.4);
ctxCv.stroke();
}
}
// Gitter
ctxCv.strokeStyle = 'rgba(0,0,0,0.04)'; ctxCv.lineWidth = 0.3;
for (var rr = 0; rr <= E.ROWS; rr++) { ctxCv.beginPath(); ctxCv.moveTo(0, rr * th); ctxCv.lineTo(W, rr * th); ctxCv.stroke(); }
for (var cc = 0; cc <= E.COLS; cc++) { ctxCv.beginPath(); ctxCv.moveTo(cc * tw, 0); ctxCv.lineTo(cc * tw, H); ctxCv.stroke(); }
// Fluss als Kreise entlang der Punkte
var pts = [];
pts.push({ x: game.riverColSmooth[0] * tw + tw/2, y: -th, rad: game.riverRadius[0] * tw * 0.45, eng: game.riverEng[0] });
for (var i = 0; i < E.ROWS; i++) {
pts.push({ x: game.riverColSmooth[i] * tw + tw/2, y: i * th + th/2, rad: game.riverRadius[i] * tw * 0.45, eng: game.riverEng[i] });
}
pts.push({ x: game.riverColSmooth[E.ROWS-1] * tw + tw/2, y: H + th, rad: game.riverRadius[E.ROWS-1] * tw * 0.45, eng: game.riverEng[E.ROWS-1] });
for (var k = 0; k < pts.length - 1; k++) {
var p0 = pts[k], p1 = pts[k+1];
var steps = 12;
for (var st = 0; st <= steps; st++) {
var tt = st / steps;
var px = p0.x + (p1.x - p0.x) * tt;
var py = p0.y + (p1.y - p0.y) * tt;
var pr = p0.rad + (p1.rad - p0.rad) * tt;
var rr2 = pr + Math.sin(animT * 1.8 + py * 0.01) * pr * 0.06;
ctxCv.fillStyle = 'rgba(50,118,172,0.88)';
ctxCv.beginPath(); ctxCv.arc(px, py, rr2, 0, Math.PI * 2); ctxCv.fill();
}
}
// Uferbefestigung bei begradigten Segmenten
for (var m = 1; m < pts.length - 1; m++) {
var p = pts[m];
if (p.eng > 0) {
var engColors = ['', 'rgba(130,100,50,0.5)', 'rgba(110,110,110,0.6)', 'rgba(160,160,160,0.7)'];
ctxCv.strokeStyle = engColors[Math.min(3, p.eng)];
ctxCv.lineWidth = 1.5 + p.eng * 1.2;
var segH = th * 0.5;
ctxCv.beginPath(); ctxCv.moveTo(p.x - p.rad - 2, p.y - segH); ctxCv.lineTo(p.x - p.rad - 2, p.y + segH); ctxCv.stroke();
ctxCv.beginPath(); ctxCv.moveTo(p.x + p.rad + 2, p.y - segH); ctxCv.lineTo(p.x + p.rad + 2, p.y + segH); ctxCv.stroke();
}
}
// Fische: je eine (bei Nah-Siedlung zwei) Fisch-Emojis, die zwischen
// der Siedlung und dem naechstgelegenen Flusspunkt leicht bobben.
// Didaktik: sichtbar "Fisch kommt zur Siedlung" → mehr Nahrung flussnah.
drawFish();
}
// Neun Ambient-Fische. Jeder pendelt stabil in seiner Fluss-Sektion.
// Zustaende:
// 0 swim — normales Hin-und-Her in der Flussmitte
// 1 fly — dreht sich und fliegt zu einer nahe gelegenen Siedlung (Fang)
// 2 gone — kurze Pause unsichtbar, dann Respawn an neuer Fluss-Stelle
var FISH = null;
function initFish() {
FISH = [];
var N = 9;
for (var i = 0; i < N; i++) {
FISH.push({
phase: Math.random() * Math.PI * 2,
speed: 0.25 + Math.random() * 0.35,
span: 2 + Math.random() * 3,
centerRow: (i + 0.5) * E.ROWS / N,
mode: 0,
tStart: 0, tEnd: 0,
fromX: 0, fromY: 0, toX: 0, toY: 0,
});
}
}
function fishSwimPos(f) {
var osc = Math.sin(animT * f.speed + f.phase);
var yRow = f.centerRow + osc * f.span * 0.5;
yRow = Math.max(0, Math.min(E.ROWS - 1, yRow));
var i0 = Math.floor(yRow), frac = yRow - i0;
var i1 = Math.min(E.ROWS - 1, i0 + 1);
var rcA = game.riverColSmooth[i0];
var rcB = game.riverColSmooth[i1];
var rc = rcA + (rcB - rcA) * frac;
var rad = game.riverRadius[i0] + (game.riverRadius[i1] - game.riverRadius[i0]) * frac;
var lateral = Math.sin(animT * f.speed * 1.7 + f.phase * 1.3) * Math.min(tw * rad * 0.25, 6);
return {
x: rc * tw + tw / 2 + lateral,
y: yRow * th + th / 2,
yRow: yRow,
dv: Math.cos(animT * f.speed + f.phase),
};
}
// Fang-Queue: wird pro Tick aus engine.tileFishYield gefuellt, pro Frame
// abgearbeitet. Damit entspricht jede sichtbare Fisch-Fluglinie exakt einem
// in der Engine verbuchten Fang — keine Zufalls-Trigger mehr.
var catchQueue = [];
function enqueueCatches() {
if (!game.tileFishYield) return;
var now = performance.now();
for (var r = 0; r < E.ROWS; r++) {
var row = game.tileFishYield[r]; if (!row) continue;
for (var c = 0; c < E.COLS; c++) {
var yi = row[c] || 0;
if (yi <= 0) continue;
// Pro eingebrachtem Fisch ein Flug-Event (auf max 3 gedeckelt pro
// Siedlung, damit es nicht spammt).
var count = Math.min(3, Math.max(1, Math.round(yi)));
for (var n = 0; n < count; n++) {
catchQueue.push({ r: r, c: c, spawnAt: now + n * 350 + Math.random() * 250 });
}
}
}
}
// Maximaler Fisch-Fang-Radius in Kacheln. Fische, die weiter weg
// schwimmen, bleiben im Wasser — die Fanggrafik soll lokal sein.
var CATCH_MAX_TILES = 4;
function processCatchQueue(now) {
if (!catchQueue.length) return;
var maxPx = CATCH_MAX_TILES * Math.max(tw, th);
var maxPxSq = maxPx * maxPx;
for (var i = 0; i < catchQueue.length; i++) {
var entry = catchQueue[i];
if (entry.spawnAt > now) continue;
var tgtX = entry.c * tw + tw / 2;
var tgtY = entry.r * th + th / 2;
// Naechsten freien Fisch suchen (mode === 0 = swim)
var bestIdx = -1, bestD = 1e9;
for (var k = 0; k < FISH.length; k++) {
var ff = FISH[k];
if (ff.mode !== 0) continue;
var pp = fishSwimPos(ff);
var d = (pp.x - tgtX) * (pp.x - tgtX) + (pp.y - tgtY) * (pp.y - tgtY);
if (d < bestD) { bestD = d; bestIdx = k; }
}
if (bestIdx < 0) {
// Alle Fische beschaeftigt — kurz warten
if (now - entry.spawnAt > 2500) { catchQueue.splice(i, 1); return; }
continue;
}
if (bestD > maxPxSq) {
// Naechster Fisch ist zu weit weg — dieses Fang-Event verfaellt.
// (Der Ertrag ist in der Engine schon verbucht; nur die Grafik entfaellt.)
catchQueue.splice(i, 1);
return;
}
var f = FISH[bestIdx];
var start = fishSwimPos(f);
f.mode = 1;
f.tStart = now;
f.tEnd = now + 1400;
f.fromX = start.x; f.fromY = start.y;
f.toX = tgtX; f.toY = tgtY;
catchQueue.splice(i, 1);
return; // ein Dispatch pro Frame reicht — laesst Wellen entstehen
}
}
function updateFishState(now) {
processCatchQueue(now);
for (var k = 0; k < FISH.length; k++) {
var f = FISH[k];
if (f.mode === 1 && now >= f.tEnd) {
f.mode = 2;
f.tStart = now;
f.tEnd = now + 1200;
} else if (f.mode === 2 && now >= f.tEnd) {
f.mode = 0;
f.phase = Math.random() * Math.PI * 2;
f.centerRow = 0.5 + Math.random() * (E.ROWS - 1);
}
}
}
// Hochwasser als sichtbare Wellenschicht: halbtransparentes Blau +
// sanfte Wellenlinien + kleine Tropfen. Je hoeher fi, desto dichter.
// Statt subtiler Tile-Einfaerbung sieht die Lernende sofort, wo Wasser steht.
function drawFloodOverlay(x, y, fi, r, c) {
// Deckkraft: erst bei sichtbarer Intensitaet, dann steigend.
var alpha = Math.min(0.55, fi * 0.7);
ctxCv.fillStyle = 'rgba(70,135,185,' + alpha.toFixed(2) + ')';
ctxCv.fillRect(x, y, tw + 0.5, th + 0.5);
// Wellenbaender: horizontale Sinuskurven, animiert.
var bandCount = fi > 0.35 ? 3 : 2;
ctxCv.strokeStyle = 'rgba(255,255,255,' + Math.min(0.45, 0.15 + fi * 0.3).toFixed(2) + ')';
ctxCv.lineWidth = Math.max(0.8, tw * 0.03);
for (var b = 0; b < bandCount; b++) {
var yBase = y + th * (0.3 + b * 0.25);
ctxCv.beginPath();
for (var xi = 0; xi <= tw; xi += 3) {
var yy = yBase + Math.sin((xi * 0.3) + animT * 2.5 + r * 0.5 + c * 0.3 + b) * 1.2;
if (xi === 0) ctxCv.moveTo(x + xi, yy);
else ctxCv.lineTo(x + xi, yy);
}
ctxCv.stroke();
}
// Ab fi >= 0.35 zusaetzlich kleine Tropfen-Punkte — das Feld ist dann
// wirklich "abgesoffen".
if (fi >= 0.35) {
ctxCv.fillStyle = 'rgba(255,255,255,0.5)';
var drops = Math.round(2 + fi * 4);
var seed = r * 31 + c * 17;
for (var d = 0; d < drops; d++) {
var px = x + ((Math.sin(seed + d * 1.7) * 0.5 + 0.5) * tw * 0.8) + tw * 0.1;
var py = y + ((Math.cos(seed + d * 2.3) * 0.5 + 0.5) * th * 0.8) + th * 0.1
+ Math.sin(animT * 2 + d) * 0.8;
ctxCv.beginPath();
ctxCv.arc(px, py, Math.max(0.8, tw * 0.035), 0, Math.PI * 2);
ctxCv.fill();
}
}
}
function drawFish() {
if (!FISH) initFish();
var now = performance.now();
updateFishState(now);
var efs = Math.max(11, Math.min(16, tw * 0.32));
ctxCv.font = efs + 'px serif';
ctxCv.textAlign = 'center';
ctxCv.textBaseline = 'middle';
for (var k = 0; k < FISH.length; k++) {
var f = FISH[k];
if (f.mode === 2) continue; // unsichtbar
if (f.mode === 0) {
var p = fishSwimPos(f);
ctxCv.save();
ctxCv.translate(p.x, p.y);
if (p.dv > 0) ctxCv.scale(-1, 1);
ctxCv.fillText('🐟', 0, 0);
ctxCv.restore();
} else if (f.mode === 1) {
// Flug mit Drehung: Parabel-Bogen, Rotation beschleunigt
var t = Math.max(0, Math.min(1, (now - f.tStart) / (f.tEnd - f.tStart)));
var ease = t; // linear
var fx = f.fromX + (f.toX - f.fromX) * ease;
var fy = f.fromY + (f.toY - f.fromY) * ease - Math.sin(Math.PI * t) * 18; // leichter Sprungbogen
var rot = t * Math.PI * 3; // 1.5 volle Drehungen
var scale = 1 - t * 0.3; // etwas schrumpfen
ctxCv.save();
ctxCv.translate(fx, fy);
ctxCv.rotate(rot);
ctxCv.scale(scale, scale);
ctxCv.globalAlpha = 1 - t * 0.4;
ctxCv.fillText('🐟', 0, 0);
ctxCv.restore();
}
}
ctxCv.globalAlpha = 1;
ctxCv.textBaseline = 'alphabetic';
}
/* ---------- UI-Refresh (DOM aus Engine-Status) ---------- */
function renderUI() {
var st = E.getStatus(game);
document.getElementById('t-year').textContent = st.year;
document.getElementById('t-budget').textContent = st.budget + ' Mio';
document.getElementById('t-weather-icon').textContent = st.weather.icon;
document.getElementById('t-weather-txt').textContent = st.weather.name;
// Params (Panel links)
setParam('population', st.params.population + '', Math.min(100, st.params.population / 2), paramClass(st.params.population, 40, 120));
setParam('food', st.params.food, st.params.food, paramClass(st.params.food, 33, 66));
setParam('biodiv', st.params.biodiv, st.params.biodiv, paramClass(st.params.biodiv, 33, 66));
setParam('flood', st.params.flood, st.params.flood, paramClass(st.params.flood, 33, 66));
setParam('economy', st.params.economy, st.params.economy, paramClass(st.params.economy, 33, 66));
// Graphen — letzte ~40 Eintraege als Polyline
updateGraph('biodiv', st.params.biodiv);
updateGraph('flood', st.params.flood);
updateGraph('economy', st.params.economy);
updateGraph('budget', st.budget);
// Werkzeuge: "kann ich mir das leisten"-Opazitaet.
// Wenn das aktuell gewaehlte Werkzeug nicht mehr bezahlbar ist,
// automatisch auf "Ansehen" (kostenlos) zurueckfallen.
document.querySelectorAll('#tools-list .ggs-card').forEach(function(card) {
var toolId = card.dataset.tool;
var tool = E.TOOLS.find(function(t){ return t.id === toolId; });
if (!tool) return;
var locked = card.classList.contains('disabled-progression');
if (!locked) {
if (game.budget < tool.cost) card.classList.add('too-expensive');
else card.classList.remove('too-expensive');
}
});
var curTool = E.TOOLS.find(function(t){ return t.id === selectedTool; });
if (curTool && game.budget < curTool.cost) {
// Automatisch auf Ansehen zurueckfallen
selectedTool = 'inspect';
document.querySelectorAll('#tools-list .ggs-card').forEach(function(c){ c.classList.remove('tool-active'); });
var inspectCard = document.querySelector('#tools-list .ggs-card[data-tool="inspect"]');
if (inspectCard) inspectCard.classList.add('tool-active');
}
// Erfolge
renderBadges(st.achievements);
// Tierarten
renderSpecies();
}
function renderSpecies() {
var wrap = document.getElementById('species-list');
if (!wrap) return;
wrap.innerHTML = '';
E.SPECIES.forEach(function(sp) {
var el = document.createElement('span');
el.textContent = sp.icon;
el.title = sp.name + ' (ab Biodiv. ' + sp.threshold + ')';
if (!game.species[sp.key]) {
el.style.opacity = '.18';
el.style.filter = 'grayscale(1)';
}
wrap.appendChild(el);
});
}
function setParam(key, value, percent, cssClass) {
var p = document.getElementById('p-' + key);
var b = document.getElementById('b-' + key);
if (p) p.textContent = value;
if (b) {
b.style.width = Math.max(0, Math.min(100, percent)) + '%';
b.className = 'ggs-param-fill ' + cssClass;
}
}
function paramClass(v, warnBelow, goodAbove) {
if (v < warnBelow) return 'danger';
if (v < goodAbove) return 'warning';
return 'good';
}
function updateGraph(key, currentValue) {
var card = document.querySelector('.ggs-graph-card[data-graph="' + key + '"]');
if (!card) return;
var valEl = card.querySelector('.ggs-graph-value');
var line = card.querySelector('.ggs-graph-line');
if (valEl) valEl.textContent = key === 'budget' ? (currentValue + ' Mio') : currentValue;
if (!line) return;
var hist = game.history;
if (hist.length < 2) return;
var take = Math.min(hist.length, game.maxYears);
var W = 200, H = 70;
var maxVal = key === 'budget'
? Math.max(100, Math.max.apply(null, hist.map(function(h){ return h.budget; })) * 1.1)
: 100;
var pts = [];
for (var i = 0; i < hist.length; i++) {
var x = (i / (game.maxYears - 1)) * W;
var v = hist[i][key];
if (v == null) v = 0;
var y = H - (v / maxVal) * H;
pts.push(x.toFixed(1) + ',' + y.toFixed(1));
}
line.setAttribute('points', pts.join(' '));
}
function renderBadges(earnedKeys) {
var wrap = document.getElementById('badges-earned');
if (!wrap) return;
wrap.innerHTML = '';
E.ACHIEVEMENTS.forEach(function(a) {
var el = document.createElement('div');
el.className = 'ggs-badge-earned';
el.title = a.title + ' — ' + a.desc;
el.textContent = a.icon;
if (earnedKeys.indexOf(a.key) === -1) {
el.style.background = 'var(--ggs-bg-dark)';
el.style.opacity = '.4';
}
wrap.appendChild(el);
});
}
/* ---------- Events aus Engine -> showEvent-Toasts + SFX ---------- */
function pumpEvents() {
var evs = game._pendingEvents;
game._pendingEvents = [];
evs.forEach(function(e) {
showEvent(e.icon, e.text, e.type, e.topic);
if (!window.flAudio) return;
// Event-Sound nach Topic bevorzugen, sonst Fallback auf Icon-Match
switch (e.topic) {
case 'flood-settlement': flAudio.play('event-flood-settlement'); return;
case 'flood-field': flAudio.play('event-flood-field'); return;
case 'levee-break': flAudio.play('event-levee-break'); return;
case 'tree-flood-die':
case 'tree-straighten-die': flAudio.play('event-tree-die'); return;
}
if (/Erfolg freigeschaltet/.test(e.text)) flAudio.play('achievement');
});
}
/* ---------- Hilfs: Summe der Feld-Ertraege im Grid ---------- */
function sumYield() {
var s = 0;
for (var r = 0; r < E.ROWS; r++) for (var c = 0; c < E.COLS; c++) {
if (game.land[r][c] === 'field') s += (game.tileYield[r][c] || 0);
}
return s;
}
/* ---------- Feedback fuer Jahreswechsel ---------- */
// Trend-Pfeile neben jedem Parameter + kurzer Farb-Flash, wenn sich
// Werte deutlich veraendern. So merkt die Lernende, was passiert ist.
function showYearChanges(pre, post) {
var params = [
{ key: 'population', flashAt: 5 },
{ key: 'food', flashAt: 5 },
{ key: 'biodiv', flashAt: 3 },
{ key: 'flood', flashAt: 3 },
{ key: 'economy', flashAt: 3 },
];
params.forEach(function(p) {
var delta = (post.params[p.key] || 0) - (pre.params[p.key] || 0);
var tEl = document.getElementById('t-' + p.key);
if (tEl) {
if (delta === 0) {
tEl.textContent = '';
tEl.className = 'ggs-trend';
} else {
tEl.textContent = (delta > 0 ? '+' : '') + delta;
tEl.className = 'ggs-trend ' + (delta > 0 ? 'up' : 'down');
}
}
if (Math.abs(delta) >= p.flashAt) {
var row = document.querySelector('[data-param="' + p.key + '"]');
if (row) flashRow(row, delta > 0);
}
});
// Budget-Flash separat (kein eigener trend-span, aber Topbar-Wert)
var dBudget = (post.budget || 0) - (pre.budget || 0);
if (Math.abs(dBudget) >= 10) {
var b = document.getElementById('t-budget');
if (b) {
b.style.transition = 'color 200ms, transform 200ms';
b.style.color = dBudget > 0 ? 'var(--ggs-moss)' : 'var(--ggs-coral)';
b.style.transform = 'scale(1.12)';
setTimeout(function() { b.style.color = ''; b.style.transform = ''; }, 900);
}
}
// Wetter-Indicator pulsen, wenn das Wetter gewechselt hat
if (pre.weather && post.weather && pre.weather.key !== post.weather.key) {
var wIcon = document.getElementById('t-weather-icon');
if (wIcon) {
wIcon.style.transition = 'transform 300ms ease';
wIcon.style.transform = 'scale(1.4)';
setTimeout(function() { wIcon.style.transform = ''; }, 400);
}
}
}
function flashRow(row, positive) {
var col = positive ? 'rgba(90,138,94,.22)' : 'rgba(192,122,107,.22)';
row.style.transition = 'background 150ms';
row.style.background = col;
setTimeout(function() { row.style.background = ''; }, 1100);
}
/* ---------- Ernte-Animation: 🌾 fliegt zur Nahrungs-Zeile ---------- */
function flyHarvestToFood() {
var target = document.querySelector('[data-param="food"]');
if (!target) return;
var tRect = target.getBoundingClientRect();
var tx = tRect.left + tRect.width * 0.3;
var ty = tRect.top + tRect.height / 2;
var cRect = canvas.getBoundingClientRect();
var harvested = [];
for (var r = 0; r < E.ROWS; r++) for (var c = 0; c < E.COLS; c++) {
if (game.land[r][c] === 'field' && (game.tileYield[r][c] || 0) > 0) {
harvested.push({ r: r, c: c });
}
}
if (!harvested.length) return;
// Maximal 6 Emojis pro Tick, damit es nicht spammt.
if (harvested.length > 6) {
harvested.sort(function() { return Math.random() - 0.5; });
harvested.length = 6;
}
harvested.forEach(function(h, i) {
setTimeout(function() {
var startX = cRect.left + (h.c + 0.5) * tw;
var startY = cRect.top + (h.r + 0.5) * th;
var el = document.createElement('div');
el.textContent = '🌾';
el.style.cssText = [
'position:fixed', 'left:0', 'top:0',
'font-size:20px', 'z-index:9999', 'pointer-events:none',
'transform:translate(' + startX + 'px,' + startY + 'px)',
'transition:transform 900ms cubic-bezier(.5,-0.2,.4,1), opacity 900ms ease-out',
'will-change:transform,opacity',
].join(';');
document.body.appendChild(el);
requestAnimationFrame(function() {
requestAnimationFrame(function() {
el.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(0.6) rotate(360deg)';
el.style.opacity = '0';
});
});
setTimeout(function() {
el.remove();
pulseFoodRow();
}, 940);
}, i * 80);
});
}
function pulseFoodRow() {
var val = document.querySelector('[data-param="food"] .ggs-param-value');
if (!val) return;
val.style.transition = 'transform 220ms ease-out, color 220ms';
val.style.transform = 'scale(1.25)';
val.style.color = 'var(--ggs-moss)';
setTimeout(function() {
val.style.transform = 'scale(1)';
val.style.color = '';
}, 240);
}
/* ---------- Tool-Auswahl ---------- */
document.querySelectorAll('#tools-list .ggs-card').forEach(function(card) {
card.addEventListener('click', function() {
if (card.classList.contains('disabled-progression')) return;
if (card.classList.contains('too-expensive')) {
// Kein Wechsel moeglich — kurzes Feedback
if (window.flAudio) flAudio.play('ui-error');
var toolId = card.dataset.tool;
var t = E.TOOLS.find(function(x){ return x.id === toolId; });
if (t) showEvent('💰', t.name + ' kostet ' + t.cost + ' Mio — noch ' + (t.cost - game.budget) + ' Mio zu wenig.', 'bad');
return;
}
document.querySelectorAll('#tools-list .ggs-card').forEach(function(c){ c.classList.remove('tool-active'); });
card.classList.add('tool-active');
selectedTool = card.dataset.tool;
var title = card.querySelector('.ggs-card-title').textContent;
var desc = card.querySelector('.ggs-card-desc') ? card.querySelector('.ggs-card-desc').textContent : '';
document.getElementById('tool-info').textContent = title + ' — ' + desc;
if (window.flAudio) flAudio.play('ui-click');
});
});
/* ---------- First-Buy-Hints (pro Durchgang einmal) ---------- */
var FIRST_BUY_HINT = {
deich: { icon: '🛡️', text: 'Dein erster Deich. Er schützt angrenzende Felder — hält aber nicht ewig, bei langer Flut kann er brechen.' },
feld: { icon: '🌾', text: 'Erstes Feld. Der Ertrag hängt von der Flussnähe ab: zu nah ertrinkt, zu weit verdorrt. Und: Felder brauchen Arbeitskräfte aus Siedlungen in der Nähe.' },
siedlung: { icon: '🏘️', text: 'Erste Siedlung. Bewohner*innen ziehen nach, wenn Nahrung da ist und es trocken bleibt. Nah am Fluss gibt es Fisch — aber auch Hochwasser.' },
wald: { icon: '🌳', text: 'Erster Wald. Hebt Biodiversität und hält Uferböden. Neben einer Begradigung stirbt er allerdings ab.' },
abriss: { icon: '🧨', text: 'Abriss entfernt Gebäude. Auf einem begradigten Flussstück renaturiert er den Fluss — dieser darf wieder wandern.' },
begradigen: { icon: '📏', text: 'Fluss begradigt. Du gewinnst Bauland, aber der Fluss fixiert sich, die Umgebung trocknet aus und Biodiversität sinkt.' },
};
/* ---------- Canvas-Click / Hover ---------- */
canvas.addEventListener('click', function(e) {
if (!game || game.gameOver) return;
var rect = canvas.getBoundingClientRect();
var gc = Math.floor((e.clientX - rect.left) / tw);
var gr = Math.floor((e.clientY - rect.top) / th);
var before = (game.stats.builtTools || {})[selectedTool] || 0;
var res = E.applyTool(game, selectedTool, gr, gc);
if (res.events) res.events.forEach(function(ev) { showEvent(ev.icon, ev.text, ev.type); });
if (window.flAudio) {
if (res.ok && selectedTool !== 'inspect') flAudio.playBuild(selectedTool);
else if (!res.ok && res.reason === 'constraint') flAudio.play('ui-error');
}
if (res.ok) {
// First-Buy-Hint: wenn dieses Werkzeug gerade zum ersten Mal wirkt
var after = (game.stats.builtTools || {})[selectedTool] || 0;
if (after === 1 && before === 0 && FIRST_BUY_HINT[selectedTool]) {
var h = FIRST_BUY_HINT[selectedTool];
setTimeout(function() { showEvent(h.icon, h.text, 'neutral'); }, 700);
}
autoSave();
renderUI();
}
});
canvas.addEventListener('mousemove', function(e) {
if (!game) return;
var rect = canvas.getBoundingClientRect();
var gc = Math.floor((e.clientX - rect.left) / tw);
var gr = Math.floor((e.clientY - rect.top) / th);
if (gr < 0 || gr >= E.ROWS || gc < 0 || gc >= E.COLS) return;
var info = document.getElementById('canvas-info');
if (!info) return;
var t = game.land[gr][gc];
var labels = { meadow:'🌱 Wiese', forest:'🌳 Wald', field:'🌾 Feld', settlement:'🏘️ Siedlung',
levee:'🛡️ Deich', ruin:'🏚️ Ruine', dead_tree:'🪵 Toter Baum' };
var lbl = E.isRiverTile(game, gr, gc) ? '🌊 Fluss' : (labels[t] || t);
info.textContent = lbl + ' · Jahr ' + game.year;
});
/* ---------- Speed ---------- */
window.onSpeedChange = function(newSpeed) { speed = newSpeed; };
/* ---------- Autosave ---------- */
var resetting = false;
function autoSave() {
if (resetting) return;
var key = 'ggs-save-fluss-' + game.level;
try { localStorage.setItem(key, E.serialize(game)); } catch {}
}
window.addEventListener('beforeunload', autoSave);
/* ---------- Master-Mute (SFX + Musik zusammen) ----------
Persistent in localStorage via flAudio-eigenem Key. Wenn Ton aus,
wird auch ein laufender Musik-Track pausiert; beim Wiedereinschalten
wird er fortgesetzt, falls er vorher lief. */
var _musicPausedByMute = false;
var btnMute = document.getElementById('btn-mute');
function musicButton() { return document.getElementById('ggs-music-toggle'); }
function isMusicPlaying() {
var b = musicButton();
return b && b.classList.contains('playing');
}
function refreshMuteButton() {
if (!btnMute || !window.flAudio) return;
btnMute.textContent = flAudio.isMuted() ? '🔇' : '🔊';
btnMute.setAttribute('aria-pressed', flAudio.isMuted() ? 'true' : 'false');
btnMute.title = flAudio.isMuted() ? 'Ton wieder einschalten' : 'Ton aus';
}
if (btnMute && window.flAudio) {
btnMute.addEventListener('click', function() {
var nowMuted = flAudio.toggleMute();
if (nowMuted) {
// Wenn Musik laeuft → pausieren und merken
if (isMusicPlaying()) {
_musicPausedByMute = true;
musicButton().click();
}
} else {
// Ton wieder an — falls wir die Musik pausiert haben, fortsetzen
if (_musicPausedByMute && !isMusicPlaying()) {
_musicPausedByMute = false;
musicButton().click();
}
}
refreshMuteButton();
});
refreshMuteButton();
}
/* ---------- Reset-Button ----------
Immer Overlay mit Stufen-Auswahl — auch bei Klassen-Session darf
beim Neustart die Stufe gewechselt werden. */
document.getElementById('btn-reset').addEventListener('click', function() {
renderResetActions();
showOverlay('reset-overlay');
});
function renderResetActions() {
var bar = document.getElementById('reset-actions');
if (!bar) return;
bar.innerHTML = '';
// Grid oben
var gridWrap = document.createElement('div');
gridWrap.style.width = '100%';
renderLevelGrid(gridWrap);
bar.appendChild(gridWrap);
// "Abbrechen" darunter
var row = document.createElement('div');
row.style.cssText = 'display:flex;justify-content:center;margin-top:4px';
var cancel = document.createElement('button');
cancel.className = 'ggs-btn ggs-btn-ghost';
cancel.textContent = 'Abbrechen';
cancel.addEventListener('click', function() { hideOverlay('reset-overlay'); });
row.appendChild(cancel);
bar.appendChild(row);
}
/* ---------- Spielende ---------- */
var endShown = false;
function maybeShowEnd() {
if (!game.gameOver || endShown) return;
endShown = true;
var summary = E.getSummary(game);
// Sterne (Animation: nacheinander aufleuchten)
var starEls = document.querySelectorAll('#end-stars .ggs-star');
starEls.forEach(function(s) { s.classList.remove('lit'); });
starEls.forEach(function(star, i) {
if (i < summary.stars) {
setTimeout(function() { star.classList.add('lit'); }, 400 + i * 220);
}
});
// Stats
var stats = document.getElementById('end-stats');
if (stats) {
stats.innerHTML = ''
+ '<div><div class="ggs-results-stat-value">' + summary.stats.settlementsLost + '</div><div class="ggs-results-stat-label">Siedlungen verloren</div></div>'
+ '<div><div class="ggs-results-stat-value">' + summary.params.biodiv + '</div><div class="ggs-results-stat-label">Biodiv. Endstand</div></div>'
+ '<div><div class="ggs-results-stat-value">' + summary.params.economy + '</div><div class="ggs-results-stat-label">Wirtschaft</div></div>'
+ '<div><div class="ggs-results-stat-value">' + summary.badges_earned.length + ' / ' + E.ACHIEVEMENTS.length + '</div><div class="ggs-results-stat-label">Erfolge</div></div>';
}
// Badges
var badgesWrap = document.getElementById('end-badges');
if (badgesWrap) {
badgesWrap.innerHTML = '';
E.ACHIEVEMENTS.forEach(function(a) {
var el = document.createElement('div');
el.className = 'ggs-badge-earned';
el.title = a.title + ' — ' + a.desc;
el.textContent = a.icon;
if (summary.badges_earned.indexOf(a.key) === -1) {
el.style.background = 'var(--ggs-bg-dark)';
el.style.opacity = '.35';
}
badgesWrap.appendChild(el);
});
}
// Reflexion
renderEndReflection(summary);
document.getElementById('end-title').textContent = 'Durchgang beendet — 100 Jahre sind um.';
document.getElementById('end-subtitle').textContent = 'Beste Leistung: ' + summary.score + ' / 100';
renderEndActions();
// Zwei-Felder-Endscreen: immer mit Feld 1 (Rückmeldung) starten.
var _s1=document.getElementById('end-step1'), _s2=document.getElementById('end-step2'), _wb=document.getElementById('end-weiter');
if(_s1) _s1.style.display=''; if(_s2) _s2.style.display='none'; if(_wb) _wb.disabled=true;
showOverlay('end-overlay');
if (window.flAudio) flAudio.play(summary.stars >= 3 ? 'level-won' : 'level-lost');
submitAssessment({
level: game.level, stars: summary.stars, score: summary.score,
leistung_pct: Math.max(0, Math.min(100, Math.round(summary.score))),
duration_ms: 0, completed: summary.completed,
results: { final_biodiv: summary.params.biodiv, final_economy: summary.params.economy,
final_flood: summary.params.flood, final_food: summary.params.food,
settlements_lost: summary.stats.settlementsLost, fields_lost: summary.stats.fieldsLost },
badges_earned: summary.badges_earned,
});
}
// Reflexionsfrage — abhaengig vom Spielverlauf waehlen wir die
// didaktisch passende Frage aus, nicht zufaellig.
function renderEndReflection(summary) {
var options;
var question;
var s = summary.stats;
if (s.settlementsLost > 0) {
question = 'Welche Lehre ziehst du aus den verlorenen Siedlungen?';
options = [
'Ich würde Siedlungen weiter vom Fluss weg bauen.',
'Ich würde früher Deiche bauen.',
'Deiche haben nicht ausgereicht — mehr Überschwemmungsgebiet hätte geholfen.',
'Ich bin mir noch nicht sicher.',
];
} else if (s.straightenCount > 0 && s.denaturalizeCount === 0) {
question = 'Du hast den Fluss begradigt, aber nicht renaturiert. Warum?';
options = [
'Begradigung hat gut funktioniert, ich brauchte das Bauland.',
'Ich wusste nicht, dass Abriss die Begradigung rückgängig macht.',
'Renaturierung ist zu teuer gewesen.',
'Ich würde beim nächsten Durchgang renaturieren.',
];
} else if (summary.params.biodiv > 70 && summary.params.economy > 60) {
question = 'Du hast Biodiversität und Wirtschaft im Gleichgewicht gehalten. Was war dein Rezept?';
options = [
'Wald als Uferzone zwischen Fluss und Feldern.',
'Siedlungen klein und verteilt gebaut.',
'Fluss wandern lassen, keine Begradigung.',
'Eine Mischung aus allem.',
];
} else {
question = 'Was war in diesem Durchgang deine wirksamste Entscheidung?';
options = [
'Deiche zur richtigen Zeit gebaut.',
'Siedlungen nah am Fluss wegen Fischfang.',
'Wald für die Biodiversität gepflanzt.',
'Ich bin mir noch nicht sicher.',
];
}
document.getElementById('end-reflection-q').textContent = question;
var box = document.getElementById('end-reflection-options');
box.innerHTML = '';
options.forEach(function(opt, i) {
var row = document.createElement('div');
row.className = 'ggs-reflection-option';
row.innerHTML = '<div class="ggs-reflection-radio"></div><span>' + opt + '</span>';
row.addEventListener('click', function() {
box.querySelectorAll('.ggs-reflection-option').forEach(function(o){ o.classList.remove('selected'); });
row.classList.add('selected');
submitReflection(game.level, question, opt);
var wb = document.getElementById('end-weiter'); if (wb) wb.disabled = false; // erst nach Antwort weiter
});
box.appendChild(row);
});
}
// Endscreen-Aktionsleiste: Stufen-Auswahl (freie Session) oder nur
// "Neu starten" (wenn Lehrperson die Stufe vorgegeben hat → classId gesetzt).
// Stufen-Auswahl als Grid (Emoji + Label + Beschreibung) — einheitlich
// mit Klima. renderLevelGrid(targetEl) baut das Grid in targetEl.
// Einmal fuer Endscreen, einmal fuer Reset-Overlay verwendet.
var LEVELS = [
{ lvl: 1, em: '🟢', label: 'Lernen', sub: 'Viel Budget · milde Folgen · gute Einstiegs­bedingungen' },
{ lvl: 2, em: '🟡', label: 'Üben', sub: 'Weniger Budget · häufigere Ereignisse' },
{ lvl: 3, em: '🔴', label: 'Profi', sub: 'Knappes Budget · harte Naturereignisse' },
];
function renderLevelGrid(targetEl) {
if (!targetEl) return;
targetEl.innerHTML = '';
var grid = document.createElement('div');
grid.className = 'ggs-level-grid';
var ctx = getCtx();
var baseUrl = ctx.baseUrl || '';
function goTo(level) {
resetting = true;
try { localStorage.removeItem('ggs-save-fluss-' + level); } catch {}
var target = baseUrl ? baseUrl + '/fluss' : location.pathname;
var sep = target.indexOf('?') >= 0 ? '&' : '?';
location.href = target + sep + 'level=' + level;
}
LEVELS.forEach(function(l) {
var card = document.createElement('div');
card.className = 'ggs-level-card' + (l.lvl === game.level ? ' current' : '');
card.innerHTML =
'<span class="em">' + l.em + '</span>' +
'<div class="lbl">' + l.label + '</div>' +
'<div class="sub">' + l.sub + '</div>' +
'<span class="ggs-badge level-' + ['', 'easy','medium','hard'][l.lvl] + '" style="margin-top:6px;display:inline-block">Stufe ' + l.lvl + '</span>';
card.addEventListener('click', function() { goTo(l.lvl); });
grid.appendChild(card);
});
targetEl.appendChild(grid);
}
function renderEndActions() {
var bar = document.getElementById('end-actions');
if (!bar) return;
bar.innerHTML = '';
// Grid oben
var gridWrap = document.createElement('div');
gridWrap.style.width = '100%';
renderLevelGrid(gridWrap);
bar.appendChild(gridWrap);
// "Zurück"-Link darunter
var row = document.createElement('div');
row.style.cssText = 'display:flex;justify-content:center;margin-top:4px';
var home = document.createElement('a');
home.href = '../../';
home.className = 'ggs-btn ggs-btn-ghost';
home.textContent = 'Zurück zur Startseite';
row.appendChild(home);
bar.appendChild(row);
}
// Nach jedem Tick kurz pruefen, ob das Spiel zu Ende ist.
var _origTick = E.tick;
E.tick = function(g) {
var out = _origTick.call(E, g);
if (g.gameOver) setTimeout(maybeShowEnd, 50);
return out;
};
/* ---------- Kickoff ---------- */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ===== Live-State für Lehrer*innen-Cockpit =====
// Read-only Snapshot — wird vom Plattform-Live-Client als Heartbeat-State
// mitgeschickt. params lebt nicht auf `game` selbst, sondern wird per
// E.getStatus(game) berechnet (population/biodiv/flood/economy/food).
window.GGS_LIVE_STATE = function() {
try {
if (!game) return null;
var st = (typeof E !== 'undefined' && E && typeof E.getStatus === 'function')
? E.getStatus(game) : null;
if (!st || !st.params) return null;
var ctx = getCtx() || {};
var maxY = st.maxYears || game.maxYears || 100;
var p = st.params;
// Erfolgs-Score (gleiche Formel wie engine.getSummary): Mittel aus
// biodiv+flood+economy, abzüglich Siedlungsverluste×5. Dient als
// didaktischer Anker im Lehrer-Cockpit, NICHT als Benchmark-Score
// (dafür → Benchmark.scoreSubmission nach Submit).
var settlementsLost = (game.stats && game.stats.settlementsLost) || 0;
var score = Math.round((p.biodiv + p.flood + p.economy) / 3) - settlementsLost * 5;
score = Math.max(0, Math.min(100, score));
var health = score >= 60 ? 'good' : score >= 30 ? 'ok' : 'struggle';
return {
simId: 'fluss',
levelId: ctx.level || game.level || 1,
year: st.year || 0,
progressPct: Math.min(100, Math.round(((st.year || 0) / maxY) * 100)),
phase: 'Jahr ' + (st.year || 0),
budget: Math.round(st.budget || 0),
biodiv: Math.round(p.biodiv || 0),
economy: Math.round(p.economy || 0),
flood: Math.round(p.flood || 0),
population: Math.round(p.population || 0),
food: Math.round(p.food || 0),
score: score,
leistung_pct: score,
health: health,
settlementsLost: settlementsLost,
gameOver: !!st.gameOver,
timestamp: Date.now(),
};
} catch (_) { return null; }
};
})();
</script>
</body>
</html>