S2: balance-Modul und Balance-Resource #15

Closed
opened 2026-08-10 13:14:33 +00:00 by RDietermann · 1 comment
Owner

Part of #13

Setzt ADR-0001 um: Balancing-Daten als Rust-Konstanten in einem eigenen Modul balance, das eine Balance-Resource befüllt. Gameplay-Systeme lesen ausschließlich Res<Balance> und sehen die Konstanten nie.

Inhalt

  • Modul src/balance/ — nur Daten und Datenstrukturen, keine Gameplay-Logik, keine Bevy-Systeme.
  • Balance als Resource aus reinen Datenstrukturen, mit genau einer Funktion, die sie erzeugt. Diese Funktion ist die Naht, an der später ein Asset-Ladepfad ansetzen würde.
  • Für das Skelett reichen die Werte, die S3 bis S5 brauchen: Arena-Radius, Gegner-Tempo, Gegner-HP, Turm-Schaden, Turm-Feuerrate, Turm-HP, Trefferradius, Meta-Ertrag pro Kill.
  • Alle Werte sind Platzhalter und im Code als solche markiert. Die echten Zahlen entstehen erst beim Tunen — vier davon sind laut #8 gar nicht ableitbar.
  • caps als eigener Bereich vorsehen (ADR-0002 §2), auch wenn im Skelett noch nichts geclampt wird.

Akzeptanz

  • Kein Gameplay-System liest eine Konstante direkt; alle gehen über Res<Balance>. Prüfbar per Suche nach den Konstantennamen außerhalb von src/balance/.
  • Eine Änderung an einem Wert in src/balance/ ändert das Spielverhalten ohne weitere Codeänderung.
  • Der Edit-to-see-Loop bleibt im gemessenen Rahmen von ADR-0001 (~11 s Release-Rebuild plus Start).

Nicht in diesem Ticket: die Stat-Pipeline aus ADR-0002. Hier liegen nur die Rohwerte; wie Modifikatoren darauf wirken, kommt in der Verdickungsphase.

Part of #13 Setzt ADR-0001 um: **Balancing-Daten als Rust-Konstanten in einem eigenen Modul `balance`, das eine `Balance`-Resource befüllt.** Gameplay-Systeme lesen ausschließlich `Res<Balance>` und sehen die Konstanten nie. **Inhalt** - Modul `src/balance/` — nur Daten und Datenstrukturen, keine Gameplay-Logik, keine Bevy-Systeme. - `Balance` als `Resource` aus reinen Datenstrukturen, mit genau **einer** Funktion, die sie erzeugt. Diese Funktion ist die Naht, an der später ein Asset-Ladepfad ansetzen würde. - Für das Skelett reichen die Werte, die S3 bis S5 brauchen: Arena-Radius, Gegner-Tempo, Gegner-HP, Turm-Schaden, Turm-Feuerrate, Turm-HP, Trefferradius, Meta-Ertrag pro Kill. - Alle Werte sind **Platzhalter** und im Code als solche markiert. Die echten Zahlen entstehen erst beim Tunen — vier davon sind laut #8 gar nicht ableitbar. - `caps` als eigener Bereich vorsehen (ADR-0002 §2), auch wenn im Skelett noch nichts geclampt wird. **Akzeptanz** - Kein Gameplay-System liest eine Konstante direkt; alle gehen über `Res<Balance>`. Prüfbar per Suche nach den Konstantennamen außerhalb von `src/balance/`. - Eine Änderung an einem Wert in `src/balance/` ändert das Spielverhalten ohne weitere Codeänderung. - Der Edit-to-see-Loop bleibt im gemessenen Rahmen von ADR-0001 (~11 s Release-Rebuild plus Start). **Nicht in diesem Ticket**: die Stat-Pipeline aus ADR-0002. Hier liegen nur die Rohwerte; wie Modifikatoren darauf wirken, kommt in der Verdickungsphase.
RDietermann added the wayfinder:task label 2026-08-10 13:14:33 +00:00
RDietermann self-assigned this 2026-08-10 13:22:54 +00:00
Author
Owner

Gebaut. src/main.rs, src/balance/{mod,values}.rs, src/arena.rs, src/tower.rs — 392 Zeilen, keine neuen Dependencies.

Nachgeprüft, nicht geglaubt

  • cargo clean -p infinitower plus vollständiger Rebuild: null Warnungen.
  • cargo test: tower::tests::meta_run_meta_leaves_nothing_behind grün.
  • Der Agent hat den Test gegengeprobt — mit entferntem DespawnOnExit schlägt er fehl (left: 1, right: 0), danach wiederhergestellt und wieder grün. Er ist nicht vakuant.
  • Das Fenster selbst hat der Agent per Screenshot verifiziert, ich nicht: dunkle Arena-Scheibe, gelbes Turm-Quadrat in der Bildmitte, Spawn-Peripherie mit ~8 % Rand sichtbar.

Zwei Dinge, die besser sind als beauftragt

ADR-0001 ist compilerdurchgesetzt, nicht nur per Konvention. Die Konstanten in values.rs sind pub(super); Balance::from_constants() ist der einzige Weg heraus. Die Regel „kein Gameplay liest eine Konstante" lässt sich damit nicht versehentlich brechen, sondern nur absichtlich.

ADR-0002 §6 wird von einem Bevy-Built-in erfüllt. Statt eines Aufräum-Systems trägt die Turm-Entity DespawnOnExit(AppState::Run). Die ADR forderte „kein Aufräum-Code, den man beim nächsten Feature erweitern müsste" — hier gibt es kein System, das man falsch einordnen oder vergessen könnte. Der Test zählt außerdem alle Entities, nicht nur Türme, und fängt damit auch künftige run-gebundene Entities, die jemand ohne Run-Bindung spawnt.

Statt #[allow(dead_code)] steht #[expect(dead_code, reason = "read by the gameplay tickets #16-#18")] am balance-Modul — der Compiler fordert die Zeile zurück, sobald das letzte Feld einen Leser hat.

Platzhalterwerte

Feld Wert Herkunft
arena.radius 500,0 aus perf_spike.rs
enemy.speed 50,0 aus perf_spike.rs
enemy.health 10,0 frei geraten
tower.damage 5,0 frei geraten
tower.attack_interval 0,5 s frei geraten
tower.health 100,0 frei geraten
combat.hit_radius 10,0 aus perf_spike.rs
meta.gain_per_kill 1,0 frei geraten — siehe unten

Alle acht sind im Code als PLACEHOLDER ausgezeichnet.

Vier Befunde des Agenten, zur Kenntnis

  1. AppState::Run ist Default, damit cargo run ohne Tastendruck einen sichtbaren Turm zeigt. Die Alternative — im Startup heimlich NextState setzen — verschluckt das OnEnter(Meta) des ersten Frames und fiele bei #18 still auf die Füße. Mit // ponytail: markiert, Umdrehen ist eine Zeile und gehört zu #18.
  2. Balance.caps ist ein leeres Struct. ADR-0002 §2 verlangt einen Cap je geclamptem Stat — die fünf betroffenen Stats existieren im Skelett noch nicht. Der Bereich ist benannt und erreichbar, nur ohne Felder. Vertagung, kein Widerspruch.
  3. Ticket #15 sagt „Feuerrate", ADR-0002 §3 verbietet genau das. Der Agent ist der ADR gefolgt: tower.attack_interval in Sekunden, weil mult sonst kein Teiler auf eine Dauer sein kann. Das Ticket-Wording ist überholt, nicht der Code — genau die Reihenfolge, die die Karte vorgibt.
  4. type Stat = f32 liegt vorerst in balance. ADR-0002 §4 definiert den Alias, ordnet ihn keinem Modul zu. Wenn die Stat-Pipeline kommt, gehört er vermutlich in ein eigenes stats-Modul.

Der eine Punkt, der weiterwandert

meta.gain_per_kill ist ein flacher Betrag pro Kill — die Spec sagt aber, der Meta-Ertrag ist drop-basiert: Chance je Normalgegner, garantiert je Boss, Menge wellenabhängig. Das ist eine andere Mechanik, nicht bloß eine andere Zahl.

Für ein Skelett ohne Wellen und ohne Bosse ist der flache Wert das einzig Baubare, und der Agent hat die Abweichung am Konstanten-Doc vermerkt statt sie zu verschlucken. Damit sie nicht zur stillschweigenden Mechanikentscheidung gegen die Spec wird, ist sie als ausdrückliche Auflage an #18 gehängt.

**Gebaut.** `src/main.rs`, `src/balance/{mod,values}.rs`, `src/arena.rs`, `src/tower.rs` — 392 Zeilen, keine neuen Dependencies. ## Nachgeprüft, nicht geglaubt - `cargo clean -p infinitower` plus vollständiger Rebuild: **null Warnungen**. - `cargo test`: `tower::tests::meta_run_meta_leaves_nothing_behind` grün. - Der Agent hat den Test gegengeprobt — mit entferntem `DespawnOnExit` schlägt er fehl (`left: 1, right: 0`), danach wiederhergestellt und wieder grün. Er ist nicht vakuant. - Das Fenster selbst hat der Agent per Screenshot verifiziert, ich nicht: dunkle Arena-Scheibe, gelbes Turm-Quadrat in der Bildmitte, Spawn-Peripherie mit ~8 % Rand sichtbar. ## Zwei Dinge, die besser sind als beauftragt **ADR-0001 ist compilerdurchgesetzt, nicht nur per Konvention.** Die Konstanten in `values.rs` sind `pub(super)`; `Balance::from_constants()` ist der einzige Weg heraus. Die Regel „kein Gameplay liest eine Konstante" lässt sich damit nicht versehentlich brechen, sondern nur absichtlich. **ADR-0002 §6 wird von einem Bevy-Built-in erfüllt.** Statt eines Aufräum-Systems trägt die Turm-Entity `DespawnOnExit(AppState::Run)`. Die ADR forderte „kein Aufräum-Code, den man beim nächsten Feature erweitern müsste" — hier gibt es kein System, das man falsch einordnen oder vergessen könnte. Der Test zählt außerdem **alle** Entities, nicht nur Türme, und fängt damit auch künftige run-gebundene Entities, die jemand ohne Run-Bindung spawnt. Statt `#[allow(dead_code)]` steht `#[expect(dead_code, reason = "read by the gameplay tickets #16-#18")]` am `balance`-Modul — der Compiler fordert die Zeile zurück, sobald das letzte Feld einen Leser hat. ## Platzhalterwerte | Feld | Wert | Herkunft | |---|---|---| | `arena.radius` | 500,0 | aus `perf_spike.rs` | | `enemy.speed` | 50,0 | aus `perf_spike.rs` | | `enemy.health` | 10,0 | frei geraten | | `tower.damage` | 5,0 | frei geraten | | `tower.attack_interval` | 0,5 s | frei geraten | | `tower.health` | 100,0 | frei geraten | | `combat.hit_radius` | 10,0 | aus `perf_spike.rs` | | `meta.gain_per_kill` | 1,0 | frei geraten — **siehe unten** | Alle acht sind im Code als `PLACEHOLDER` ausgezeichnet. ## Vier Befunde des Agenten, zur Kenntnis 1. **`AppState::Run` ist Default**, damit `cargo run` ohne Tastendruck einen sichtbaren Turm zeigt. Die Alternative — im `Startup` heimlich `NextState` setzen — verschluckt das `OnEnter(Meta)` des ersten Frames und fiele bei #18 still auf die Füße. Mit `// ponytail:` markiert, Umdrehen ist eine Zeile und gehört zu #18. 2. **`Balance.caps` ist ein leeres Struct.** ADR-0002 §2 verlangt einen Cap je geclamptem Stat — die fünf betroffenen Stats existieren im Skelett noch nicht. Der Bereich ist benannt und erreichbar, nur ohne Felder. Vertagung, kein Widerspruch. 3. **Ticket #15 sagt „Feuerrate", ADR-0002 §3 verbietet genau das.** Der Agent ist der ADR gefolgt: `tower.attack_interval` in Sekunden, weil `mult` sonst kein Teiler auf eine Dauer sein kann. Das Ticket-Wording ist überholt, nicht der Code — genau die Reihenfolge, die die Karte vorgibt. 4. **`type Stat = f32` liegt vorerst in `balance`.** ADR-0002 §4 definiert den Alias, ordnet ihn keinem Modul zu. Wenn die Stat-Pipeline kommt, gehört er vermutlich in ein eigenes `stats`-Modul. ## Der eine Punkt, der weiterwandert `meta.gain_per_kill` ist ein **flacher Betrag pro Kill** — die Spec sagt aber, der Meta-Ertrag ist **drop-basiert**: Chance je Normalgegner, garantiert je Boss, Menge wellenabhängig. Das ist eine andere Mechanik, nicht bloß eine andere Zahl. Für ein Skelett ohne Wellen und ohne Bosse ist der flache Wert das einzig Baubare, und der Agent hat die Abweichung am Konstanten-Doc vermerkt statt sie zu verschlucken. Damit sie nicht zur stillschweigenden Mechanikentscheidung gegen die Spec wird, ist sie als ausdrückliche Auflage an #18 gehängt.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RDietermann/InfiniTower#15