saumwacht-godot/docs/architecture.md
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
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>
2026-09-27 20:00:36 +02:00

17 KiB
Raw Permalink Blame History

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

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:

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.

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

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:

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.

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:

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:

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 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. 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:

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. 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.