docs: add ADR-0001 on balancing data location
This commit is contained in:
134
docs/adr/0001-balancing-daten-als-rust-konstanten.md
Normal file
134
docs/adr/0001-balancing-daten-als-rust-konstanten.md
Normal 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.
|
||||||
Reference in New Issue
Block a user