- Design-System (assets/css/design-system.css) mit 21 Komponenten, iPad-Responsive-Breakpoints, Touch-Ziele 36px, Music-Player, Glossar-Tooltips - Templates (sims/template.html + student/teacher-Dashboard) - Docs: module-interface.md (inkl. 4a Sprachregel, 4b Leichte Sprache, 4c iPad), content-architecture.md, crash-recovery.md, music-registry.md - Admin-Infrastruktur: admin-modules.html + api/admin-modules.php (Titel, Emoji, Bild, Status, Dauer, Alter pro Modul) - Inbox-System: _inbox/README.md + _status.md fuer Atlas + Briefings an Klima, Glossar, Lehrplan, Fluss - Zentrale SFX-Pipeline (scripts/generate-sounds.py) - DALL-E-Bilder: 8 Badges + 5 Glossar-Repraesentationsbilder (Querformat) - Logo + Inter-Font lokal - PHP-APIs: admin, glossar, levels, licenses, progress, waypoints, assignments, profile, tickets - Spielsprache entfernt (admin-modules, admin-levels, schueler) - Landing-Page-Bearbeitungen (Boote sichtbarer, Button-Hintergrund) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
GeoGraSim — Architektur & Schnittstellen
Zweck dieses Dokuments Zentrale Referenz für alle Module, Datentypen, Schnittstellen und Konventionen. Geschrieben so, dass sowohl ein Mensch als auch eine KI das gesamte Projekt verstehen kann, ohne den Source-Code zu lesen. Bei jeder größeren Änderung dieses Dokument aktualisieren.
Stand: April 2026 (nach Strom-System, 3 Schwierigkeitsgraden, Bilanz-UI, Steg-Rampe, Graph-Zoom, Auto-Pause)
1. Verzeichnis-Struktur
App/
├── index.html # Übersicht aller Simulationen
├── game-3d.html # Klimawächter 3D-Renderer (Hauptspiel)
├── game.html # Klimawächter 2D-Renderer (älter, hinkt hinterher)
├── erdbeben.html # SIM-07: Erdbeben
├── energiemix.html # SIM-09: Energiemix
├── stilauswahl.html # Frontend-Stil-Auswahl-Tool
│
├── src/
│ ├── core/
│ │ └── game-engine.ts # Basis-Klasse aller Simulationen
│ ├── ui/
│ │ ├── game-ui.ts # Generic GameUI (Resources, Goals, Shop, Graphs, ...)
│ │ ├── info-overlay.ts # Tooltips + erklärende Overlay-Modals
│ │ └── theme.ts # (falls vorhanden) Farb-Themes
│ └── sims/
│ ├── sim-05-treibhaus/ # Klimawächter Logik (engine-level)
│ │ ├── game.ts # KlimawaechterGame, MEASURES, DIFFICULTY_LEVELS
│ │ └── logic.ts # computeTemperature() etc.
│ ├── sim-05-treibhaus-3d/ # 3D-Renderer für Klimawächter
│ │ └── game-renderer-3d.ts (~1900 Zeilen)
│ ├── sim-07-erdbeben/ # SIM-07
│ └── sim-09-energiemix/ # SIM-09
│
├── tests/unit/
│ ├── sim-05-trace.test.ts # Mechanik-Trace über 75 Jahre, 7 Strategien
│ ├── sim-05-treibhaus.test.ts # Standard-Tests
│ └── ... (pro Sim)
│
└── docs/
├── architektur.md # ← DIESES DOKUMENT
├── klimawaechter-dev-log.html # Entwicklungs-Log
└── ...
2. Core-Schicht: game-engine.ts
Die GameEngine-Basisklasse stellt alle gemeinsamen Mechaniken bereit:
Ressourcen, Ziele, Tutorial, Events, Tick-Loop, Save/Load, Citizen-Events, State-Machine.
2.1 Lifecycle
constructor(meta) → setupResources() → setupGoals() → setupTutorial()
↓
state: 'tutorial' (warten auf Spieler-Klick durch Tutorial-Schritte)
↓
state: 'running' (msPerTick × speed → simulateTick() pro Zeitschritt)
↓
state: 'won' | 'lost' (checkWinCondition / checkLossCondition)
2.2 GameMeta
interface GameMeta {
id: string // 'sim-05'
title: string // 'Klimawächter'
description: string
msPerTick: number // Wieviele ms pro Tick (4000 = 4 Sek/Jahr)
tickUnit: string // 'Jahr', 'Monat', etc. (UI-Beschriftung)
maxTicks: number // Spielende-Tick (75 für 2025-2100)
tutorialSteps: number // Anzahl der Schritte (siehe setTutorial)
}
2.3 Resources
interface Resource {
id: string // eindeutig: 'budget', 'population', ...
name: string // Anzeige
icon: string // Emoji
initial: number
unit: string // 'Mio €', 'MW', 'cm', ...
format?: (v: number) => string // Custom Display-Format
current: number // wird vom Engine gehalten
}
// API:
addResource(r: Resource) // im constructor() aufrufen
setResource(id, value)
getResource(id): number
changeResource(id, delta)
getResourcesArray(): Resource[]
Konvention: Custom format-Funktionen können auf andere Ressourcen zugreifen:
this.addResource({
id: 'power', initial: 0,
format: (v) => {
const demand = Math.round(this.getResource('population') / 1000)
return `${demand}/${Math.round(v)} MW`
}
})
2.4 Variables (interne, nicht angezeigte Werte)
setVariable(key, value)
getVariable(key): number
Werden für interne Berechnungen verwendet, erscheinen nicht in der UI. Beispiele:
co2Reduction, protection, upkeepTotal.
2.5 Goals
interface Goal {
id: string
title: string
description: string
check: (game: GameEngine) => boolean // erreicht?
progress?: (game: GameEngine) => number // 0-100 für Progress-Bar
required: boolean // muss für 'won' erfüllt sein
}
addGoal(g: Goal)
2.6 Events (Ereignis-Karten in der UI)
addEvent(id, text, severity, infoKey?)
// severity: 'info' | 'success' | 'warning' | 'danger'
// infoKey: optional Schlüssel für INFO_TOPICS Lookup
getEvents(limit): Event[]
id macht es idempotent (jedes Event nur einmal). Mit eventFired(id) aus
einer Subklasse kann man prüfen, ob ein Event schon ausgelöst wurde.
2.7 Citizen Events (modale Bürger-Dilemmata)
interface CitizenEvent {
id: string
character: string // Emoji-Avatar
title: string // 'Lina, die Fischerin'
message: string // 1-3 Sätze, kindgerecht
choices: CitizenChoice[]
}
interface CitizenChoice {
label: string // Button-Text
description: string // Vor-/Nachteile-Hint
apply: (game: GameEngine) => void
}
triggerCitizenEvent(ev: CitizenEvent)
getPendingCitizenEvent(): CitizenEvent | null
resolveCitizenEvent(choiceIdx: number)
Die UI zeigt das Modal automatisch an, wenn pendingCitizenEvent !== null.
Nur eines gleichzeitig.
2.8 Tick-Loop
protected simulateTick(): void // ← in Subklasse implementieren
setSpeed(s: 0 | 1 | 2 | 4) // 0 = Pause
getSnapshot(): { tick, speed, state, goals[], timeline[] }
Die Engine ruft simulateTick() periodisch auf. tick zählt bei jedem Aufruf
hoch. Bei jedem Tick wird notify() gerufen → UI rendert neu.
2.9 Save / Load
serialize(): string // JSON
deserialize(s: string): boolean // gibt false bei alter Version
// Subklassen-Hooks (überschreibbar):
protected serializeSubclass(): Record<string, unknown>
protected deserializeSubclass(data: Record<string, unknown>): void
Versionierung: SAVE_VERSION = 2. Saves mit anderer Version werden
abgelehnt → neues Spiel. Bei Mechanik-Änderungen Version erhöhen.
Save-Format (Auszug):
{
"version": 2,
"tick": 12,
"state": "running",
"speed": 1,
"resources": { "budget": 437, ... },
"variables": { "co2Reduction": 1.2, ... },
"goalsAchieved": ["temp"],
"events": [...],
"subclass": { /* serializeSubclass() output */ }
}
3. UI-Schicht: game-ui.ts
Generische UI-Klasse, die von jeder Sim-HTML-Seite verwendet wird. Erwartet DOM-Elemente mit bestimmten IDs (siehe unten) und rendert da hinein.
3.1 GameUIConfig
interface GameUIConfig {
game: GameEngine
Renderer: new (container: HTMLElement, game: any) => Renderer
shopItems: ShopItem[]
onBuy: (id: string) => boolean
onDemolish?: (id: string) => boolean // Optional: 🗑-Button
getOwnedCount?: (id: string) => number // Optional: ×N Badge
graphs: GraphConfig[]
saveKey: string // localStorage key
finishOnTick?: number
yearOffset?: number // 2025
mode?: 'free' | 'guided'
totalDurationSec?: number // bei mode=guided
statusResources?: string[] // Whitelist welche Resources im Status-Panel
}
3.2 ShopItem
interface ShopItem {
id: string
name: string
emoji: string
description: string
cost: number
upkeep: number
badges?: { label: string; type: BadgeType }[]
}
type BadgeType = 'cost' | 'reduction' | 'protection' | 'capacity' | 'quality' | 'neutral'
3.3 GraphConfig
interface GraphConfig {
id: string // SVG-Element-ID
title: string
field: string // Resource-ID, dessen Werte geplottet werden
yMin: number
yMax: number
color: string // CSS-Farbe
unit?: string
zones?: GraphZone[] // farbige Bänder im Hintergrund
lines?: GraphLine[] // horizontale Referenzlinien
autoScale?: boolean // yMax wandert mit Daten mit
}
3.4 Erwartete DOM-Element-IDs (HTML-Seite)
| ID | Inhalt |
|---|---|
#canvas-wrap |
Container für 3D/2D Renderer |
#resources |
Status-Box (Resources werden hier eingefügt) |
#status-goals ODER #goals |
Ziele-Liste |
#measures |
Shop |
#events |
Vollständige Ereignis-Liste (Popout) |
#events-latest |
Nur das letzte Event (Trigger in Status-Box) |
#tutorial, #tut-title, #tut-text, #tut-dots, #tut-next |
Tutorial-Modal |
#end-screen-host, #citizen-event-host, #info-overlay-host |
Modal-Slots |
| `[data-speed="0 | 1 |
#btn-save, #btn-load, #btn-finish, #btn-reset |
System-Buttons |
3.5 Sim-Spezifische UI-Sonderfälle (Duck-Typing)
GameUI ist generisch, hat aber drei Hooks für sim-spezifische UI-Erweiterungen:
power-Resource →updatePowerGrid()rendert Bedarf/Kapazitäts-Balkenbudget-Resource +game.getYearlyBalance()→updateBudgetBalance()rendert Bilanz-Tabelle- Resource-Format-Closures dürfen
this(das Game-Objekt) nutzen
Konvention: Sim-spezifische UI-Logik in game-ui.ts mit Kommentar
// Spezialfall <NAME>: markieren.
3.6 Public API (von außen aufrufbar)
ui.getRenderer(): Renderer | null // für Kamera-Steuerung
ui.redrawGraphs(): void // erzwingt Graph-Neuzeichnung (nach Resize)
4. Renderer-Schicht: game-renderer-3d.ts
Three.js-basierter 3D-Renderer. Konstruktor erhält (container, game) und
hängt sich selbst an container.
4.1 Erwartete Methoden eines Renderers
interface Renderer {
start(): void
stop(): void
// optional, für UI-Buttons:
zoomIn?(): void
zoomOut?(): void
recenter?(): void
}
4.2 KlimawaechterRenderer3D — interne Struktur
Scene
├── Wasserfläche (steigt mit sealevel)
├── Himmel-Kuppel
├── Insel (PlaneGeometry mit Vertex-Colors, 120×160 Segmente)
├── Vulkan-Kuppel + Schnee-Gletscher (LatheGeometry, ClippingPlane)
├── Steg + Rampe + wartende Bewohner-Sprites
├── villageGroup (Häuser + 3 Baumtypen mit Stadien)
├── smokeGroup (Heizwolken pro Haus)
├── fxGroup (FX-Effekte: Totenkopf, Boot, Skulls)
├── placed[] (gekaufte Maßnahmen)
└── mountainShieldGroup? (wenn hasMountainShield)
4.3 Wichtige Konstanten
ISLAND_HALF_WIDTH = 13 // X-Halbachse
ISLAND_HALF_LENGTH = 17 // Z-Halbachse
WATER_Y_BASE = 0.0 // Start-Wasserlinie
MAX_SEA_RISE_UNITS = 1.6 // Wasser steigt bis +1.6 Y bei 200 cm Sealevel
VOLCANO_X = -3.0
VOLCANO_Z = -11.0
VOLCANO_DOME_HEIGHT = 2.1
VOLCANO_DOME_RADIUS = 2.2
ZONE_BEACH_END = 0.38 // Vertex-Color-Zonen
ZONE_MEADOW_END = 0.95
ZONE_FOREST_END = 1.80
4.4 FX-System
interface FxEffect {
root: THREE.Object3D
lifetime: number
age: number
update: (dt: number, ef: FxEffect) => boolean // false → entfernen
}
private fxEffects: FxEffect[] = []
private fxGroup: THREE.Group
// Pro Frame in updateScene():
for (let i = fxEffects.length - 1; i >= 0; i--) {
if (!fxEffects[i].update(dt, fxEffects[i])) fxEffects.splice(i, 1)
}
Spawn-Helpers:
spawnSkull(x, y, z)— 💀 schwebt 1.2 Units nach oben, 3.2s LebensdauerspawnRefugeeBoat()— Boot mit Boarding → Spirale → Escape (28s total)makeEmojiSprite(emoji, size)— Billboard-Sprite mit CanvasTexture
4.5 Maßnahmen-Platzierung
private placeMeasure(typeId: string, index: number): THREE.Object3D | null
Switch über typeId. Pro neuem Measure-Typ ein neuer Case + ein
makeXxx()-Helper. Das Renderer-Update-Loop scannt game.getOwnedMeasures()
und ruft placeMeasure für neue, entfernt Meshes für demolierte.
5. Sim-Spezifisch: sim-05-treibhaus/game.ts
5.1 KlimawaechterGame
Erbt von GameEngine. Ungefährer Aufbau:
export class KlimawaechterGame extends GameEngine {
private measures: OwnedMeasure[]
private diff: DifficultyConfig
private co2Ppm = 425
private currentTemp = 0
private targetTemp = 0
private seaLevelCm = 0
private floodedHouses = 0
researchDiscount = 1.0
tourismMode = false
hasMountainShield = false
treesChoppedForHeat = 0
private blackoutStreak = 0
private firedEvents = new Set<string>()
constructor(difficulty: 1|2|3 = 2)
// Public API
buyMeasure(id): boolean
demolishMeasure(id): boolean
getOwnedMeasures(): OwnedMeasure[]
getMeasureCount(id): number
getDifficulty(): DifficultyConfig
getYearlyBalance(): { income, tourism, upkeep, climate, net }
getIncomeAtCurrentLevel(): number
// Subclass-Hooks
protected simulateTick()
protected serializeSubclass()
protected deserializeSubclass()
protected checkLossCondition()
}
5.2 MEASURES-Tabelle (Stand April 2026)
| id | name | cost | upkeep | co2Reduction | protection | powerOutput |
|---|---|---|---|---|---|---|
forest |
🌲 Wald | 12 | 0.5 | -0.04 | – | – |
solar |
☀️ Solar | 40 | 2 | -0.14 | – | +1 MW |
wind |
🌬 Wind | 150 | 4 | -0.50 | – | +3 MW |
green-roof |
🏡 Gründach | 30 | 0 | -0.04 | – | – |
dike |
🌊 Deich | 80 | 3 | – | +12 cm | – |
sea-wall |
🛡 Hochw.schutz | 240 | 6 | – | +50 cm | – |
coal |
🏭 Kohle | 20 | 1 | +0.38 | – | +4 MW |
airport |
✈️ Flughafen | 62 | 3 | +0.22 | – | – |
cloud-seed |
🌤 Wolken-Impfung | 125 | 4.5 | -0.04 | – | – |
Differenzierung:
- Solar vs Wind: Solar günstig pro Stück, Wind effizienter pro MW Betrieb
- Wald: Günstigste Wartung pro CO₂-ppm (didaktisch: Naturlösung)
- Gründach: Einzige Maßnahme mit 0 Wartung (Set-and-forget)
- Deich vs Hochwasserschutz: Niedrige Hürde vs effiziente Großlösung
- Kohle/Flughafen/Wolken: Negative oder unwirksame Maßnahmen (Lehrtraps)
5.3 DIFFICULTY_LEVELS
DIFFICULTY_LEVELS[1] // 🟢 Lernen: 750/220, 3J Schonfrist, ×0.5 Folgen
DIFFICULTY_LEVELS[2] // 🟡 Üben: 550/175, 1J Schonfrist, ×1.0 Folgen
DIFFICULTY_LEVELS[3] // 🔴 Profi: 400/145, 0J Schonfrist, ×1.5 Folgen
Beeinflusst: startBudget, incomePer10k, blackoutGrace, treesPerBlackout,
co2BlackoutPerYear, climateDamageMul, popLossMul.
5.4 Klimaberechnung (computeClimate())
targetTemp = computeTemperature(co2Ppm, sensitivity=0.3)
currentTemp += (targetTemp - currentTemp) * 0.08 // Trägheit (Ozean)
sealevelTarget = (currentTemp - 15) * 30 cm/°C
sealevelCm += (target - current) * (rising ? 0.08 : 0.005) // committed rise
floodedHouses = max(0, (sealevel - protection - 35) * 0.9) [%]
5.5 simulateTick — Reihenfolge
1. Steuern (income = popRatio × diff.incomePer10k)
1b. Tourismus (Bonus + CO₂, oder Kollaps wenn Strand weg)
2. Wartung
3. CO₂-Update (emissions - reduction)
4. computeClimate()
4b. Strom-Bilanz + Stromausfall-Folgen (Bäume + CO₂ + Pop-Drift)
5. Häuser-Überflutung
5b. Sealevel-Folgen (Versalzung, Pop-Drift)
6. Zeit-Events
7. Citizen-Events (maybeTriggerCitizenEvent)
5.6 Citizen Events
| Tick | ID | Trigger |
|---|---|---|
| 15 | citizen-fisher |
Lina, Fischerin (warmes Wasser) |
| 9 | citizen-scientist |
Dr. Hassan, Forschungszentrum |
| 14 | citizen-farmer |
Yusuf, Bewässerung |
| 20 | citizen-tourism |
Maria, Hotel |
| 28 | citizen-youth |
Lia, 14, Streik |
| 38 | citizen-industry |
Konzernchef Vogel |
| 50 | citizen-mountain |
Anna, Bergdorf |
6. Auto-Pause-System (in HTML-Seite)
In game-3d.html als Token-basiertes Pause-System:
const overlayPauseTokens = new Set()
let speedBeforeOverlayPause = 1
function pauseGameForOverlay(token) { ... }
function resumeGameForOverlay(token) { ... }
// Auto-Hooks via MutationObserver:
observeOverlayForPause('.citizen-overlay', 'citizen-event')
observeOverlayForPause('.info-overlay', 'info-overlay')
// Manuell für Graph-Zoom + Events-Popout
Konvention: Jeder neue Overlay-Typ registriert sich mit eigenem Token.
Beim letzten Schließen wird speedBeforeOverlayPause wiederhergestellt.
7. Konventionen & Best Practices
7.1 Tooltip-System
Globaler Listener: installTooltips() in info-overlay.ts registriert
einen delegated mouseover-Handler. Jedes Element mit data-tip="key"
zeigt automatisch das TIPS[key]-Tooltip beim Hover.
<span data-tip="ppm">ppm <span class="unit-i">i</span></span>
Erweitern: Neuen Eintrag in TIPS: Record<string, TipDef> hinzufügen.
7.2 Display-Animation
Numeric Resources werden via gedämpfter rAF-Loop (12% pro Frame) zum
Zielwert hingezogen. Implementiert in GameUI.ensureDisplayAnim().
Snapping bei absDelta < 0.5.
7.3 Graph-Zoom (FLIP-Pattern)
Beim Klick auf eine .graph-card:
- Card via
appendChild(body)aus Container herauslösen (wegenbackdrop-filter) - Platzhalter im Original-Grid lassen
position: fixedmit Original-Rect setzen, Reflow erzwingen- Transition zu zentrierten Größen
- Nach Animation
ui.redrawGraphs()für SVG-Resize
Bei Unzoom: zurück animieren, Card in Platzhalter-Position wieder einfügen, Inline-Styles aufräumen.
7.4 Save-Versionierung
Bei jeder Mechanik-Änderung in einer Sim:
SAVE_VERSIONingame-engine.tserhöhen (oder Sim-spezifischesSCHEMA_VERSIONfalls vorhanden)- Alte Saves werden automatisch verworfen
- UI zeigt freundlichen Hinweis
7.5 Trace-Tests
tests/unit/sim-05-trace.test.ts simuliert verschiedene Strategien rein
rechnerisch über alle 75 Jahre und prüft, dass:
- Optimale Strategie gewinnt
- "Nichts tun" verliert
- "Nur Wald" verliert (zu langsam)
- "Nur Deich" verliert (CO₂ killt sie trotzdem) — TODO: aktuell unsicher
8. Ranking-System (vorgeschlagen, noch nicht implementiert)
Erweitere GameMeta um:
interface GameRanking {
complexity: 1|2|3|4|5
timeMinutes: number
gradeRecommendation: { min: number; max: number } // 1-12 Schulstufe
focusedLearning: 1|2|3|4|5
topics: string[]
}
Klimawächter (sim-05) Vorschlag:
ranking: {
complexity: 4,
timeMinutes: 15,
gradeRecommendation: { min: 6, max: 8 },
focusedLearning: 3,
topics: ['Klimawandel', 'Energiewende', 'Anpassung vs Mitigation', 'Wirtschaftsbilanz']
}
index.html filtert/sortiert dann Sims nach Ranking.
9. Offene Punkte (kurzgefasst)
| Was | Wo dokumentiert |
|---|---|
| Phase B: Drag-and-Drop-Baueditor | dev-log §4 |
| Phase C: Wohnungsbau + Neuankömmlinge | dev-log §4 |
| Mitigation vs Adaptation Kipppunkt | dev-log §4 (idee) |
| Insel-Leben Sprites (Wolken, Möwen, Boote) | brainstorm offen |
| Sand-Aufschüttung mit Erosion | brainstorm offen |
| Mangroven (Win-Win-Maßnahme) | brainstorm offen |
| Sterne-Ranking implementieren | dieses Dok §8 |
| 2D-Renderer auf Strom-System updaten | dev-log §8 |
10. Konventionen für die KI
Wenn die KI Code-Änderungen macht:
- Erst lesen, dann ändern. Nie Annahmen über Code, der nicht im Kontext ist.
- Build + Tests nach jeder Änderung.
npx vite build && npx vitest run. - Headless-Verify für UI-Änderungen. Playwright-Script in
App/scripts/. - Save-Schema bei Mechanik-Änderung erhöhen. Sonst kaputte Saves.
- Trace-Test grün halten. Wenn er rot wird: Balance kalibrieren, nicht ignorieren.
- Diese Doku aktualisieren bei Schnittstellen-Änderungen.
- Wald = günstigste CO₂-Reduktion pro Mio €. Heilig. Nie verschlechtern.
- Mio € ist die Geldeinheit. Nie nackte Euro.
- Tick = Jahr in sim-05. Falls Wechsel auf Monate: alle Werte ÷12.
- Einfache emojis bevorzugen (Single-Codepoint, kein ZWJ). 🧍 ja, 🧍♀️ nein.