docs: add ADR-0001 on balancing data location

This commit is contained in:
2026-08-10 13:02:57 +02:00
parent 40a19f39db
commit 6fa10f72de

View File

@@ -0,0 +1,134 @@
# 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.