docs: add ADR-0002 on stat and modifier pipeline
This commit is contained in:
339
docs/adr/0002-stat-und-modifier-pipeline.md
Normal file
339
docs/adr/0002-stat-und-modifier-pipeline.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# ADR-0002: Stat- und Modifier-Pipeline
|
||||
|
||||
- **Status:** Angenommen
|
||||
- **Datum:** 2026-08-10
|
||||
- **Betrifft:** Turm-Stats, Workshop, Cards, In-Run-Upgrades, Ultimate; Datentyp aller Stat-Werte
|
||||
- **Baut auf:** ADR-0001 (Balancing-Daten als Rust-Konstanten hinter `Balance`)
|
||||
|
||||
## Kontext
|
||||
|
||||
Vier Quellen wirken auf dieselben Turm-Stats: **In-Run-Upgrades** (verfallen am Run-Ende),
|
||||
**Workshop** (permanent), **Cards** (1–5 Slots im Slice) und die **Ultimate** (Death Wave,
|
||||
zeitlich begrenzt). Ohne feste Stapelregel wird jede neue Quelle zu einer eigenen Sonderregel,
|
||||
und ein Save, der einen berechneten Wert speichert, friert eine Formel ein, die sich noch ändert.
|
||||
|
||||
Zwei Randbedingungen aus dem Repo:
|
||||
|
||||
- Es gibt genau **einen** Turm. Ticket #4 hat gemessen: bei 10 000 Gegnern kostet die gesamte
|
||||
Gameplay-Logik **0,114 ms von 16,67 ms**; 96–98 % gehen ins Rendering. Eine Stat-Berechnung
|
||||
pro Frame wäre nicht messbar. Performance ist hier also **kein** Argument.
|
||||
- Alle Zahlen kommen laut ADR-0001 aus `Res<Balance>`. Diese ADR fügt keine Zahlen hinzu,
|
||||
sondern nur die Struktur, in der sie stehen. Auch die Clamp-Grenzen sind Balancing-Werte und
|
||||
leben in `Balance`.
|
||||
|
||||
**Geprüft an der Bevy-0.19-Doku** (docs.rs, über context7): Components lassen sich zur Laufzeit
|
||||
über `commands.entity(e).insert(..)` / `.remove::<T>()` an- und abheften; `RemovedComponents<T>`
|
||||
und die Lifecycle-Hooks `on_add`/`on_insert`/`on_discard`/`on_remove` existieren, werden hier
|
||||
aber **nicht** gebraucht (siehe Entfernungspfad). Change Detection über `Res<T>::is_changed()`
|
||||
und `Changed<T>` ist der Auslöser der Neuberechnung. `Resource` = weltweit genau einmal,
|
||||
`Component` = an eine Entity gebunden und stirbt mit ihr — dieser Unterschied trägt unten die
|
||||
Run-Grenze.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
### 1. Drei Akkumulatoren, ein Anwendungsmodus pro Stat
|
||||
|
||||
Jeder Stat sammelt drei Akkumulatoren:
|
||||
|
||||
| Akkumulator | Einheit | Verknüpfung |
|
||||
|---|---|---|
|
||||
| `flat` | absolute Einheiten des Stats | additiv |
|
||||
| `points` | Prozentpunkte (0,15 = +15 pp) | additiv, **einmal am Ende** geclamped |
|
||||
| `mult` | Faktor | multiplikativ |
|
||||
|
||||
Jeder Stat trägt zusätzlich **ein Bit**, seinen Anwendungsmodus:
|
||||
|
||||
- **`Direct`** — der Wert wächst mit dem Bonus:
|
||||
`(base + flat) × (1 + clamp(points)) × mult`
|
||||
- **`Interval`** — der Wert ist eine Zeitspanne in Sekunden und **schrumpft** mit dem Bonus:
|
||||
`(base + flat) × (1 − clamp(points)) ÷ mult`
|
||||
|
||||
Der Modus ist eine Eigenschaft des Stats, kein Sonderfall des Modifikators. Damit gibt es drei
|
||||
Akkumulatoren und vier Anwendungsregeln, ohne dass irgendwo eine fünfte Regel entsteht.
|
||||
|
||||
Zeitbasierte Stats bleiben **Intervalle, keine Raten**: Der Spieler liest „12 s Cooldown", nicht
|
||||
„0,083 Auslösungen/s". Eine Cooldown-Reduktion ist deshalb kein eigener Akkumulator, sondern
|
||||
`points` im `Interval`-Modus.
|
||||
|
||||
### 2. Clamp: summieren, dann einmal begrenzen
|
||||
|
||||
`clamp(points)` ist `points.clamp(0.0, cap)`. Das `cap` ist ein Balancing-Wert und steht in
|
||||
`Balance.caps`, pro Stat einer (Crit Chance, Defense %, jeder Cooldown). Es gibt keinen globalen
|
||||
Clamp und keinen Default im Code.
|
||||
|
||||
Geclamped wird **die Summe, genau einmal, nach der Summierung** — nie pro Quelle. Folgen, die
|
||||
ausdrücklich gewollt sind:
|
||||
|
||||
- Die Reihenfolge der Quellen ist egal; das Ergebnis ist assoziativ und kommutativ.
|
||||
- Es gibt **keine** abnehmenden Erträge, sondern eine harte Wand. Liefern drei Quellen je 30 pp
|
||||
Cooldown-Reduktion bei `cap = 0,60`, wirken 60 pp und 30 pp verfallen ersatzlos.
|
||||
- Die **ungeclampte** Summe bleibt für die Anzeige erhalten (siehe Sichtbarkeitsanforderung).
|
||||
Sie wird nicht persistiert, sondern bei der Neuberechnung nebenher ermittelt.
|
||||
|
||||
### 3. Stat-Tabelle
|
||||
|
||||
`base` kommt immer aus `Balance`. „Erwartet" nennt die Akkumulatoren, die im Slice tatsächlich
|
||||
bespielt werden — technisch stehen jedem Stat alle drei offen (die übrigen sind 0 bzw. 1).
|
||||
|
||||
| Stat | `base` | Modus | Erwartet | Clamp |
|
||||
|---|---|---|---|---|
|
||||
| `Damage` | > 0 | Direct | `flat` (Workshop, In-Run), `mult` (Cards) | — |
|
||||
| `AttackInterval` | > 0 | **Interval** | `mult` (Workshop, Cards, In-Run) | — |
|
||||
| `Range` | > 0 | Direct | `mult` | — |
|
||||
| `MaxHealth` | > 0 | Direct | `flat` (Workshop, In-Run), `mult` (Cards) | — |
|
||||
| `CritFactor` | > 0 | Direct | `mult` | — |
|
||||
| `CoinGain` | 1,0 | Direct | `mult` | — |
|
||||
| `CritChance` | 0 | Direct | `points` | `caps.crit_chance` |
|
||||
| `DefensePercent` | 0 | Direct | `points` | `caps.defense_percent` |
|
||||
| `HealthRegen` | **0** | Direct | `flat` (Workshop), `mult` (Cards) | — |
|
||||
| `DefenseFlat` | 0 | Direct | `flat`, `mult` | — |
|
||||
| `UltimateCooldown` | > 0 | **Interval** | `points` | `caps.cooldown_reduction` |
|
||||
| `WaveCooldown` | > 0 | **Interval** | `points` | `caps.wave_cooldown_reduction` |
|
||||
|
||||
`HealthRegen` und `DefenseFlat` sind der Grund für den flachen Akkumulator: Ihre Basis ist 0,
|
||||
und ein rein multiplikativer Bonus auf 0 bleibt 0. Dasselbe gilt später für Lifesteal und Thorns.
|
||||
|
||||
**`AttackInterval` ist der Stat, der die Zuordnung fast gesprengt hätte.** „Attack Speed ×1,25"
|
||||
ist genrenüblich eine Rate, und eine Rate widerspricht der Intervall-Regel. Der
|
||||
`Interval`-Modus löst das ohne Sonderregel: Die Karte liefert `mult = 1,25`, das Intervall wird
|
||||
geteilt, der Spieler sieht weiterhin „×1,25 Angriffsgeschwindigkeit", und der Schuss-Timer
|
||||
bekommt direkt eine Dauer. Deshalb heißt der Stat `AttackInterval` und nicht `AttackSpeed`.
|
||||
|
||||
### 4. `type Stat = f32`
|
||||
|
||||
Alias, kein Newtype. f32 trägt sieben signifikante Stellen — genug für den Slice.
|
||||
Revisions-Auslöser unten.
|
||||
|
||||
### 5. Nur Quellen werden persistiert
|
||||
|
||||
Ein Save enthält **niemals** einen berechneten Wert: gespeichert werden Workshop-Level, besessene
|
||||
und ausgerüstete Cards mit ihrem Level, die Slot-Zahl und (falls ein Run fortgesetzt wird) die
|
||||
In-Run-Upgrade-Level. Alles andere wird beim Laden neu gerechnet.
|
||||
|
||||
Das entkoppelt #7 (Save/Load) vollständig von dieser ADR: Jede Formeländerung wirkt rückwirkend
|
||||
auf bestehende Saves, ohne Migration. Es ist die einzige Festlegung hier, deren spätere Rücknahme
|
||||
wirklich teuer wäre.
|
||||
|
||||
### 6. Herkunft steckt im Ablageort, nicht im Datenmodell
|
||||
|
||||
Die Akkumulatoren tragen **keine** Herkunftsmarkierung. Sie sind ein reiner Cache und werden bei
|
||||
jeder Änderung **vollständig aus den Quellen neu gerechnet** — nie inkrementell addiert oder
|
||||
subtrahiert. Bei einem Turm und ≤ 5 Slots ist das eine Handvoll Multiplikationen; die einzige
|
||||
Alternative wäre Buchführung über das Rückgängigmachen einzelner Beiträge, und genau die geht
|
||||
schief.
|
||||
|
||||
Die Herkunft, die man wirklich braucht, ist die Run-Grenze. Sie wird nicht als Feld ausgedrückt,
|
||||
sondern als Ablageort:
|
||||
|
||||
| Lebensdauer | Ablage | Beispiele |
|
||||
|---|---|---|
|
||||
| permanent | `Resource` | `Balance`, `Workshop`, `OwnedCards`, `EquippedCards` |
|
||||
| Run-gebunden | `Component` **an der Turm-Entity** | `RunUpgrades`, `EffectiveStats`, Ultimate-Buff, alle Fähigkeits-Components |
|
||||
|
||||
Die Turm-Entity wird **beim Run-Start gespawnt und beim Run-Ende despawnt**. Damit verschwindet
|
||||
am Run-Ende genau das, was verschwinden muss, ohne dass irgendein Code es aufräumt. Kein
|
||||
`clear()`, keine Reset-Liste, die man beim nächsten Feature zu erweitern vergisst.
|
||||
|
||||
Für die UI-Frage „woher kommen meine 340 % Damage?" gibt es eine Funktion, die die
|
||||
Neuberechnung mit Zwischenwerten pro Quelle ausführt und das Ergebnis zurückgibt. Sie wird auf
|
||||
Anforderung aufgerufen, nicht im Gameplay-Pfad, und speichert nichts.
|
||||
|
||||
### 7. `EffectiveStats` wird bei Änderung berechnet — aus Nachvollziehbarkeit
|
||||
|
||||
`EffectiveStats` ist ein `Component` an der Turm-Entity, kein `Resource`. Genau ein System
|
||||
schreibt es, und es läuft, wenn eine Quelle sich geändert hat (`is_changed()` auf den
|
||||
Quell-Resources, `Changed<..>` auf den Quell-Components) — nicht jeden Frame.
|
||||
|
||||
Der Grund ist **nicht** Performance. Jeden Frame zu rechnen wäre bei 0,114 ms Gesamtbudget für
|
||||
die Gameplay-Logik nicht messbar. Der Grund ist, dass es dann genau einen Zeitpunkt gibt, an dem
|
||||
ein Stat sich ändert: ein Breakpoint, eine Log-Zeile, ein Wert, den man ansehen kann. Bei
|
||||
Neuberechnung pro Frame wäre jeder falsche Wert ein Beobachtungsproblem statt eines Fehlers an
|
||||
einer Stelle.
|
||||
|
||||
### 8. Card-Effekte sind ein `enum`
|
||||
|
||||
```rust
|
||||
type Stat = f32;
|
||||
|
||||
enum CardEffect {
|
||||
Modifier { stat: StatKind, kind: ModifierKind, value: Stat },
|
||||
Ability(AbilityKind),
|
||||
}
|
||||
|
||||
enum ModifierKind { Flat, Points, Mult }
|
||||
```
|
||||
|
||||
Kein Trait-Object, keine Registry, kein Plugin-Mechanismus. Null externe Autoren, kein
|
||||
Modding-Ziel (ADR-0001, Slice-Scope). Ein neuer Stat-Modifikator ist eine neue `StatKind`-Variante
|
||||
plus eine Zeile in `Balance`; der Compiler zeigt jede Stelle, die nachziehen muss. Genau das ist
|
||||
der Nutzen und nicht der Preis.
|
||||
|
||||
### 9. Fähigkeiten sind Components, keine Zahlen
|
||||
|
||||
Ein Card-Effekt der Gruppe `Ability` heftet ein eigenes Component an die Turm-Entity —
|
||||
`DeathRay { duration }`, `SlowAura { factor }` — mit einem eigenen System. Death Ray ist kein
|
||||
Stat und wird nicht in die Zahlen-Pipeline gepresst. Fähigkeits-Parameter (Dauer, Radius) leben
|
||||
im Component, nicht in `EffectiveStats`.
|
||||
|
||||
Das fängt auch die Modifikatoren ab, die gar nicht am Turm ansetzen: `SlowAura` verändert die
|
||||
Geschwindigkeit von Gegnern. Als Stat wäre das ein Fremdkörper, als Component am Turm, das
|
||||
das Gegner-Bewegungssystem liest, ist es die naheliegende Form.
|
||||
|
||||
**Fähigkeits-Components dürfen keinen Zustand tragen, der einen Run überdauern soll.** Cooldowns
|
||||
und Timer im Component sind in Ordnung — sie sollen beim Neuaufbau zurückgesetzt werden.
|
||||
„Einmal pro Run"-Zustand gehört in eine Run-gebundene Ablage, die nicht bei jedem
|
||||
Karten-Abgleich neu geschrieben wird.
|
||||
|
||||
### 10. Entfernungspfad für Fähigkeits-Components
|
||||
|
||||
Eine nicht entfernte Fähigkeits-Component ist der wahrscheinlichste Bug dieser Architektur: Sie
|
||||
zeigt sich Wochen später als „der Turm hat manchmal noch den Death Ray", und dann ist die
|
||||
Ursache nicht mehr rückverfolgbar. Der Schutz ist strukturell, nicht disziplinarisch:
|
||||
|
||||
**Ein einziges System `sync_tower_abilities` ist der einzige Schreiber von
|
||||
Fähigkeits-Components.** Es fügt nirgends an der Stelle ein, an der eine Karte gewechselt wird.
|
||||
Es läuft mit derselben Änderungserkennung wie die `EffectiveStats`-Neuberechnung und gleicht ab:
|
||||
Für jede `AbilityKind`-Variante wird das Soll (aus den Quellen) mit dem Ist (`Has<T>` an der
|
||||
Turm-Entity) verglichen, fehlende Components werden per `insert` ergänzt, überzählige per
|
||||
`remove::<T>()` entfernt. Weil abgeglichen und nicht ereignisweise gepflegt wird, fallen die drei
|
||||
geforderten Fälle auf denselben Pfad:
|
||||
|
||||
1. **Card-Wechsel im laufenden Run** — `EquippedCards` ändert sich → Abgleich läuft → die
|
||||
Component der abgelegten Karte wird per `remove::<T>()` entfernt, die der neuen eingefügt.
|
||||
Das ist der einzige Fall, in dem überhaupt jemand entfernt.
|
||||
2. **Run-Ende** — die Turm-Entity wird despawnt. Es gibt nichts zu entfernen; die Components
|
||||
verschwinden mit der Entity. Deshalb steht in Punkt 6, dass alles Run-gebundene an der
|
||||
Turm-Entity hängt.
|
||||
3. **Laden eines Saves** — der Save enthält nur Quellen (Punkt 5). Laden heißt: Quellen setzen,
|
||||
Turm frisch spawnen, Abgleich einmal vor dem ersten Gameplay-Frame laufen lassen. Eine
|
||||
übriggebliebene Component kann es nicht geben, weil die Entity gerade erst entstanden ist.
|
||||
|
||||
Dazu eine `debug_assert`-Prüfung am Ende des Abgleichs: Die Menge der Fähigkeits-Components an
|
||||
der Turm-Entity ist gleich der aus den Quellen abgeleiteten Menge. Bei einem Turm kostet das
|
||||
nichts und verwandelt den späten, unauffälligen Bug in einen sofortigen Abbruch.
|
||||
|
||||
Bevys Lifecycle-Hooks und `RemovedComponents` werden dafür ausdrücklich **nicht** verwendet: Ein
|
||||
Abgleich, der das Soll kennt, braucht keine Benachrichtigung darüber, was gerade passiert ist.
|
||||
|
||||
### 11. Sichtbarkeitsanforderung an jeden neuen Modifikator
|
||||
|
||||
Ein Modifikator, dessen Wirkung der Spieler nicht spürt, hat versagt — und keine technische
|
||||
Prüfung zeigt das an. Der Compiler akzeptiert einen Bonus, der gegen einen Clamp läuft, und ein
|
||||
Test, der die Formel prüft, ist grün.
|
||||
|
||||
**Verbindlich für jeden neuen Modifikator, vor dem Merge:**
|
||||
|
||||
1. Der Autor nennt eine Fortschritts-Stelle, an der der Modifikator wirken soll (Welle, Workshop-
|
||||
Stand, Card-Level).
|
||||
2. Er notiert den betroffenen `EffectiveStats`-Wert **vor und nach** dem Modifikator an dieser
|
||||
Stelle — mit der ungeclampten Summe, falls ein Clamp beteiligt ist.
|
||||
3. Er nennt die **beobachtbare** Folge in einer Größe, die die Run-Telemetrie aus ADR-0001
|
||||
ohnehin erfasst: Zeit bis Welle N, DPS-Verlauf, Coins pro Minute, überlebte Welle. Die
|
||||
Änderung muss dort über der Streuung zwischen zwei Läufen liegen.
|
||||
4. **Läuft der Modifikator an der genannten Stelle in einen Clamp, ist er abgelehnt** — nicht
|
||||
dokumentiert, nicht mit einem Hinweis versehen: abgelehnt oder umskaliert. Ein Bonus, der
|
||||
ersatzlos verfällt, ist im Spiel nicht von einem fehlenden Bonus zu unterscheiden.
|
||||
|
||||
Punkt 3 ist die einzige Anforderung, die sich nicht am Schreibtisch erfüllen lässt, und deshalb
|
||||
die einzige, die wirkt.
|
||||
|
||||
## Durchgerechnetes Beispiel
|
||||
|
||||
> **Alle Zahlen sind Platzhalter.** Die echten Werte entscheidet Ticket #8. Hier zählt der
|
||||
> Rechenweg, nicht das Ergebnis.
|
||||
|
||||
**Damage** (Modus `Direct`), Basis 10, Workshop-Level 12 zu +1,20 je Level, zwei Cards:
|
||||
|
||||
| Schritt | Rechnung | Wert |
|
||||
|---|---|---|
|
||||
| Basis aus `Balance` | — | 10,00 |
|
||||
| `flat` = Workshop Damage Lv 12 × 1,20 | 10,00 + 14,40 | 24,40 |
|
||||
| `points` = 0 (keine Quelle liefert pp auf Damage) | 24,40 × (1 + 0,00) | 24,40 |
|
||||
| `mult` = Card „Damage" Lv 3 (×2,40) × Card „Berserker" Lv 5 (×1,30) = 3,12 | 24,40 × 3,12 | **76,13** |
|
||||
|
||||
Ein In-Run-Upgrade „+15 pp Damage" (hypothetisch, ohne Clamp auf diesem Stat) ergäbe
|
||||
`24,40 × 1,15 × 3,12 = 87,55` — die Prozentpunkte greifen **vor** den Multiplikatoren, nicht
|
||||
danach.
|
||||
|
||||
**Health Regen** (Basis **0**), zeigt, wozu der flache Akkumulator da ist:
|
||||
`(0 + 4,00 Workshop) × 1,60 (Card Lv 2) = 6,40 HP/s`.
|
||||
Ohne flachen Akkumulator: `0 × 1,60 = 0`. Die Karte wäre wirkungslos.
|
||||
|
||||
**Ultimate-Cooldown** (Modus `Interval`), Basis 60 s, `caps.cooldown_reduction = 0,60`:
|
||||
Workshop −8 pp und Card −30 pp ergeben 38 pp → `60 × (1 − 0,38) = 37,2 s`.
|
||||
Kommt eine dritte Quelle mit −30 pp dazu, ist die Summe 68 pp, geclamped auf 60 pp →
|
||||
`60 × 0,40 = 24,0 s`. Von den 30 pp der dritten Quelle wirken nur 22, die restlichen 8 verfallen
|
||||
ersatzlos — eine vierte Quelle wäre komplett wirkungslos. Genau das lehnt die
|
||||
Sichtbarkeitsanforderung ab.
|
||||
|
||||
## Verworfene Alternativen
|
||||
|
||||
- **Nur additiv oder nur multiplikativ.** Rein additiv verpufft im Endgame, rein multiplikativ
|
||||
lässt Stats mit Basis 0 (Health Regen, später Lifesteal, Thorns) unerreichbar. Beide Formen
|
||||
scheitern an je einer Hälfte der Stat-Tabelle.
|
||||
- **Additiv innerhalb einer Quelle, multiplikativ zwischen Quellen** (die Frage aus dem Ticket).
|
||||
Klingt ordentlich, koppelt aber die Rechenregel an die Herkunft: Dieselbe Karte wirkt anders,
|
||||
je nachdem, ob ein Workshop-Upgrade denselben Stat trifft. Die drei Akkumulatoren erreichen
|
||||
dasselbe Verhalten, ohne dass ein Modifikator wissen muss, wer sonst noch am Stat hängt.
|
||||
- **Modifikatoren als Liste mit Herkunft, Entfernen durch Subtraktion.** Braucht eine stabile
|
||||
Identität pro Modifikator, Buchführung beim Run-Ende und ist gegen Rundungsdrift bei f32 nicht
|
||||
robust. Vollständige Neuberechnung kostet bei einem Turm nichts und hat keinen dieser Fehler.
|
||||
- **Card-Effekte als Trait-Objects oder Registry.** Kauft Erweiterbarkeit ohne externe Autoren.
|
||||
Der Preis ist der Verlust der Vollständigkeitsprüfung durch den Compiler — genau die Prüfung,
|
||||
die einen vergessenen Stat sichtbar macht.
|
||||
- **Fähigkeiten als Stat-Werte** (z. B. „Death-Ray-Stärke" als Zahl in `EffectiveStats`). Zwingt
|
||||
jede Fähigkeit in eine Skalarform, die sie nicht hat, und lässt `EffectiveStats` um Felder
|
||||
wachsen, die kein Gameplay-System liest.
|
||||
- **`EffectiveStats` als `Resource`.** Funktioniert bei einem Turm, verliert aber die Kopplung
|
||||
an die Entity-Lebensdauer — und damit den kostenlosen Reset am Run-Ende, der in Punkt 10 die
|
||||
Bug-Klasse verhindert.
|
||||
|
||||
## Offene Punkte, die diese ADR nicht entscheidet
|
||||
|
||||
- **`MaxHealth` ist der einzige Stat mit einem veränderlichen Ist-Wert daneben.** Steigt
|
||||
`MaxHealth` mitten im Run, muss entschieden werden, ob die aktuelle HP mitwächst, den Anteil
|
||||
behält oder unverändert bleibt. Das ist keine Frage der Pipeline, sondern des Health-Systems;
|
||||
es ist hier nur festgehalten, damit es nicht zwischen den Tickets verschwindet.
|
||||
- **Bedingte Modifikatoren** („nur gegen Bosse", „skaliert mit erlittenem Schaden" wie
|
||||
Berserker). Im Slice gibt es keinen; taucht einer auf, ist er entweder eine `Ability` oder er
|
||||
bricht die Zwei-Gruppen-Teilung des `enum` (siehe Revisions-Auslöser 3).
|
||||
|
||||
## Konsequenzen
|
||||
|
||||
**Positiv**
|
||||
|
||||
- Ein Save enthält keine Formel. Balancing- und Formeländerungen wirken rückwirkend, ohne
|
||||
Migration.
|
||||
- Das Run-Ende braucht keinen Aufräum-Code, den man beim nächsten Feature erweitern müsste.
|
||||
- Ein neuer Stat oder Modifikator erzeugt Compile-Fehler an genau den Stellen, die nachziehen
|
||||
müssen.
|
||||
- Die Stapelregel ist reihenfolgeunabhängig; es gibt keine „richtige" Sortierung von Quellen.
|
||||
|
||||
**Negativ**
|
||||
|
||||
- Der Clamp ist eine harte Wand. Ohne die Sichtbarkeitsanforderung entstehen wirkungslose
|
||||
Modifikatoren, die niemandem auffallen. Die Prüfung ist manuell und damit die schwächste
|
||||
Stelle dieser ADR.
|
||||
- Vollständige Neuberechnung skaliert nicht auf viele Turm-artige Entities mit vielen Quellen.
|
||||
Bei einem Turm ist das irrelevant, und die Alternative wäre die Buchführung, die oben
|
||||
verworfen wurde.
|
||||
- Ein neuer Effekt, der weder Stat-Modifikator noch Fähigkeit ist, erzwingt eine Änderung am
|
||||
`enum` und an jedem `match`. Das ist beabsichtigt, aber es ist Arbeit.
|
||||
- Die Ablageregel „Run-gebunden = Component an der Turm-Entity" ist eine Konvention. Sie hält
|
||||
nur, solange sie eingehalten wird; ein Run-Zustand, der versehentlich als `Resource` landet,
|
||||
überlebt den Run und bringt genau den Bug zurück, den Punkt 10 verhindern soll.
|
||||
|
||||
## Revisions-Auslöser
|
||||
|
||||
1. **Zahlentyp:** Ein Stat-Wert im Spiel überschreitet **10⁶**, oder eine Anzeige rundet
|
||||
sichtbar (zwei Läufe mit gleichem Stand zeigen unterschiedliche letzte Stellen). Dann wird
|
||||
`type Stat` neu entschieden — f64 zuerst, Big-Number-Semantik nur, wenn f64 auch reißt.
|
||||
2. **Mehrere Turm-artige Entities:** Sobald mehr als eine Entity `EffectiveStats` trägt, ist die
|
||||
Änderungserkennung pro Entity zu prüfen — global geänderte Quellen müssten dann alle
|
||||
betreffen, entity-eigene nur eine.
|
||||
3. **Dritte Effektgruppe:** Ein Card-Effekt, der weder als `Modifier` noch als `Ability`
|
||||
ausdrückbar ist, kippt die Zwei-Gruppen-Teilung. Ein Fall reicht; es ist keine Frage der
|
||||
Häufigkeit.
|
||||
4. **Clamp-Druck:** Wenn an der vorgesehenen Fortschritts-Stelle mehr als eine Quelle je Stat
|
||||
regelmäßig am Clamp anschlägt, ist nicht der Modifikator falsch, sondern das
|
||||
Prozent-Budget — dann wird die harte Wand gegen abnehmende Erträge neu bewertet.
|
||||
Reference in New Issue
Block a user