# 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 ```typescript 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 ```typescript 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: ```typescript 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) ```typescript setVariable(key, value) getVariable(key): number ``` Werden für interne Berechnungen verwendet, erscheinen nicht in der UI. Beispiele: `co2Reduction`, `protection`, `upkeepTotal`. ### 2.5 Goals ```typescript 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) ```typescript 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) ```typescript 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 ```typescript 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 ```typescript serialize(): string // JSON deserialize(s: string): boolean // gibt false bei alter Version // Subklassen-Hooks (überschreibbar): protected serializeSubclass(): Record protected deserializeSubclass(data: Record): void ``` **Versionierung:** `SAVE_VERSION = 2`. Saves mit anderer Version werden abgelehnt → neues Spiel. Bei Mechanik-Änderungen Version erhöhen. **Save-Format (Auszug):** ```json { "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 ```typescript 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 ```typescript 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 ```typescript 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|2|4"]` | Speed-Buttons | | `#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: 1. **`power`-Resource** → `updatePowerGrid()` rendert Bedarf/Kapazitäts-Balken 2. **`budget`-Resource + `game.getYearlyBalance()`** → `updateBudgetBalance()` rendert Bilanz-Tabelle 3. **Resource-Format-Closures** dürfen `this` (das Game-Objekt) nutzen **Konvention:** Sim-spezifische UI-Logik in `game-ui.ts` mit Kommentar `// Spezialfall :` markieren. ### 3.6 Public API (von außen aufrufbar) ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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 Lebensdauer - `spawnRefugeeBoat()` — Boot mit Boarding → Spirale → Escape (28s total) - `makeEmojiSprite(emoji, size)` — Billboard-Sprite mit CanvasTexture ### 4.5 Maßnahmen-Platzierung ```typescript 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: ```typescript 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() 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 ```typescript 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: ```javascript 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. ```html ppm i ``` **Erweitern:** Neuen Eintrag in `TIPS: Record` 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`: 1. Card via `appendChild(body)` aus Container herauslösen (wegen `backdrop-filter`) 2. Platzhalter im Original-Grid lassen 3. `position: fixed` mit Original-Rect setzen, Reflow erzwingen 4. Transition zu zentrierten Größen 5. 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: 1. `SAVE_VERSION` in `game-engine.ts` erhöhen (oder Sim-spezifisches `SCHEMA_VERSION` falls vorhanden) 2. Alte Saves werden automatisch verworfen 3. 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: ```typescript 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: ```typescript 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: 1. **Erst lesen, dann ändern.** Nie Annahmen über Code, der nicht im Kontext ist. 2. **Build + Tests nach jeder Änderung.** `npx vite build && npx vitest run`. 3. **Headless-Verify für UI-Änderungen.** Playwright-Script in `App/scripts/`. 4. **Save-Schema bei Mechanik-Änderung erhöhen.** Sonst kaputte Saves. 5. **Trace-Test grün halten.** Wenn er rot wird: Balance kalibrieren, nicht ignorieren. 6. **Diese Doku aktualisieren** bei Schnittstellen-Änderungen. 7. **Wald = günstigste CO₂-Reduktion pro Mio €.** Heilig. Nie verschlechtern. 8. **Mio €** ist die Geldeinheit. Nie nackte Euro. 9. **Tick = Jahr** in sim-05. Falls Wechsel auf Monate: alle Werte ÷12. 10. **Einfache emojis bevorzugen** (Single-Codepoint, kein ZWJ). 🧍 ja, 🧍‍♀️ nein.