ADR: Stat- und Modifier-Pipeline #6

Closed
opened 2026-08-10 07:58:33 +00:00 by RDietermann · 2 comments
Owner

Part of #1
Blocked by: #3

Entscheidung plus ADR unter docs/adr/. Das ist das Herzstück der Meta-Progression — hier laufen Workshop, Cards und die Ultimate zusammen.

Die Frage

  • Wie stapeln Modifikatoren: additiv, multiplikativ, oder additiv innerhalb einer Quelle und multiplikativ zwischen Quellen?
  • Wo wird gerechnet: jedes Frame neu, oder einmal bei Änderung in einen EffectiveStats-Component?
  • Wie sieht ein Card-Effekt als Datentyp aus, damit ein neuer Effekt keine Änderung am Kern erzwingt?
  • Zahlentyp: reicht f32 für den Slice, oder brauchen Idle-Kurven schon Big-Number-Semantik?

Fertig, wenn die ADR die Regel festlegt und ein durchgerechnetes Beispiel enthält: Base-Damage über Workshop-Level und zwei Cards bis zum Endwert.

Part of #1 Blocked by: #3 Entscheidung plus ADR unter `docs/adr/`. Das ist das Herzstück der Meta-Progression — hier laufen Workshop, Cards und die Ultimate zusammen. **Die Frage** - Wie stapeln Modifikatoren: additiv, multiplikativ, oder additiv innerhalb einer Quelle und multiplikativ zwischen Quellen? - Wo wird gerechnet: jedes Frame neu, oder einmal bei Änderung in einen `EffectiveStats`-Component? - Wie sieht ein Card-Effekt als Datentyp aus, damit ein neuer Effekt keine Änderung am Kern erzwingt? - Zahlentyp: reicht `f32` für den Slice, oder brauchen Idle-Kurven schon Big-Number-Semantik? **Fertig, wenn** die ADR die Regel festlegt und ein durchgerechnetes Beispiel enthält: Base-Damage über Workshop-Level und zwei Cards bis zum Endwert.
RDietermann added the wayfinder:task label 2026-08-10 07:58:33 +00:00
Author
Owner

Entschieden. ADR in docs/adr/0002-stat-und-modifier-pipeline.md, elf nummerierte Festlegungen.

Die Kernentscheidung — schärfer als das, was reinging

Vorgegeben waren drei Akkumulatoren (flat, points, mult) und vier Anwendungsregeln. Der eigentliche Fund ist, dass die vierte Regel gar kein Sonderfall ist: die Form gehört nicht zum Stat, sondern zum Modifikator. Der Stat trägt nur ein Bit — Direct oder Interval:

  • Direct: (base + flat) × (1 + clamp(points)) × mult — der Wert wächst
  • Interval: (base + flat) × (1 − clamp(points)) ÷ mult — der Wert schrumpft

Damit sind es 3 Akkumulatoren × 1 Bit statt drei Regeln plus einem Sonderfall. Kein Pfad, an dem später eine fünfte Regel entsteht.

Der Clamp greift auf die Summe, genau einmal, nach der Summierung — nie pro Quelle. Folge, die ausdrücklich gewollt ist: keine abnehmenden Erträge, sondern eine harte Wand. Drei Quellen à 30 pp Cooldown-Reduktion bei cap = 0,60 ergeben 60 pp, die restlichen 30 pp verfallen ersatzlos. Die ungeclampte Summe bleibt für die Anzeige erhalten, damit der Spieler sieht, dass er gegen die Wand läuft.

Drei Präzisierungen über die Vorgaben hinaus

1. Attack Speed war eine Falle. „Attack Speed ×1,25" ist genrenüblich eine Rate — also genau das, was die Intervall-Entscheidung verbietet. Statt einer fünften Regel löst der Interval-Modus es: mult teilt das Intervall, statt es zu multiplizieren. Der Stat heißt deshalb AttackInterval, die Anzeige bleibt „×1,25".

2. „Nur Quellen persistieren" hatte ein Loch. Die Regel trägt nicht, wenn eine run-gebundene Quelle als Resource liegt — sie überlebt den Run und bringt genau die Bug-Klasse zurück, die der Component-Ansatz verhindern sollte. Die Ablageregel ist deshalb Teil der Entscheidung geworden: Lebensdauer bestimmt Resource vs. Component. Run-gebunden hängt an der Turm-Entity (auch RunUpgrades und EffectiveStats), permanent ist Resource. Das beantwortet zugleich die Herkunftsfrage — Herkunft steckt im Ablageort, nicht im Datenmodell, und Akkumulatoren werden immer vollständig neu gerechnet, nie subtrahiert.

3. Zwei Stats passen in keine Form — als Befund festgehalten, nicht als Fehler. SlowAura modifiziert Gegner statt den Turm und ist als Stat ein Fremdkörper, als Fähigkeits-Component dagegen naheliegend. Und MaxHealth ist der einzige Stat mit einem veränderlichen Ist-Wert daneben: steigt das Maximum mitten im Run, muss entschieden werden, was mit der aktuellen HP passiert. Beides steht unter „Offene Punkte", damit es nicht zwischen den Tickets verschwindet.

Entfernungspfad — der Bug, der sonst in drei Wochen zuschlägt

Ein einziges System sync_tower_abilities ist der einzige Schreiber von Fähigkeits-Components und gleicht Soll gegen Ist ab. Kein insert/remove an der Ereignisstelle. Dadurch:

  1. Card-Wechsel im Run — EquippedCards ändert sich, Abgleich läuft, remove::<T>(). Der einzige Fall, in dem überhaupt entfernt wird.
  2. Run-Ende — die Turm-Entity wird despawnt, Components verschwinden mit ihr. Kein Aufräum-Code.
  3. Save laden — der Save enthält nur Quellen, der Turm wird frisch gespawnt, Abgleich einmal vor dem ersten Gameplay-Frame.

Eine übriggebliebene Component ist damit strukturell unmöglich, nicht nur unwahrscheinlich. Dazu ein debug_assert am Ende des Abgleichs. Bevy-Lifecycle-Hooks und RemovedComponents bewusst nicht — ein Abgleich, der das Soll kennt, braucht keine Benachrichtigung.

Rechenbeispiel (Zahlen als Platzhalter markiert)

  • Damage: Basis 10 → flat +14,40 → 24,40 → mult 2,40 × 1,30 = 3,12 → 76,13
  • Health Regen: (0 + 4,00) × 1,60 = 6,40 HP/s. Ohne den flachen Akkumulator wäre es 0 × 1,60 = 0 — die Karte wirkungslos. Das ist der Grund, warum es drei Akkumulatoren sind und nicht zwei.
  • Ultimate-Cooldown: 60 s, −8 pp + −30 pp = 38 pp → 37,2 s. Mit dritter Quelle −30 pp: Summe 68 pp, Clamp 0,60 → 24,0 s, 8 pp verfallen.

type Stat = f32 mit Revisions-Auslöser bei 10⁶ oder sichtbarer Rundung.

Damit sind #7 und #12 frei.

**Entschieden.** ADR in `docs/adr/0002-stat-und-modifier-pipeline.md`, elf nummerierte Festlegungen. ## Die Kernentscheidung — schärfer als das, was reinging Vorgegeben waren drei Akkumulatoren (`flat`, `points`, `mult`) und vier Anwendungsregeln. Der eigentliche Fund ist, dass die vierte Regel gar kein Sonderfall ist: **die Form gehört nicht zum Stat, sondern zum Modifikator.** Der Stat trägt nur **ein Bit** — `Direct` oder `Interval`: - `Direct`: `(base + flat) × (1 + clamp(points)) × mult` — der Wert wächst - `Interval`: `(base + flat) × (1 − clamp(points)) ÷ mult` — der Wert schrumpft Damit sind es 3 Akkumulatoren × 1 Bit statt drei Regeln plus einem Sonderfall. Kein Pfad, an dem später eine fünfte Regel entsteht. **Der Clamp greift auf die Summe, genau einmal, nach der Summierung** — nie pro Quelle. Folge, die ausdrücklich gewollt ist: keine abnehmenden Erträge, sondern eine harte Wand. Drei Quellen à 30 pp Cooldown-Reduktion bei `cap = 0,60` ergeben 60 pp, die restlichen 30 pp verfallen ersatzlos. Die ungeclampte Summe bleibt für die Anzeige erhalten, damit der Spieler sieht, dass er gegen die Wand läuft. ## Drei Präzisierungen über die Vorgaben hinaus **1. Attack Speed war eine Falle.** „Attack Speed ×1,25" ist genrenüblich eine Rate — also genau das, was die Intervall-Entscheidung verbietet. Statt einer fünften Regel löst der `Interval`-Modus es: `mult` **teilt** das Intervall, statt es zu multiplizieren. Der Stat heißt deshalb `AttackInterval`, die Anzeige bleibt „×1,25". **2. „Nur Quellen persistieren" hatte ein Loch.** Die Regel trägt nicht, wenn eine run-gebundene Quelle als `Resource` liegt — sie überlebt den Run und bringt genau die Bug-Klasse zurück, die der Component-Ansatz verhindern sollte. Die **Ablageregel** ist deshalb Teil der Entscheidung geworden: *Lebensdauer bestimmt `Resource` vs. `Component`.* Run-gebunden hängt an der Turm-Entity (auch `RunUpgrades` und `EffectiveStats`), permanent ist `Resource`. Das beantwortet zugleich die Herkunftsfrage — Herkunft steckt im Ablageort, nicht im Datenmodell, und Akkumulatoren werden immer vollständig neu gerechnet, nie subtrahiert. **3. Zwei Stats passen in keine Form** — als Befund festgehalten, nicht als Fehler. `SlowAura` modifiziert Gegner statt den Turm und ist als Stat ein Fremdkörper, als Fähigkeits-Component dagegen naheliegend. Und `MaxHealth` ist der einzige Stat mit einem veränderlichen Ist-Wert daneben: steigt das Maximum mitten im Run, muss entschieden werden, was mit der aktuellen HP passiert. Beides steht unter „Offene Punkte", damit es nicht zwischen den Tickets verschwindet. ## Entfernungspfad — der Bug, der sonst in drei Wochen zuschlägt Ein einziges System `sync_tower_abilities` ist der **einzige Schreiber** von Fähigkeits-Components und gleicht Soll gegen Ist ab. Kein `insert`/`remove` an der Ereignisstelle. Dadurch: 1. **Card-Wechsel im Run** — `EquippedCards` ändert sich, Abgleich läuft, `remove::<T>()`. Der einzige Fall, in dem überhaupt entfernt wird. 2. **Run-Ende** — die Turm-Entity wird despawnt, Components verschwinden mit ihr. Kein Aufräum-Code. 3. **Save laden** — der Save enthält nur Quellen, der Turm wird frisch gespawnt, Abgleich einmal vor dem ersten Gameplay-Frame. Eine übriggebliebene Component ist damit **strukturell unmöglich**, nicht nur unwahrscheinlich. Dazu ein `debug_assert` am Ende des Abgleichs. Bevy-Lifecycle-Hooks und `RemovedComponents` bewusst nicht — ein Abgleich, der das Soll kennt, braucht keine Benachrichtigung. ## Rechenbeispiel (Zahlen als Platzhalter markiert) - **Damage**: Basis 10 → `flat` +14,40 → 24,40 → `mult` 2,40 × 1,30 = 3,12 → **76,13** - **Health Regen**: `(0 + 4,00) × 1,60 = 6,40 HP/s`. Ohne den flachen Akkumulator wäre es `0 × 1,60 = 0` — die Karte wirkungslos. Das ist der Grund, warum es drei Akkumulatoren sind und nicht zwei. - **Ultimate-Cooldown**: 60 s, −8 pp + −30 pp = 38 pp → 37,2 s. Mit dritter Quelle −30 pp: Summe 68 pp, Clamp 0,60 → 24,0 s, 8 pp verfallen. `type Stat = f32` mit Revisions-Auslöser bei 10⁶ oder sichtbarer Rundung. Damit sind #7 und #12 frei.
Author
Owner

Nachtrag: der Entfernungspfad wird einfacher.

Nach dem Schreiben dieser ADR ist entschieden worden, dass Cards im laufenden Run über einen Pause-Button nur einsehbar, nicht wechselbar sind — das Loadout steht vor dem Run fest.

Damit entfällt Fall 1 des Entfernungspfads (Card-Wechsel im Run) vollständig. Er war der einzige Fall, in dem sync_tower_abilities überhaupt remove::<T>() aufruft. Übrig bleiben:

  • Run-Ende — die Turm-Entity wird despawnt, Components verschwinden mit ihr. Kein Aufräum-Code.
  • Save laden — der Turm wird frisch gespawnt, der Abgleich läuft einmal vor dem ersten Gameplay-Frame.

In beiden Fällen entsteht der Turm neu, statt umgebaut zu werden. Im laufenden Run wird nie eine Fähigkeits-Component entfernt. Die Architektur der ADR bleibt richtig und wird strikt einfacher — sync_tower_abilities ist weiterhin der einzige Schreiber, aber sein Abgleich läuft praktisch nur noch beim Spawn.

Die ADR selbst muss dafür nicht geändert werden: sie beschreibt einen Abgleich Soll-gegen-Ist, und ein Soll, das sich im Run nicht ändert, ist der triviale Sonderfall davon. Wenn Card-Wechsel im Run später doch kommen, ist der Pfad schon da — das ist genau die Eigenschaft, für die der Abgleich gewählt wurde.

**Nachtrag: der Entfernungspfad wird einfacher.** Nach dem Schreiben dieser ADR ist entschieden worden, dass Cards im laufenden Run über einen Pause-Button **nur einsehbar, nicht wechselbar** sind — das Loadout steht vor dem Run fest. Damit entfällt Fall 1 des Entfernungspfads (*Card-Wechsel im Run*) vollständig. Er war der **einzige** Fall, in dem `sync_tower_abilities` überhaupt `remove::<T>()` aufruft. Übrig bleiben: - **Run-Ende** — die Turm-Entity wird despawnt, Components verschwinden mit ihr. Kein Aufräum-Code. - **Save laden** — der Turm wird frisch gespawnt, der Abgleich läuft einmal vor dem ersten Gameplay-Frame. In beiden Fällen entsteht der Turm neu, statt umgebaut zu werden. **Im laufenden Run wird nie eine Fähigkeits-Component entfernt.** Die Architektur der ADR bleibt richtig und wird strikt einfacher — `sync_tower_abilities` ist weiterhin der einzige Schreiber, aber sein Abgleich läuft praktisch nur noch beim Spawn. Die ADR selbst muss dafür nicht geändert werden: sie beschreibt einen Abgleich Soll-gegen-Ist, und ein Soll, das sich im Run nicht ändert, ist der triviale Sonderfall davon. Wenn Card-Wechsel im Run später doch kommen, ist der Pfad schon da — das ist genau die Eigenschaft, für die der Abgleich gewählt wurde.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RDietermann/InfiniTower#6