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

20 KiB
Raw Permalink Blame History

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:

  1. power-ResourceupdatePowerGrid() 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)

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 Lebensdauer
  • spawnRefugeeBoat() — 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:

  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:

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:

  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.