---
von: klima (Session vom 18.04. abends)
an: klima (folgende Session)
datum: 2026-04-18 22:40
status: uebergabe
betrifft: Engine-Refactor — Schritt 1 + 2 fertig, Schritt 3-5 offen
---

# Übergabe-Notiz: Klima-Engine-Refactor

Auftrag aus `2026-04-18-2145-auftrag-engine-und-3d-v2.md`. Schritt 1 (Engine
extrahieren) und Schritt 2 (2D auf Engine umstellen) sind durch. Schritt 3
(3D V2) wurde bewusst noch NICHT angefangen, weil der 4-6h-Block mehr
Kontext braucht als in dieser Session verfügbar war.

## Was fertig ist

### Schritt 1 — `App/sims/klima/engine.js`

Neues, headless JS-Modul, ESM-frei (`window.KlimaEngine`). Enthält:

- **Konstanten:** `MEASURES`, `DIFFICULTY`, `ACHIEVEMENTS`, `CITIZEN_EVENTS`,
  `FIRST_BUY_HINTS`, `CO2_FLOOR`, `TEMP_FLOOR` — genau 1:1 aus der alten
  2D-Version übernommen.
- **Lebenszyklus:** `createGame(levelId, { maxTick, startYear, levelConfig })`,
  `serialize(game)`, `deserialize(data)`.
- **Simulation:** `recalcEffects`, `computeClimate`,
  `computeTemperatureFromCO2`, `getEmissionsThisYear`, `getPowerDemand`,
  `getIncomeThisYear`, `getYearlyBalance`, `computePopulationGrowth`.
- **Aktionen:** `buyMeasure(game, id, { resolveMeta, forceNegBalance })`,
  `demolishMeasure(game, id)`. `buyMeasure` liefert bei `reason:
  'negative-balance'` eine Signal-Struktur — die View zeigt `confirm()`, und
  ruft `buyMeasure` erneut mit `forceNegBalance: true` auf.
- **Tick:** `tick(game)` — simuliert ein Jahr, gibt
  `{ newEvents, unlockedAchievements, citizenEvent, endReason, timelineSample }`
  zurück. `endReason ∈ {null, 'won', 'pleite', 'ueberflutet'}`.
- **Bürger-Events:** `maybeCitizenEvent`, `applyCitizenChoice`, `getCitizenEvent`.
  Choices sind `apply(game, ctx)`-Funktionen, `ctx = { resolveMeta, pushEvent }`.
- **Achievements:** `tryUnlockAchievement(game, key)` — gibt Achievement-Objekt
  zurück, wenn frisch freigeschaltet, sonst null. `checkRenewable100(game)`
  wird intern in `buyMeasure` und `tick` aufgerufen.

Wichtig: Engine-Events puffern sich in `game._pendingEvents` und
`game._pendingAchievements`; `_beginEvents` / `_drainEvents` werden in jeder
mutierenden Aktion aufgerufen, sodass der Return nur die DIESE-Aktion-Events
enthält.

### Schritt 2 — `App/sims/klima/game-2d.html`

Alle zuvor inline definierten Regel-Konstanten durch `const MEASURES =
KlimaEngine.MEASURES;` etc. ersetzt. Alle Logik-Funktionen sind jetzt dünne
Wrapper:

- `state` ist jetzt `let state = KlimaEngine.createGame(1)` +
  `Object.assign(state, { phase, speed, MS_PER_TICK, tutorial*, pendingCitizenEvent })`.
  `startGame(lvl)` ersetzt `state` durch eine frische Engine-Instanz
  (dank `let` greifen alle View-Funktionen auf die neue Instanz zu).
- `simulateTick()` ruft `KlimaEngine.tick(state)` auf und reicht das Resultat
  an `consumeEngineEvents`, `consumeEngineAchievements`, `triggerCitizenDialog`,
  `endLevel` weiter.
- `buyMeasure(id)` / `demolishMeasure(id)` sind UI-Wrapper um die Engine;
  negative Bilanz wird weiterhin als confirm()-Dialog angezeigt.
- `showEvent` wurde in `showEventUI(ev)` (reines UI, für Engine-Events) und
  `showEvent(icon,text,type,infoKey)` (Legacy-Einstieg, pusht + UI)
  aufgespalten, um doppelte Event-Stapel-Einträge zu vermeiden.
- `unlockAchievement(key)` nutzt `KlimaEngine.tryUnlockAchievement`,
  `notifyAchievementUI(a)` macht Toast + Sound + Server-POST.
- `serializeState` / `deserializeState` delegieren an die Engine und hängen
  View-Flags (phase, tutorial) an/lesen sie wieder aus.
- `pickBuildPosition` bleibt View-seitig. `resolveMeta: () => ({ pos:
  pickBuildPosition(id) })` spreizt die Position in das Instance-Objekt,
  damit der bestehende Canvas-Draw-Code (`inst.pos.x/y`) unverändert läuft.

Build-Positionen in `ownedMeasures[id].instances[i].pos.{x,y}` — das Format
der Saves ist rückwärtskompatibel (alte Saves der V2-2D laden ohne
Migration).

### Weitere Fixes / Hausaufgaben, die im selben Zug reingefallen sind

- `.ggs-event-viewport` volle Canvas-Breite (10px Rand links/rechts),
  Toasts mit 8px/16px Innenabstand (flacher).
- Hintergrundmusik: Default-Volume 22 %, Auto-Start bei erster User-Geste,
  wenn state.playing !== false.
- Schornstein + Rauchfahne bei Häusern (in `drawHouse`): Rauch verblasst
  proportional zu `state.renewablePower / powerDemand`, volles 100 %
  erneuerbar → kein Rauch.
- Helikopter in `drawAirport` fliegt nach seed-basiertem Zyklus
  (ca. 26-34 s): 8 % Start, 64 % Flug, 8 % Anflug, 20 % Pad-Parken.

### Gesendet: Start-Meldung

`_inbox/zentrale/2026-04-18-2200-klima-engine-start.md` ging an Atlas raus
("Starte jetzt Schritt 1"). Eine Fertig-Meldung für Schritt 1+2 sollte
raus, sobald die neue Session die 2D-Refactor-Regression-Tests durch hat
(Level starten, Maßnahme kaufen, Bürger-Event beantworten, Save/Load,
Level gewinnen/verlieren).

## Was noch offen ist

### Schritt 3 — `App/sims/klima/game-3d.html` (V2)

**Der dickste Brocken.** Neue Datei, analoges Pattern wie 2D V2:

- `<script src="engine.js"></script>` + `<script src="audio.js"></script>`
- `<link rel="stylesheet" href="../../assets/css/design-system.css">`
- Header mit Speed-Control, 📚-Button, 🔊 Mute, 🔄 Reset, 🎵 Music-Widget
- Status-Panels links (Status, Bilanz), Action-Cards rechts,
  Graphen-Tabs, Event-Stapel wie in 2D
- Glossar-Tooltips via `data-glossar`
- Autosave localStorage-Key: `ggs-save-klima-3d-L{n}`
- `ggsMusicSetup([...])` mit den 4 Klima-Tracks

**Three.js per CDN:**

    <script src="https://unpkg.com/three@0.155.0/build/three.min.js"></script>

Szenen-Baustellen aus `App/src/sims/sim-05-treibhaus-3d/*.ts` als
Vorlage — NICHT importieren, sondern in Vanilla JS portieren.
Übernehmen:
- Scene + Camera + Lights + Insel-Geometrie
- 3D-Modelle der Maßnahmen (Windrad, Solar, Deich, Mangrove …)
- Drag-Orbit + Scroll-Zoom + Auto-Drohnen-Flug
- Raycaster für Platzierung, Placement-Ghost (grün/rot)
- Arrow-Keys für Rotation

`resolveMeta` liefert in 3D etwas wie `{ pos: {x,y,z}, rotation, zoneId }` —
Engine ist dafür agnostisch (spreizt nur in die Instance).

### Schritt 4 — `App/pages/klima-3d.php`

Analog zu `klima-2d.php`, lädt `App/sims/klima/game-3d.html`.
Level-Config kommt aus `game_levels` mit `game_id='klima'` (gleiche Quelle
wie 2D — 2D und 3D teilen sich die Admin-Einstellungen).

### Schritt 5 — module_info / Lehrplan

Nachricht an `_inbox/lehrplan/` schicken:

- `module_info`-Eintrag `klima` um 2D/3D-Toggle erweitern
- `modul-klima.php`-Detailseite: "▶ 2D spielen" / "▶ 3D spielen" Buttons

## Regression-Tests (für neue Session, bevor Fertig-Meldung)

1. `http://localhost/geograsim/App/pages/klima-2d.php?level=1` aufrufen
2. Level-Select → "Lernen" wählen → Spiel startet mit Budget 600, Pop 6000
3. Eine Windkraft kaufen → Toast, `🌬️ Windkraft-Pionier`-Achievement
4. Kohlekraftwerk kaufen → First-Buy-Hint "Kohle liefert …"
5. Einige Ticks laufen lassen → Timeline + Graphen füllen sich,
   Bürger-Event bei Tick 30 (Lina, Fischerin) → eine Option wählen
6. Reload → Spielstand wird wiederhergestellt
7. Reset → Speicher gelöscht
8. Level 2/3 durchlaufen, Events + Achievements vergleichen

## Bestätigen

- Diese Notiz steht als `status: uebergabe` in klima-Inbox
- Wenn neue Session anfängt: Schritt 3 starten, vorher in dieser Inbox
  checken, ob Atlas oder Thomas noch was drangelegt haben
- Erwartete Kontext-Größe für Schritt 3 allein: vermutlich eine komplette
  Session (Three.js-Szenenaufbau + 12 Maßnahmen-Modelle + Platzierung)
