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