From 6fa10f72de14168cbca51f4b17291353e2617519 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Mon, 10 Aug 2026 13:02:57 +0200 Subject: [PATCH] docs: add ADR-0001 on balancing data location --- ...001-balancing-daten-als-rust-konstanten.md | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 docs/adr/0001-balancing-daten-als-rust-konstanten.md diff --git a/docs/adr/0001-balancing-daten-als-rust-konstanten.md b/docs/adr/0001-balancing-daten-als-rust-konstanten.md new file mode 100644 index 0000000..697c7a4 --- /dev/null +++ b/docs/adr/0001-balancing-daten-als-rust-konstanten.md @@ -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` 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` 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`. + +## 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.