Files
InfiniTower/docs/adr/0001-balancing-daten-als-rust-konstanten.md

135 lines
7.5 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.
# ADR-0001: Balancing-Daten als Rust-Konstanten hinter einer `Balance`-Resource
- **Status:** Angenommen
- **Datum:** 2026-08-10
- **Betrifft:** Wellenkurven, Kosten und Wirkung von In-Run-Upgrades, Workshop-Upgrade-Kurven, Gegner-Stats pro Archetyp, Card-Effekte
## Kontext
Der Slice braucht einen Ort für seine Balancing-Zahlen. Die Datenmenge ist klein und bekannt:
drei bis vier Gegner-Archetypen, die tragenden Turm-Stats (Damage, Attack Speed, Health,
Defense, optional Crit), Workshop-Upgrades mit 20–50 Leveln, Cards mit maximal sieben Stufen,
eine Ultimate.
Zwei Größen sind **nicht** aus dem Referenzspiel ableitbar und müssen von Hand getunt werden:
die HP-/Schadens-Skalierung pro Welle und die Kostenkurven der Upgrades. Das Wiki dokumentiert
beides nicht (siehe `docs/WikiData.md`). Wie teuer eine Tuning-Iteration ist, ist deshalb das
entscheidende Kriterium.
**Gemessen** (dieses Repo, `examples/perf_spike.rs` als Stellvertreter für einen realen
Gameplay-Crate, ~490 Zeilen; Bevy-Dependencies bereits kompiliert im gemeinsamen
`CARGO_TARGET_DIR`):
| Vorgang | Zeit |
|---|---|
| Inkrementeller Rebuild `--release` (Datei angefasst) | 9–10 s |
| Inkrementeller Rebuild `dev` | 22–26 s |
| Prozessstart bis Fenster und Frames | ~1,4 s |
| **Edit-to-see-Loop (Release)** | **~11 s** |
Der Release-Build ist schneller als der Debug-Build — das Linken der Debug-Info dominiert. Die
Annahme „Balancing braucht Release, also ist der Rebuild teuer" ist damit empirisch widerlegt.
Dagegen steht der Rest der Schleife: Eine Wellenkurve bewertet man, indem man von Welle 1 an
spielt. Das kostet Minuten, nicht Sekunden. Die ~11 s Rebuild sind ein einstelliger Prozentsatz
eines Tuning-Zyklus.
**Was Bevy 0.19 mitbringt** (geprüft über docs.rs und context7, nicht aus dem Gedächtnis):
- Es gibt **keinen** eingebauten generischen Loader für strukturierte Daten (RON/TOML/JSON) für
eigene Typen. Ein eigener `AssetLoader` ist Pflicht (Vorbild: Bevy-Beispiel `custom_asset`).
- Hot-Reload erfordert das **nicht-default** Cargo-Feature `file_watcher`.
`AssetPlugin::watch_for_changes_override` ist laut Doku ausdrücklich nicht der empfohlene Weg.
- `ron` (0.12.2) und `serde` (1.0.229) liegen bereits als transitive Dependencies im
`Cargo.lock`; sie direkt zu deklarieren kostet keine zusätzliche Compile-Zeit. `file_watcher`
dagegen zieht `notify` neu ein und funktioniert nur auf Desktop.
**Randbedingungen:** Desktop nativ zuerst, Mobile und Web später (siehe Slice-Scope in
`CONTEXT.md`) — auf Wasm gibt es keinen Dateisystem-Watcher, und Assets müssten asynchron über
HTTP geladen werden. Kein Modding-Ziel, keine externen Autoren, ein Entwickler mit Toolchain.
## Entscheidung
Die Balancing-Daten leben als **Rust-Konstanten in einem eigenen Modul `balance`**, das eine
einzige `Balance`-Resource befüllt. Gameplay-Systeme lesen ausschließlich `Res<Balance>` und
sehen die Konstanten nie.
Verbindlich:
1. `balance` enthält nur Daten und Datenstrukturen — keine Gameplay-Logik, keine Bevy-Systeme.
2. `Balance` ist eine `Resource` aus reinen Datenstrukturen, so geformt, dass sie ohne
Umbau serialisierbar wäre (flache Felder, Zahlen und Vektoren, keine `Handle`, keine
`Entity`). Ein `serde`-Derive wird **jetzt nicht** hinzugefügt.
3. Genau **eine** Funktion befüllt `Balance` aus den Konstanten. Sie ist die Naht, an der ein
späterer Asset-Ladepfad ansetzt.
4. Kein Gameplay-Code liest eine Konstante aus `balance` direkt. Diese Regel ist die einzige
Bedingung, unter der die Entscheidung ihren Wert behält.
## Verworfene Alternativen
- **Konstanten verstreut im Gameplay-Code** — gleiche Iterationskosten wie die gewählte Lösung,
aber ein späterer Wechsel auf Assets würde jedes System anfassen, das eine Zahl liest.
- **Daten-Assets (RON) über den `AssetServer` mit Hot-Reload** — der Preis (eigener
`AssetLoader`, direkte `serde`-/`ron`-Deps, `file_watcher` mit `notify`, ein
Lade-Zustand im Start-Ablauf, ausgelieferte Asset-Dateien plus asynchrones Laden auf Web und
Mobile) kauft ~11 s pro Tuning-Zyklus, dessen teurer Teil das Spielen ist. Zusätzlich: Eine
Wellenkurve lässt sich mitten im Run ohnehin nicht sinnvoll austauschen — die laufende Welle
hat ihre Werte bereits vergeben, man startet den Run neu.
- **`bevy_common_assets` statt eigenem Loader** (0.17.0 unterstützt bevy 0.19.0) — spart den
Loader-Code, ändert aber nichts an den übrigen Kosten und tauscht sie gegen eine
Dependency, deren Release-Takt an Bevy-Versionen hängt.
- **Konfiguration über Umgebungsvariablen** (wie in `examples/perf_spike.rs`) — trägt für ein
paar Spike-Knöpfe, nicht für Tabellen mit 50 Leveln.
## Abweichung von der naheliegenden Wahl
Die übliche Antwort in der Spieleentwicklung lautet „Daten raus aus dem Code". Sie stammt aus
Kontexten, die hier nicht gelten: mehrere Designer ohne Compiler, Balancing-Patches ohne
Client-Update, Modding. Der Slice hat keinen davon. Wo die Zahl steht, ist deshalb nicht die
Frage, die die Iterationszeit entscheidet — die entscheidende Frage ist, ob man die Zahl ohne
Neustart ändern und die interessante Welle ohne Vorspielen erreichen kann. Beides adressiert
kein Dateiformat, sondern ein Debug-Panel, das `Balance` im laufenden Prozess mutiert, plus
einen Sprung-zu-Welle-Pfad. Beides setzt auf `Res<Balance>` auf, also auf genau die Naht, die
diese ADR festlegt — und beides ist billiger als eine Asset-Pipeline.
## Konsequenzen
**Positiv**
- Keine neuen Dependencies, kein neues Cargo-Feature, kein Lade-Zustand im Start-Ablauf.
- Keine Plattform-API im Spielkern — Mobile und Web erben keine Asset-Auslieferung.
- Falsche Zahlen und fehlende Felder sind Compile-Fehler statt Laufzeit-Parse-Fehler.
- Balancing-Werte sind in die Binary gebacken; ein Save kann nicht auf Werte verweisen, die es
im aktuellen Build nicht mehr gibt.
**Negativ**
- Jede Zahländerung kostet einen Rebuild (~11 s heute, wachsend mit dem Crate).
- Nicht-Entwickler können nicht balancen. Für den Slice irrelevant, für ein späteres Ziel nicht.
- Das Bevy-Asset-System bleibt in diesem Bereich ungelernt. Wenn das ein Lernziel ist, gehört es
in ein eigenes Ticket mit einem echten Asset (z. B. Sprites), nicht in die Balancing-Daten.
**Folgeaufgaben** (nicht Teil dieser ADR)
- Run-Telemetrie (Zeit bis Tod, Coins pro Welle, DPS-Verlauf) — ohne sie beschleunigt ein
schnellerer Edit-Loop nur das Raten.
- Debug-Panel und Sprung-zu-Welle-Pfad, beide auf `Res<Balance>`.
## Revisions-Auslöser
Diese Entscheidung wird neu getroffen, sobald **eines** der folgenden Ereignisse eintritt:
1. **Rebuild-Schwelle:** `cargo build --release` nach einer Änderung, die nur `src/balance/`
berührt, dauert wiederholt **über 30 s** (heute 9–10 s). Nachmessen mit derselben Methode wie
oben: Datei anfassen, bauen, drei Läufe, Median.
2. **Zyklus-Schwelle:** Eine protokollierte Tuning-Session enthält **mehr als 30
Rebuild-Zyklen pro Stunde**, deren Diff ausschließlich `src/balance/` betrifft. Das ist der
Punkt, an dem die 11 s messbar zur dominierenden Kostenstelle werden — vorher nicht.
3. **Scope-Änderung:** Balancing-Werte müssen von jemandem geändert werden können, der keine
Rust-Toolchain hat, oder ohne neuen Build ausgeliefert werden (Live-Balancing, Modding,
externe Tester). Keines davon steht im Slice-Scope.
Auslöser 1 und 2 werden zuerst durch das Debug-Panel beantwortet, nicht durch Assets — es ist die
billigere Antwort auf dieselbe Beobachtung. Erst wenn das Panel gebaut ist und die Schwelle
weiterhin gerissen wird, ist der Wechsel auf Assets fällig. Auslöser 3 führt direkt zu Assets.