Files
Adminator 9a61f55cb1 Atlas: Infrastruktur + Team-Konventionen + Sprachregel Lernarbeit
- 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>
2026-04-19 11:51:14 +02:00

644 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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):**
```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 <NAME>:` 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<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
```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
<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`:
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.