fix(kampf): Gegner bekommen endlich ihre Werte

Gemeldet als "Ratten vergiften zu 100 %". Dahinter lagen drei Fehler:

1. Creature::from_def parste Attributnamen mit toml::from_str, das eine
   ganze TOML-Datei erwartet. Der Aufruf schlug fuer jeden Namen fehl,
   der Zweig hiess Err(_) => continue — jeder Gegner hatte seit M3 still
   die Standardwerte. Die Riesenratte: 100 Leben statt 14, der
   Knochenfuerst war eine grosse Ratte. Kampf gegen den ersten Gegner:
   22 Zuege statt 4.
2. attack_hit feuerte bei allen Traegern statt nur beim Angreifer, also
   vergiftete die Ratte auch bei eigenen Treffern. EventKind::
   bound_to_source benennt die zwei seitengebundenen Ereignisse.
3. Action::Damage wuerfelte die Trefferformel auch bei Selbstschaden —
   ein flinker Held wich seinem eigenen Gift aus.

Dazu ein Datenfehler: Das Gift stand in der Aktionsliste von rat_bite,
und Aktionen haben kein chance. Es haengt jetzt am Trigger mit 25 %.

Drei Regressionstests, darunter der, den es seit M3 haette geben muessen:
ein Gegner bekommt die Werte aus seiner Definition.
This commit is contained in:
2026-08-13 08:33:32 +02:00
parent a8fea8b526
commit e145d5d2df
7 changed files with 190 additions and 4 deletions

View File

@@ -284,3 +284,52 @@ Initiativreihenfolge dieses Zuges. **Ein** Ansagepunkt, kein zweiter beim Kampfb
Ausdrücklich **nicht** dazugekommen ist ein Ziel an der Ansage: Die `ai` greift ausschließlich den
Helden an, `intent` trägt deshalb nur die Fähigkeits-ID. Ein Zielfeld käme mit dem ersten Gegner, der
seine Verbündeten heilt.
### Befunde aus der zweiten Balancing-Runde — drei Fehler, ein Symptom
Gemeldet wurde „Ratten vergiften zu 100 %, kaum zu überleben". Dahinter lagen drei unabhängige Fehler,
und der größte hatte mit Gift nichts zu tun.
**1. Jeder Gegner hatte seit M3 die Standardwerte.** `Creature::from_def` las die Attributnamen mit
`toml::from_str::<Attribute>("\"max_health\"")` — und `toml::from_str` erwartet eine ganze TOML-*Datei*,
kein nacktes Skalar. Der Aufruf schlug für **jeden** Namen fehl, der Zweig hieß `Err(_) => continue`,
und damit behielt jedes Wesen still die Voreinstellung: Die Riesenratte hatte **100 Leben statt 14** und
2 Schaden statt 3. Der Knochenfürst war eine große Ratte. Ein Kampf gegen den ersten Gegner dauerte 22
Züge.
Kein Test hat das gesehen, weil alle Kampftests ihre Zahlen selbst setzen und nie eine
Gegnerdefinition nachschlugen. Der Fehler ist ein stiller `continue` — die Sorte, gegen die Ticket 04
„jeder Verlust bekommt eine Zeile" beschlossen hat, nur eben in `game/` statt im Lader.
**2. Seitengebundene Ereignisse feuerten bei allen.** Dieses Ticket und 08 trennen `attack_hit`
(feuert beim Angreifer) von `damage_taken` (beim Getroffenen), ausdrücklich damit *„kein Trigger erst
prüfen muss, auf welcher Seite er steht"*. Die Trägerschleife in `drain_events` legte aber jedes
Ereignis jedem Beteiligten vor — der Gift-Trigger der Ratte feuerte deshalb auch, wenn der **Held**
zuschlug. Dieselbe Lücke hätte den Dornenpanzer des Angreifers reflektieren lassen.
`EventKind::bound_to_source()` benennt die beiden. Alle übrigen bleiben offen: `turn_started` muss bei
jedem ticken, sonst vergiftet sich nur der Spieler.
**3. Man verfehlte sich selbst.** Der M3-Befund „die Trefferformel war toter Code" wurde behoben, indem
`Action::Damage` sie *immer* würfelt. Ein Gift-Tick ist aber kein Angriff, und die Formel liest die
Geschicklichkeit des Ziels — ein flinker Held wich also seinem eigenen Gift aus. Im Log stand
„M verfehlt M". Neue Regel, eine Zeile: **Man verfehlt sich nicht selbst.**
**Dazu ein Datenfehler, kein Codefehler:** `rat_bite` legte `core:poisoned` in seiner *Aktionsliste* ab,
und Aktionen haben kein `chance` — das gibt es nur am Trigger (Ticket 08). Jeder Biss vergiftete also,
und weil erneutes Auflegen die Dauer auffrischt, war der Held ab dem ersten Treffer dauerhaft vergiftet.
Das Gift hängt jetzt am Trigger der Ratte mit `chance = 0.25` — genau der Fall, für den Ticket 08 das
Feld erfunden hat („20 % Chance, Brennen auszulösen, braucht sonst ein Script").
**Danach gemessen**, Elf-Dieb auf Stufe 1, je 40 Kämpfe:
| Gegner | Züge | Lebensverlust | Tode |
|---|---|---|---|
| Riesenratte | 4,2 | 6 | 0/40 |
| Skelett | 6,6 | 18 | 0/40 |
| Ghul | 15,8 | 94 | 24/40 |
| Knochenfürst | 9,9 | 110 | 40/40 |
Die Kurve *existiert* damit zum ersten Mal — vorher war jeder Gegner derselbe. Ob Etage 3 bis 5 fair
sind, sagt diese Tabelle **nicht**: Sie vergleicht einen Helden auf Stufe 1 mit Startdolch gegen Gegner,
denen er erst mit mehreren Aufstiegen und besserer Ausrüstung begegnet. Das entscheidet Spielen.

View File

@@ -235,6 +235,22 @@ direkt aus der Stapelregel aus Ticket 01 und wird wieder zuschnappen, wenn sie n
Startkampf danach: Krieger 2,5 Züge je Ratte, Dieb 3,9 bei 20 % Krit und 108 Leben.
**Zweite Balancing-Runde** (76 Tests) — gemeldet als „Ratten vergiften zu 100 %". Dahinter lagen
**drei unabhängige Fehler**, und der größte hatte mit Gift nichts zu tun:
| Fehler | Wirkung |
|---|---|
| `Creature::from_def` parste Attributnamen mit `toml::from_str`, das eine ganze Datei erwartet | **Jeder Gegner hatte seit M3 die Standardwerte.** Die Riesenratte: 100 Leben statt 14. Der Knochenfürst war eine große Ratte. Ein stilles `Err(_) => continue` |
| `attack_hit` feuerte bei *allen* Trägern statt nur beim Angreifer | Der Gift-Trigger der Ratte feuerte auch, wenn der **Held** zuschlug. Dieselbe Lücke hätte den Dornenpanzer des Angreifers reflektieren lassen |
| `Action::Damage` würfelte die Trefferformel auch bei Selbstschaden | „M verfehlt M" — ein flinker Held wich seinem eigenen Gift aus |
Dazu ein reiner Datenfehler: Das Gift stand in der *Aktionsliste* von `rat_bite`, und Aktionen haben
kein `chance`. Es hängt jetzt am Trigger — genau der Fall, für den Ticket 08 das Feld erfunden hat.
Der erste ist der teuerste Befund des Projekts und der einzige, den **kein** Werkzeug gemeldet hat:
kein Compiler, kein clippy, kein Test. Die Kampftests setzen ihre Zahlen selbst und schlugen nie eine
Gegnerdefinition nach. Gefunden hat es das Spielen, und zwar erst, als alles andere schon stimmte.
**Drei Befunde aus M1, die nur der Compiler liefern konnte** — alle in die Tickets zurückgetragen:
- **11**: Der Grenztest in seiner wörtlichen Fassung schlägt beim eigenen Doc-Kommentar an. Muss Kommentarzeilen ausnehmen.