Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
17 KiB
Markdown
198 lines
17 KiB
Markdown
# Saumwacht · Architektur der Glutpakt-Slice
|
||
|
||
Stand: 16. September 2026. Das Projekt implementiert ein lokales Match mit Godot 4 und GDScript: zehn Glutpakt-Wächter, wahlweise 20 Wellen mit vier Bossen oder acht Wellen mit zwei Bossen, fünf Söldner und eine Gegner-KI in zwei Stufen. `AssetLibrary` bindet die Icons und GLB-Modelle aus Phase 3/4 ein; zehn hochdetaillierte Gegnermodelle ergänzen sie. Weitere Fraktionen sind im Dossier und als Assets vorbereitet.
|
||
|
||
GDScript eignet sich hier für kurze Iterationen, direkte Godot-Integration und einen Start ohne zusätzliche .NET-Laufzeit. Die Logik verwendet Typangaben an Schnittstellen und für zentrale Werte. Ein späterer Wechsel einzelner rechenintensiver Teile ist möglich, sollte aber erst nach Profiling erfolgen. Die ausführbare Referenz ist die lokale Godot-Version 4.7.2.
|
||
|
||
## Projekt und Szenen
|
||
|
||
```text
|
||
saumwacht-godot/
|
||
├── project.godot
|
||
├── scenes/main.tscn
|
||
├── scripts/
|
||
│ ├── main.gd
|
||
│ ├── autoload/
|
||
│ │ ├── game_state.gd
|
||
│ │ ├── wave_manager.gd
|
||
│ │ └── economy_manager.gd
|
||
│ ├── data/data_catalog.gd
|
||
│ ├── ai/simple_ai.gd
|
||
│ ├── combat/
|
||
│ │ ├── battle_controller.gd
|
||
│ │ ├── unit.gd
|
||
│ │ ├── movement_component.gd
|
||
│ │ ├── targeting_component.gd
|
||
│ │ ├── attack_component.gd
|
||
│ │ └── ability_component.gd
|
||
│ └── ui/hud.gd
|
||
├── tools/
|
||
│ ├── balance_sim.gd
|
||
│ └── balance_sim.tscn
|
||
├── tests/test_runner.tscn
|
||
├── tests/test_runner.gd
|
||
└── docs/
|
||
├── architecture.md
|
||
├── balance-report.md
|
||
└── asset-handoff.md
|
||
```
|
||
|
||
`main.tscn` ist bewusst klein. `main.gd` erzeugt Karte, Baufelder und Beleuchtung; `hud.gd` erstellt die bedienbaren Controls. Die Laufzeit-Hierarchie hat folgende Grundstruktur:
|
||
|
||
```text
|
||
Main (Node3D, main.gd)
|
||
├── World (Node3D)
|
||
│ └── Lane-, Baufeld- und Kerngeometrie, Beleuchtung
|
||
├── Camera3D (orthografische Draufsicht)
|
||
├── BattleController (Node3D)
|
||
│ └── CombatUnit (Node3D) × aktuelle Wächter und Angreifer
|
||
│ └── austauschbare Darstellung, HP-/Schildbalken, Auswahlmarkierung
|
||
└── CanvasLayer
|
||
└── HUD (Control)
|
||
└── Ressourcen, Bau-/Einheitenaktionen, Sends, Wellenbericht
|
||
```
|
||
|
||
`CombatUnit` wird aus dem Script erzeugt. Eine eigene `.tscn` ist dafür noch nicht nötig. Die vier Komponenten sind `RefCounted`-Objekte mit klaren Zuständigkeiten und keine zusätzlichen SceneTree-Knoten. Die Darstellung nutzt inzwischen importierte GLB-Geometrie, während Trefferpunkte, Ziele, Angriffslogik und Besitz getrennt bleiben. `AssetLibrary` fasst starre Formteile materialweise im Speicher zusammen; `AssetLibrary` lädt auch alle zehn Gegnertypen als GLB mit Gelenkgruppen.
|
||
|
||
## Zuständigkeiten und Datenfluss
|
||
|
||
| Baustein | Verantwortet | Schnittstelle |
|
||
| --- | --- | --- |
|
||
| `GameState` – Autoload | Phase, Wellennummer, Kern-HP, Pause, Tempo, Ergebnis und letzte Berichte | `reset_match()`, `set_phase()`, `apply_core_damage()`, `decide_winner()` |
|
||
| `EconomyManager` – Autoload | Besitz, Gold, Funken, Einkommen, Arbeiter, reservierte Sends, Rücknahmehistorie | `apply_command()`, `get_team()`, `open_build()`, `freeze()`, `settle()` |
|
||
| `WaveManager` – Autoload | Timer, Phasenübergänge, KI-Aufruf, Kampfstart und Abrechnung | `attach_battle()`, `start_match()`, `ready_for_battle()`, `advance()` |
|
||
| `AssetLibrary` – Autoload | ID-Auflösung, Icon-Cache und im Speicher zusammengefasste Modellgeometrie | `icon()`, `instantiate_model()`, `bake_mesh()` |
|
||
| `DataCatalog` – Autoload | Wächter-/Gegner-/Söldnerdefinitionen und skalierte Wellen | `get_unit()`, `get_wave()`, `get_attackers()`, `get_send()` |
|
||
| `BattleController` | Flüchtige Kampfinstanzen beider Lanes, Simulationsreihenfolge, Lecks und Prämien | `show_formation()`, `begin_battle()`, `step()` |
|
||
| `CombatUnit` | HP, Schild, Effekte, Ziel, vier Komponenten und Darstellung | `setup()`, `receive_damage()`, `heal()`, `apply_effect()`, `apply_dot()` |
|
||
| `SimpleAI` | Bauentscheidungen aus Budget, Welle und erlaubten Gegnerinformationen in zwei Stufen (Lernmodus/Standard, Dossier § 9.2) | `plan()`; benutzt dieselben Economy-Kommandos wie der Mensch |
|
||
| `Main` und `HUD` | Eingabe, Auswahl, Kamera, Anzeige | Lesen Snapshots; senden Kommandos und Bereitschaft |
|
||
|
||
Die Autoloads machen die zentralen Dienste unabhängig vom Inhalt der aktiven Szene verfügbar. Sie ersetzen keine klare Besitzregel: Der `EconomyManager` hält den dauerhaften Einheitenbesitz; der `BattleController` hält nur die aktuelle Kampfkopie. Beim Wechsel in die nächste Bauphase entsteht die Formation erneut aus dem gespeicherten Besitz. Gefallene Wächter gehen dadurch nicht verloren. Godot beschreibt den Mechanismus in der [Dokumentation zu Autoloads](https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html).
|
||
|
||
`DataCatalog` enthält die Einheitenwerte aus dem Dossier und getrennte Progressionen für Kurzpartie und Expedition. Basiseinheiten werden über `BASE_IDS` angeboten; eine Aufwertung wird über `upgrade_id` gefunden. Aufwertungspreise sind Zusatzkosten. Wellen skalieren Angreiferwerte beim Abruf und liefern eigenständige Dictionaries. Auch `get_team()` liefert eine tiefe Kopie: HUD oder KI können durch Änderungen an einem Snapshot keine Ressourcen erzeugen.
|
||
|
||
## Zustandsmaschine
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> MENU
|
||
MENU --> BUILD: Partie starten
|
||
BUILD --> COMBAT: Bereit oder Bauzeit abgelaufen / freeze
|
||
COMBAT --> RESULTS: Beide Lanes abgeschlossen / settle
|
||
RESULTS --> BUILD: 4 Sekunden oder Weiter / nächste Welle
|
||
COMBAT --> GAME_OVER: Kern zerstört oder gewählte letzte Welle abgeschlossen
|
||
GAME_OVER --> BUILD: Neue Partie / vollständiger Reset
|
||
```
|
||
|
||
`MENU` ist der Initial- und Reset-Zustand. Die Hauptszene kann die erste Partie direkt starten; das Eröffnungs-Overlay bietet die KI-Stufe (Lernmodus/Standard, Dossier § 3.1) und übernimmt die Auswahl in `GameState.ai_level`, das `WaveManager` bei jeder Bauphase an die KI weiterreicht. Bauzeit: 45 Sekunden in Welle 1, danach 30 Sekunden. Kämpfe enden beim Abschluss beider Lanes oder spätestens nach 90 Sekunden. Nach gewöhnlichen Wellen folgt eine viersekündige Abrechnung; die gewählte letzte Welle führt direkt zum Endergebnis.
|
||
|
||
Nur `WaveManager` führt im normalen Spiel die Übergänge aus. Seine `_physics_process()`-Methode betreibt die Simulation mit dem Godot-Physiktakt. Bei 2× Tempo werden zwei gleich große Simulationsschritte ausgeführt. Pause verhindert den Fortschritt von Timern und Kampf, während Baukommandos in der pausierten Bauphase zulässig bleiben.
|
||
|
||
Beim Festschreiben werden zuerst weitere Wirtschaftsänderungen gesperrt. Danach werden die Reservierungen beider Seiten gemeinsam verbindlich, Einkommen erhöht und die Send-Aufträge als Angreifer der gegenüberliegenden Lane übernommen. Erst dann startet der Kampf. Eine UI-Aktion kann somit nicht nachträglich noch den eingefrorenen Kampf verändern.
|
||
|
||
## Wirtschaft als Kommandoschnittstelle
|
||
|
||
Jedes Kommando hat einen Typ und nur die dazugehörigen Nutzdaten:
|
||
|
||
```gdscript
|
||
var response: Dictionary = EconomyManager.apply_command(0, {
|
||
"type": "build",
|
||
"type_id": "g01",
|
||
"cell": Vector2i(2, 3),
|
||
})
|
||
if not response.ok:
|
||
print(response.message)
|
||
```
|
||
|
||
| `type` | Weitere Felder | Wirkung |
|
||
| --- | --- | --- |
|
||
| `build` | `type_id`, `cell: Vector2i` | Freie Zelle belegen und Kaufpreis abbuchen |
|
||
| `upgrade` | `uid: int` | Aufwertung gegen Zusatzpreis, gleiche ID und Zelle |
|
||
| `sell` | `uid` | 75 % der tatsächlichen Investition, abgerundet, erstatten |
|
||
| `move` | `uid`, `cell` | Kostenlos auf freie Zelle versetzen |
|
||
| `target` | `uid`, `policy: int` | 0 = nächstes Ziel, 1 = niedrigste HP, 2 = höchste Maximal-HP |
|
||
| `worker` | keine | Nächste Preisstufe bezahlen, Arbeiter für folgende Bauphase bestellen |
|
||
| `queue_send` | `type_id` | Funken unter Vorrats-, Mengen- und Wellenbudgetgrenze reservieren |
|
||
| `cancel_send` | `index: int` | Reservierung des gewählten Auftrags freigeben |
|
||
| `undo` | keine | Letzten erfolgreichen Wirtschaftsschritt dieser Bauphase zurücknehmen |
|
||
|
||
Die Antwort ist immer `{"ok": bool, "message": String}`. Ein abgewiesener Befehl verändert weder Ressourcen noch Rücknahmehistorie. Jede Mannschaft besitzt eine eigene Historie. Einheiten-IDs steigen auch nach Rücknahme weiter an; die IDs beider Teams haben getrennte Zahlenbereiche.
|
||
|
||
`open_build(wave)` aktiviert bestellte Arbeiter und produziert genau einmal. `freeze(wave)` bucht die vorgemerkten Sends genau einmal ab und liefert `[aufträge_team_0, aufträge_team_1]`. `settle(team, wave, bounty)` zahlt Grundgold, das inzwischen aktuelle Einkommen und Abschussprämien genau einmal aus. Wiederholte Aufrufe derselben abgeschlossenen Transaktion sind unschädlich. Diese Sicherung verhindert doppelte Auszahlungen durch wiederholte UI-Signale oder spätere Netzwerk-Wiederholungen.
|
||
|
||
## Kampfinstanzen und Komponenten
|
||
|
||
`begin_battle(rosters, attackers, wave)` erhält zwei Aufstellungen und zwei Arrays skalierter Angreifer. `step(delta)` gibt einen Bericht zurück: `finished`, `elapsed`, `leak_damage`, `bounties` und `leaks`. `leak_damage` ist ausschließlich der neu entstandene Schaden dieses Schritts; Prämien und Leckzahlen sind kumulativ. Der `WaveManager` darf kumulativen Schaden daher nicht nochmals anwenden.
|
||
|
||
| Komponente | Aufgabe |
|
||
| --- | --- |
|
||
| Bewegung | Bewegung auf der XZ-Ebene, Reichweitenabstand und lokale Trennung von Figuren |
|
||
| Zielwahl | Zielpolitik, Lane-Zugehörigkeit, Beibehaltung gültiger Ziele und stabile ID bei Gleichstand |
|
||
| Angriff | Intervall und Grundangriff; Weitergabe erfolgreicher Angriffe an Fähigkeiten |
|
||
| Fähigkeiten | Start-, Intervall- und Trefferfähigkeiten; Schaden über Zeit wird durch die Statusverwaltung der Unit fortgeführt |
|
||
|
||
Kernschaden beider Lanes wird in einem gemeinsamen Schritt angewendet, bevor der Sieger entschieden wird. Sterben beide Kerne dabei, ist das Ergebnis unentschieden. Lecks werden einzeln als erledigt markiert und geben keine Abschussprämie. Am Timeout wird zunächst der reguläre Schritt abgearbeitet, danach werden die restlichen Angreifer als Lecks entfernt. Die Aufstellung und Investitionen bleiben erhalten.
|
||
|
||
Die Statusverwaltung der Unit bündelt Schilde, Schaden über Zeit, Heilung und zeitlich begrenzte Effekte. Die 3×3-Schadenstypmatrix liegt an der Schadensannahme; Fähigkeiten verwenden denselben Weg. Statische Typangaben unterstützen die Prüfung der Schnittstellen im Editor; die Möglichkeiten und Grenzen beschreibt [Godots Anleitung zu statischem GDScript](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/static_typing.html).
|
||
|
||
## Erweiterung zu Netzwerkspiel
|
||
|
||
Die spätere Grenze ist bereits erkennbar: Der Client sendet Befehle mit Team, Wellennummer und Sequenz-ID; ein autoritativer Host validiert sie mit den Wirtschaftsregeln. Der Host führt Phasen und Kampf aus und verteilt freigegebene Zustandsabbilder und Ereignisse. Für den Transport müssten `Vector2i`-Zellen in einfache Zahlenpaare übersetzt und Befehle gegen doppelte Sequenz-IDs abgesichert werden.
|
||
|
||
Die vorhandenen lokalen Methoden sind noch kein Netzwerkprotokoll. Es fehlen Verbindung, Authentifizierung, Latenzbehandlung, Wiederverbindung und Snapshot-Serialisierung. Eine stabile Zielreihenfolge und ein fester Takt helfen beim Debuggen, garantieren aber keinen plattformübergreifend deterministischen Lockstep. Autoritative Zustandsabgleiche sind deshalb die vorgesehene Erweiterung. Die KI erhält eine freigegebene gegnerische Aufstellung aus der zuletzt abgeschlossenen Bauphase; verborgene aktuelle Send-Aufträge gehören nicht zu ihren Eingaben.
|
||
|
||
## Start und Prüfung
|
||
|
||
`project.godot` im Godot-Projektmanager importieren und mit **F6/F5** die Hauptszene bzw. das Projekt starten. Alternativ aus dem Projektordner:
|
||
|
||
```bash
|
||
godot --path . --editor
|
||
godot --path .
|
||
```
|
||
|
||
Nach Änderungen an Klassen oder beim frischen Import zunächst den Editorimport durchführen. Der Szenen-Testlauf lädt dieselben Autoloads wie das Spiel:
|
||
|
||
```bash
|
||
godot --headless --path . --editor --quit
|
||
godot --headless --path . res://tests/test_runner.tscn
|
||
```
|
||
|
||
Der Testprozess meldet `PASS` und Exitcode 0 oder konkrete fehlgeschlagene Prüfungen und Exitcode 1. Die CLI-Optionen sind in [Godots Kommandozeilenanleitung](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html) dokumentiert.
|
||
|
||
Die Tests prüfen Wirtschaftsabrechnung und Rücknahme, Zellenbelegung, Schreibsperre im Kampf, alle neun Matrixkombinationen, einmalige Timeout-Lecks ohne Prämie, gleichzeitigen Kerntod, Reset, den Ablauf bis zum Ende von Welle 8 sowie die Verhaltensgrenzen beider KI-Stufen (Arbeiterdeckel 4/8, Send-Rhythmus, Budgetgrenzen). Komponentenprüfungen sichern drei exakte DoT-Ticks einschließlich Auffrischung ohne Zusatztreffer, Schildersetzung ohne Stapeln, den vierten Funkenlanzen-Treffer ohne rekursive Bonusangriffe und höchstens drei zusätzliche Aschenwerfer-Ziele in derselben Lane. Weitere Prüfungen kontrollieren eindeutige IDs beider Teams und Angreifer sowie die vollständige Wiederherstellung von HP, Status, Zielen und Cooldowns zwischen Wellen. Der Ablauf-Test erhöht ausschließlich in seiner Fixture die Kern-HP und setzt die Kampfzeit kurz vor das Timeout. Er prüft Zustandsübergänge und Abrechnung; er ist keine Balance-Simulation. Die ergänzende Phase-5-Abnahme simuliert drei vollständige Partien mit normalen Ressourcen; Ergebnisse und Kaufplan stehen in [phase5-balance.md](phase5-balance.md). Spielgefühl und Schwierigkeit müssen zusätzlich durch menschliche Partien beurteilt werden.
|
||
|
||
### Balance-Sweep
|
||
|
||
Für Wertänderungen gibt es einen deterministischen Prüfstand unter `tools/`:
|
||
zwei vollständige KI-Partien (Standard/Standard und Lernmodus/Standard) über
|
||
die produktionsidentische Wirtschaft und Kampfsimulation, ausschließlich auf
|
||
der Konsole:
|
||
|
||
```bash
|
||
godot --headless --path . res://tools/balance_sim.tscn
|
||
```
|
||
|
||
Der Lauf legt pro Welle Kampfdauer, Lecks, Kernschaden, Prämien, Gold und
|
||
Wächterzahl beider Fronten aus und schlägt bei verletzten Wirtschaftsgrenzen
|
||
mit Exitcode 1 fehl. Auswertung und erste Messpunkte vom 12. September 2026
|
||
stehen in [balance-report.md](balance-report.md). Werte im `DataCatalog`
|
||
sollten nur mit einem Vorher/Nachher-Vergleich dieses Sweeps geändert werden.
|
||
|
||
## Phase 6 · Darstellung, Einstellungen und Release
|
||
|
||
`AssetLibrary.instantiate_model()` bleibt der statische, nach Material zusammengefasste Weg. `instantiate_fighter()` erzeugt für Glutpakt-Figuren zwischengespeicherte Meshes pro benannter Gelenkgruppe. `animated_figure.gd` steuert ausschließlich lokale Transforms und Instanz-Trefferfarben. Neutrale Bounds und Dreieckszahlen bleiben erhalten. Die originalen `.blend`-/GLB-Einzelteile sind weiterhin editierbar.
|
||
|
||
`Preferences` speichert Lautstärke, Stummschaltung, Kamerafolge und Vollbild per `ConfigFile`. `Soundscape` lädt 13 eigene WAVs, verwaltet zwölf Player und begrenzt Kampfgeräusche anhand echter Millisekunden. Phasenwechsel, Angriffe, Fähigkeiten und Lecks lösen Klänge aus; das Audio verändert keine Spielregeln. Tests ohne Anzeige schalten Wiedergabe ab.
|
||
|
||
Das HUD-Optionsmenü merkt sich Pause und Tastaturfokus und stellt beide beim Schließen wieder her. Kameraorbit, freie Verschiebung und Reset liegen in `main.gd`. Gesundheitsbalken orientieren sich am Kamerabasisvektor; Boss-HP wird zusätzlich im HUD angezeigt.
|
||
|
||
`export_presets.cfg` und `tools/build_demo.py` erzeugen eigenständige Release-Pakete mit eingebettetem PCK. Der ausdrücklich gewählte Startparameter `--smoke-test` lädt `scripts/qa/export_smoke.gd` und prüft genau das geladene Paket. Entwicklungswerkzeuge und Dokumentation sind aus dem PCK ausgeschlossen.
|
||
|
||
## Expedition 0.8.0
|
||
|
||
`GameState.match_mode` bleibt über einen Neustart erhalten. `select_mode()` akzeptiert ausschließlich `short` und `expedition` im Menü oder nach Spielende; eine laufende Partie kann ihre Länge nicht wechseln. Das Menü zeigt Dauer, Bossanzahl und Wellenzahl passend zur Auswahl.
|
||
|
||
`DataCatalog.wave_count()` liefert 8 oder 20. `get_wave()` wählt die passende Komposition, Hinweise und Skalierung. EconomyManager, WaveManager, KI und HUD verwenden diese gemeinsame Grenze: Arbeiter sind bis zur vorletzten Welle erlaubt, die KI begrenzt späte Wirtschaftsplätze und die finale Abrechnung entscheidet den HP-Sieg. Die alten Kurzpartie-Prüfungen wählen ihren Modus ausdrücklich.
|
||
|
||
Die neue Abnahme `scripts/qa/expedition_test.gd` läuft mit `--expedition-test` auch aus dem exportierten Programm. Sie kontrolliert drei vollständige reguläre Partien, Bossfähigkeiten und Neustarts zwischen beiden Modi. Der Leistungstest unterstützt `--expedition`, um die letzte Welle bei 2× zu messen. Details: [Expeditionsbericht](../../docs/phase-08-vollstaendige-expedition.md).
|