docs: add ADR-0004 spatial grid, fix Fraction stat mode, extend spec
This commit is contained in:
@@ -4,6 +4,18 @@
|
||||
- **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`)
|
||||
- **Nachträge:**
|
||||
- **2026-08-10 — §3 um vier Utility-Stats ergänzt.** Auslöser: Widerspruch 5 der
|
||||
Vertical-Slice-Spec (`docs/spec-vertical-slice.md`) — der Utility-Tab des In-Run-Shops hatte mit
|
||||
`CoinGain` genau **einen** Stat, und die Spec musste entweder den Tab oder diese ADR für
|
||||
unvollständig erklären.
|
||||
- **2026-08-10 — §1 korrigiert: dritter Anwendungsmodus `Fraction`.** Auslöser: Beim Einordnen des
|
||||
neuen `FreeUpgradeChance` fiel auf, dass die `Direct`-Formel jeden Stat mit `base = 0` und
|
||||
`points` als einziger Quelle auf **null** rechnet. Betroffen waren `CritChance` und
|
||||
`DefensePercent` — **ein Defekt, kein Beobachtungsposten**: Crit-Chance hätte im Spiel nie über
|
||||
0 % steigen können. §1 und §2 sind neu gefasst, in §3 hat sich für diese beiden Zeilen die
|
||||
**Modus-Spalte** geändert (sonst nichts), und das durchgerechnete Beispiel hat einen
|
||||
Prozent-Stat bekommen, damit der Fall belegt ist.
|
||||
|
||||
## Kontext
|
||||
|
||||
@@ -33,6 +45,9 @@ Run-Grenze.
|
||||
|
||||
### 1. Drei Akkumulatoren, ein Anwendungsmodus pro Stat
|
||||
|
||||
> **Korrigiert am 2026-08-10.** Die erste Fassung kannte zwei Modi und rechnete die Prozent-Stats
|
||||
> auf null. Der Befund und die Korrektur stehen am Ende dieses Punktes.
|
||||
|
||||
Jeder Stat sammelt drei Akkumulatoren:
|
||||
|
||||
| Akkumulator | Einheit | Verknüpfung |
|
||||
@@ -41,26 +56,84 @@ Jeder Stat sammelt drei Akkumulatoren:
|
||||
| `points` | Prozentpunkte (0,15 = +15 pp) | additiv, **einmal am Ende** geclamped |
|
||||
| `mult` | Faktor | multiplikativ |
|
||||
|
||||
Jeder Stat trägt zusätzlich **ein Bit**, seinen Anwendungsmodus:
|
||||
Jeder Stat trägt zusätzlich **ein Feld**, seinen Anwendungsmodus. Es hat drei Varianten, und die
|
||||
Frage, die es beantwortet, ist immer dieselbe: **Was tun Prozentpunkte mit diesem Stat?**
|
||||
|
||||
- **`Direct`** — der Wert wächst mit dem Bonus:
|
||||
- **`Direct`** — der Stat ist eine **Größe**; `points` skalieren sie nach oben:
|
||||
`(base + flat) × (1 + clamp(points)) × mult`
|
||||
- **`Interval`** — der Wert ist eine Zeitspanne in Sekunden und **schrumpft** mit dem Bonus:
|
||||
- **`Interval`** — der Stat ist eine **Zeitspanne** in Sekunden; `points` skalieren sie nach unten:
|
||||
`(base + flat) × (1 − clamp(points)) ÷ mult`
|
||||
- **`Fraction`** — der Stat **ist selbst ein Anteil** (Chance, Prozentsatz); `points` skalieren
|
||||
nichts, sie **sind** der Wert:
|
||||
`clamp((base + flat + 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.
|
||||
Der Modus ist eine Eigenschaft des Stats, kein Sonderfall des Modifikators. Es bleibt bei **einem**
|
||||
Feld und drei Akkumulatoren; hinzugekommen ist eine dritte Variante desselben Feldes, nicht ein
|
||||
zweites Bit und kein vierter Akkumulator.
|
||||
|
||||
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.
|
||||
|
||||
Bei einem `Fraction`-Stat tragen `base`, `flat` und `points` **dieselbe Einheit** — Prozentpunkte —,
|
||||
weil der Wert des Stats aus nichts anderem besteht. Deshalb werden sie addiert und nicht
|
||||
gegeneinander skaliert, und deshalb greift der Clamp auf das Ergebnis: Bei einem `Fraction`-Stat
|
||||
*ist* das Ergebnis die Prozentpunkt-Summe. `mult` steht innerhalb des Clamps, damit die harte Wand
|
||||
aus Punkt 2 nicht durch einen Faktor umgangen werden kann.
|
||||
|
||||
#### Warum es einen dritten Modus gibt und keine billigere Lösung
|
||||
|
||||
Die erste Fassung dieser ADR kannte nur `Direct` und `Interval`. Damit rechneten `CritChance` und
|
||||
`DefensePercent` **auf null**: Beide haben `base = 0` und beziehen ihre Wirkung ausschließlich aus
|
||||
`points`, und `(0 + 0) × (1 + 0,30) × 1 = 0`. Drei Quellen mit je 10 pp Crit-Chance hätten
|
||||
zusammen 0 % ergeben. Es ist dieselbe Basis-null-Falle, wegen der es den `flat`-Akkumulator
|
||||
überhaupt gibt (siehe Punkt 3) — sie wurde dort für `mult` gezogen und für `points` übersehen,
|
||||
obwohl `(1 + points)` genauso ein Faktor auf die Basis ist.
|
||||
|
||||
Die Ursache ist nicht die Basis, sondern eine Doppelbedeutung: Bei `Damage` und `MaxHealth` ist der
|
||||
Stat eine Größe und Prozentpunkte vergrößern sie. Bei `CritChance` ist der Stat **selbst** ein
|
||||
Prozentsatz; die Prozentpunkte sind der Wert und skalieren nichts. Zwei verschiedene Dinge trugen
|
||||
denselben Akkumulatornamen und wurden mit derselben Formel bedient.
|
||||
|
||||
Drei billigere Wege wurden geprüft und verworfen:
|
||||
|
||||
1. **Prozent-Stats nehmen `flat` statt `points`.** Rechnet richtig (`(0 + 0,30) × 1 × 1 = 0,30`) und
|
||||
käme ohne dritten Modus aus — aber der Clamp hängt in Punkt 2 an `points`. Die Caps für Crit
|
||||
Chance und Defense % würden ersatzlos ausfallen, und sie sind der Grund, warum diese Stats
|
||||
überhaupt einen Cap-Eintrag haben.
|
||||
2. **Den Clamp generell auf das Ergebnis legen** statt auf die Prozentpunkt-Summe. Löst die
|
||||
Prozent-Stats, ändert aber die Bedeutung der Cooldown-Caps: Aus „höchstens 60 pp Reduktion" würde
|
||||
„mindestens 24 s Dauer". Der Cap-Wert wechselte die Einheit, würde von `base` abhängig, und eine
|
||||
Basisänderung im Balancing verschöbe still eine Obergrenze.
|
||||
3. **Eine Ausnahme je Zeile in der Stat-Tabelle.** Genau das, was der Modus verhindern soll — der
|
||||
Modus ist eine Eigenschaft des Stats, und eine Eigenschaft gehört in ein Feld, nicht in eine
|
||||
Fußnote.
|
||||
|
||||
Bleibt der dritte Modus. Er kostet eine `enum`-Variante, keinen Akkumulator und keine Regel, die
|
||||
irgendwo außerhalb dieser drei Formeln steht; der Compiler erzwingt bei jedem neuen Stat eine
|
||||
Entscheidung, welcher der drei es ist.
|
||||
|
||||
**Prüfregel für jede neue Zeile der Stat-Tabelle**, in dieser Reihenfolge:
|
||||
|
||||
1. Ist der Stat selbst ein Anteil oder eine Wahrscheinlichkeit → **`Fraction`**.
|
||||
2. Ist er eine Zeitspanne in Sekunden → **`Interval`**.
|
||||
3. Sonst → **`Direct`**. Dann muss `base + flat` an der nach Punkt 11.1 genannten Fortschritts-Stelle
|
||||
**größer als 0** sein — sonst ist der Stat entweder falsch eingeordnet oder er braucht eine
|
||||
`flat`-Quelle. Ein `Direct`-Stat mit `base = 0` und `points` als einziger Quelle ist genau der
|
||||
Fehler, den diese Korrektur behebt.
|
||||
|
||||
### 2. Clamp: summieren, dann einmal begrenzen
|
||||
|
||||
`clamp(points)` ist `points.clamp(0.0, cap)`. Das `cap` ist ein Balancing-Wert und steht in
|
||||
`clamp(x)` ist `x.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.
|
||||
|
||||
**Worauf `clamp` angewandt wird, sagt die Formel des Modus** — und es ist in allen drei Fällen
|
||||
dieselbe Größe: die **Prozentpunkt-Summe**. Bei `Direct` und `Interval` ist das der
|
||||
`points`-Akkumulator, weil nur er Prozentpunkte trägt. Bei `Fraction` ist es der ganze Wert, weil
|
||||
der Wert dort aus nichts anderem als Prozentpunkten besteht. Der Cap behält damit in jedem Fall die
|
||||
Einheit „Prozentpunkte" und bleibt vom `base` des Stats unabhängig.
|
||||
|
||||
Geclamped wird **die Summe, genau einmal, nach der Summierung** — nie pro Quelle. Folgen, die
|
||||
ausdrücklich gewollt sind:
|
||||
|
||||
@@ -83,8 +156,8 @@ bespielt werden — technisch stehen jedem Stat alle drei offen (die übrigen si
|
||||
| `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` |
|
||||
| `CritChance` | 0 | **Fraction** | `points` | `caps.crit_chance` |
|
||||
| `DefensePercent` | 0 | **Fraction** | `points` | `caps.defense_percent` |
|
||||
| `HealthRegen` | **0** | Direct | `flat` (Workshop), `mult` (Cards) | — |
|
||||
| `DefenseFlat` | 0 | Direct | `flat`, `mult` | — |
|
||||
| `UltimateCooldown` | > 0 | **Interval** | `points` | `caps.cooldown_reduction` |
|
||||
@@ -93,6 +166,84 @@ bespielt werden — technisch stehen jedem Stat alle drei offen (die übrigen si
|
||||
`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.
|
||||
|
||||
**Nachtrag 2026-08-10:** `CritChance` und `DefensePercent` standen in der ersten Fassung als
|
||||
`Direct` in dieser Tabelle und rechneten deshalb auf null — dieselbe Basis-null-Falle, hier nur über
|
||||
`points` statt über `mult`. Sie stehen jetzt als `Fraction`; `base`, „Erwartet" und `Clamp` sind
|
||||
unverändert, geändert hat sich allein die Modus-Spalte. Die Begründung steht in Punkt 1.
|
||||
|
||||
#### Nachweis: rechnet jede Zeile richtig?
|
||||
|
||||
Einmal komplett durchgegangen, mit den Formeln aus Punkt 1. „Richtig" heißt: Der Stat kann seinen
|
||||
erwarteten Quellen folgen, statt an einer Basis von 0 hängen zu bleiben.
|
||||
|
||||
| Stat | Modus | Prüfung | Richtig? |
|
||||
|---|---|---|---|
|
||||
| `Damage` | Direct | `base > 0`, `flat`+`mult` tragen | ja |
|
||||
| `AttackInterval` | Interval | `base > 0`, `mult` teilt das Intervall | ja |
|
||||
| `Range` | Direct | `base > 0`, `mult` trägt | ja |
|
||||
| `MaxHealth` | Direct | `base > 0`, `flat`+`mult` tragen | ja |
|
||||
| `CritFactor` | Direct | `base > 0`; ein Faktor wie 2,5× ist eine **Größe**, kein Anteil — `Direct` bleibt richtig | ja |
|
||||
| `CoinGain` | Direct | `base = 1,0 > 0`, `mult` trägt | ja |
|
||||
| `CritChance` | **Fraction** | war `Direct`: `(0+0)×(1+p) = 0`. Jetzt `clamp(0 + 0 + p)` | **korrigiert** |
|
||||
| `DefensePercent` | **Fraction** | war `Direct`: dasselbe. Jetzt `clamp(0 + 0 + p)` | **korrigiert** |
|
||||
| `HealthRegen` | Direct | `base = 0`, aber `flat` ist erwartete Quelle → `base + flat > 0`; Prüfregel 3 erfüllt | ja |
|
||||
| `DefenseFlat` | Direct | wie `HealthRegen` | ja |
|
||||
| `UltimateCooldown` | Interval | `base > 0`; `points` skalieren eine Dauer, also eine Größe | ja |
|
||||
| `WaveCooldown` | Interval | `base > 0`; wie `UltimateCooldown` | ja |
|
||||
| `CoinsPerWave` | Direct | `base = 0`, aber `flat` ist erwartete Quelle → wie `HealthRegen` | ja |
|
||||
| `CritCoinGain` | Direct | `base = 1,0 > 0`, `mult` trägt | ja |
|
||||
| `FreeUpgradeChance` | **Fraction** | eine Wahrscheinlichkeit → Prüfregel 1 | **korrigiert** |
|
||||
| `MetaGain` | Direct | `base = 1,0 > 0`, `mult` trägt | ja |
|
||||
|
||||
**`Interval` ist von der Korrektur nicht betroffen.** Beide `Interval`-Stats haben eine Basis über
|
||||
null, und ihre `points` skalieren eine Dauer — also eine Größe, nicht einen Anteil. Der inverse Term
|
||||
`(1 − clamp(points))` bleibt wörtlich stehen, ebenso ihre Caps und das durchgerechnete
|
||||
Ultimate-Cooldown-Beispiel weiter unten. Es gibt keinen Zeitspannen-Stat mit Basis 0; ein solcher
|
||||
wäre auch sinnlos, weil ein Intervall von 0 s keine Reduktion braucht.
|
||||
|
||||
#### Nachtrag 2026-08-10: vier Utility-Stats
|
||||
|
||||
> **Diese vier Zeilen sind später hinzugekommen als die zwölf oben.** Auslöser war Widerspruch 5 der
|
||||
> Vertical-Slice-Spec: Der Utility-Tab des In-Run-Shops stand mit `CoinGain` bei genau einer Zeile,
|
||||
> während Attack fünf und Defence vier hat. Ein Tab mit einer Zeile ist kein Tab. Die Kalibrierung
|
||||
> aus #3 sagt, was Utility im Referenzspiel ist — **kein Kampf-Stat, sondern ein
|
||||
> Ökonomie-Multiplikator** —, und `docs/WikiData.md` listet dort Cash Bonus, Cash/Wave, Coins/Kill
|
||||
> und Free Upgrades. Diese vier Zeilen übernehmen das Muster, nicht die Werte. An den zwölf Zeilen
|
||||
> oben ist nichts geändert.
|
||||
|
||||
| Stat | `base` | Modus | Erwartet | Clamp |
|
||||
|---|---|---|---|---|
|
||||
| `CoinsPerWave` | **0** | Direct | `flat` (Workshop, In-Run), `mult` (Cards) | — |
|
||||
| `CritCoinGain` | 1,0 | Direct | `mult` (Workshop, In-Run, Cards) | — |
|
||||
| `FreeUpgradeChance` | 0 | **Fraction** | `points` (Workshop, In-Run) | `caps.free_upgrade_chance` |
|
||||
| `MetaGain` | 1,0 | Direct | `mult` (Workshop, Cards) — **nicht** In-Run | — |
|
||||
|
||||
Damit hat die Tabelle **16** Stats. Drei der vier laufen im Modus `Direct`, `FreeUpgradeChance` als
|
||||
Wahrscheinlichkeit im Modus `Fraction`; keiner ist eine Zeitspanne, also braucht keiner `Interval`.
|
||||
|
||||
`CoinsPerWave` hat Basis **0** und steht damit in derselben Gruppe wie `HealthRegen` und
|
||||
`DefenseFlat`: Ein rein multiplikativer Bonus auf 0 bleibt 0, der flache Akkumulator ist der Grund,
|
||||
dass es die Zeile überhaupt gibt.
|
||||
|
||||
**`MetaGain` ist die einzige Zeile mit einer Quellen-Einschränkung, und die ist keine
|
||||
Balancing-Frage.** Ein aus `Coins` gekaufter Multiplikator auf die Meta-Währung wäre ein
|
||||
Konvertierungspfad zwischen den beiden Währungen — und Nicht-Konvertierbarkeit ist eine
|
||||
Kern-Entscheidung des Slice (Issue #1, #11). Die Einschränkung gehört deshalb in diese Tabelle und
|
||||
nicht in die UI: Wer `MetaGain` in den In-Run-Shop legt, bricht eine Entscheidung, nicht ein Layout.
|
||||
|
||||
**`CritCoinGain` erbt eine Vorbedingung.** Es wirkt nur, solange `CritChance > 0`, und `CritChance`
|
||||
hat Basis 0. Das ist gewollt — es koppelt den Utility-Tab an den Attack-Tab, damit die
|
||||
Kaufreihenfolge eine Entscheidung ist und keine Sortierung. Es heißt aber auch, dass die
|
||||
Sichtbarkeitsanforderung aus Punkt 11 hier zwingend greift: Die in Punkt 11.1 zu nennende
|
||||
Fortschritts-Stelle muss **hinter** dem ersten Crit-Kauf liegen, sonst ist der Modifikator an der
|
||||
genannten Stelle wirkungslos und nach Punkt 11.4 abzulehnen.
|
||||
|
||||
**`FreeUpgradeChance` ist als einziger der vier eine Wahrscheinlichkeit** und folgt deshalb der Form
|
||||
von `CritChance` und `DefensePercent`: Basis 0, `points`, ein eigenes `cap` in `Balance`, Modus
|
||||
**`Fraction`**. Beim Schreiben dieser Zeile ist aufgefallen, dass diese Form mit der damaligen
|
||||
`Direct`-Formel auf null rechnete — der Fund hat die Korrektur in Punkt 1 ausgelöst. Nach Prüfregel 1
|
||||
dort ist `Fraction` die richtige Einordnung, und der Cap greift auf die Prozentpunkt-Summe.
|
||||
|
||||
**`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
|
||||
@@ -259,6 +410,26 @@ danach.
|
||||
`(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.
|
||||
|
||||
**Crit Chance** (Modus **`Fraction`**, Basis **0**, `caps.crit_chance = 0,80`) — der Fall, an dem
|
||||
die erste Fassung dieser ADR gescheitert ist. Drei Quellen liefern Prozentpunkte:
|
||||
|
||||
| Schritt | Rechnung | Wert |
|
||||
|---|---|---|
|
||||
| Basis aus `Balance` | — | 0,00 |
|
||||
| Workshop Crit Chance Lv 20 zu +1 pp je Level | `points += 0,20` | 0,20 |
|
||||
| Card „Präzision" Lv 4 | `points += 0,12` | 0,32 |
|
||||
| In-Run-Upgrade Crit Chance | `points += 0,05` | 0,37 |
|
||||
| `clamp((0 + 0 + 0,37) × 1)` bei `cap = 0,80` | — | **0,37 → 37 % Crit-Chance** |
|
||||
|
||||
**Mit der alten `Direct`-Formel wäre dasselbe `(0 + 0) × (1 + 0,37) × 1 = 0` gewesen** — zwanzig
|
||||
Workshop-Level, eine Karte auf Stufe 4 und ein gekauftes In-Run-Upgrade zusammen wirkungslos, ohne
|
||||
dass ein Test oder der Compiler es angezeigt hätte.
|
||||
|
||||
Der Clamp greift unverändert: Kommt eine vierte Quelle mit +50 pp dazu, ist die Summe 0,87 und der
|
||||
geclampte Wert 0,80 → **80 %**. Die **ungeclampte** Summe 0,87 bleibt für die Anzeige erhalten
|
||||
(Punkt 2), und die vierte Quelle wäre nach der Sichtbarkeitsanforderung (Punkt 11.4) abzulehnen oder
|
||||
umzuskalieren, weil 7 der 50 pp ersatzlos verfallen.
|
||||
|
||||
**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 →
|
||||
@@ -337,3 +508,7 @@ Sichtbarkeitsanforderung ab.
|
||||
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.
|
||||
5. **Vierter Anwendungsmodus:** Ein Stat, der weder eine Größe noch eine Zeitspanne noch ein Anteil
|
||||
ist. Ein Fall reicht. Die Prüfregel in Punkt 1 ist so gebaut, dass sie ihn sichtbar macht statt
|
||||
ihn in `Direct` zu drängen — und genau dieses Hineindrängen war der Defekt, den der Nachtrag vom
|
||||
2026-08-10 behoben hat.
|
||||
|
||||
296
docs/adr/0004-spatial-grid-fuer-gegner-nachbarschaft.md
Normal file
296
docs/adr/0004-spatial-grid-fuer-gegner-nachbarschaft.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# ADR-0004: Spatial Grid für Gegner-Nachbarschaft und weiche Separation
|
||||
|
||||
- **Status:** Angenommen
|
||||
- **Datum:** 2026-08-10
|
||||
- **Betrifft:** Gegner-gegen-Gegner-Kollision, Zielauswahl in `tower_fire`, geteilter Reichweiten-Scan
|
||||
für das Auslöse-System, Projektil-Kollision
|
||||
- **Baut auf:** ADR-0001 (alle Zahlen aus `Res<Balance>`), ADR-0002 (Ablageregel `Resource` vs.
|
||||
`Component`; vollständige Neuberechnung statt Buchführung), Perf-Messungen aus #4,
|
||||
Cap-Analyse in #1, Auslöse-Entscheidung in #12
|
||||
|
||||
## Kontext
|
||||
|
||||
Die Gegner-gegen-Gegner-Kollision steht seit #4 als „offene Architekturfrage" im Raum — Stapeln
|
||||
zulassen oder Spatial Grid mit weicher Separation — und hat seitdem in keiner ADR und auf keiner
|
||||
Liste gestanden. Sie entscheidet, ob 10 000 Gegner spielbar statt nur lauffähig sind, und sie ist
|
||||
teuer nachzurüsten.
|
||||
|
||||
Der eigentliche Grund für eine eigene ADR ist aber nicht die Separation. **Drei Systeme brauchen
|
||||
dieselbe Nachbarschaftsabfrage und wollten sie bisher jedes für sich beantworten:**
|
||||
|
||||
| Abfrage | Radius | Aufrufe pro Frame |
|
||||
|---|---|---|
|
||||
| Weiche Separation: „welche Gegner sind mir zu nahe?" | Separationsradius (klein) | einmal **je Gegner** → bis 10 000 |
|
||||
| Zielauswahl in `tower_fire`: „welcher Gegner ist der nächste?" | Turm-`Range` (groß) | einmal (ein Turm) |
|
||||
| Geteilter Reichweiten-Scan für das Auslöse-System (#12) | Turm-`Range` (groß) | einmal (ein Turm) |
|
||||
|
||||
Dazu kommt die **Projektil-Kollision** mit einem kleinen Trefferradius und einem Aufruf je
|
||||
Projektil. #4 hat sie als einzige davon gemessen.
|
||||
|
||||
#12 hat den geteilten Reichweiten-Scan bereits entschieden — „der Scan wird einmal gemacht und
|
||||
geteilt, sonst läuft er siebenmal pro Frame gegen 10 000 Entities". Diese ADR erledigt den zweiten
|
||||
Teil derselben Beobachtung: **der eine geteilte Scan ist immer noch ein linearer Durchlauf über
|
||||
alle Gegner.** Ein Grid macht daraus eine Abfrage, an der vier Konsumenten hängen — es ist geteilte
|
||||
Infrastruktur, keine Optimierung eines einzelnen Systems.
|
||||
|
||||
### Die Messungen, gegen die hier gerechnet wird (#4, #1)
|
||||
|
||||
| Größe | Wert |
|
||||
|---|---|
|
||||
| Gesamte Gameplay-Logik bei 10 000 Gegnern | **0,114 ms** von 16,67 ms (0,7 %) |
|
||||
| `move_enemies`, teuerstes Logik-System | **~10 ns pro Gegner** → 0,100 ms bei 10 000 |
|
||||
| Population-Maintainer bei 50 000 Gegnern | 6,1 µs |
|
||||
| Kollisions-Paartest | 1,5–6,5 ns, exakt linear in n·m |
|
||||
| Naives O(n·m) trägt bis | **~5·10⁵ Paartests pro Frame** |
|
||||
| Bruch unter 60 fps | ~70 000 Sprites, Rendering dominiert mit 96–98 % |
|
||||
|
||||
`move_enemies` ist mit 0,100 ms von 0,114 ms fast die gesamte Logik. Das ist der Maßstab: Ein
|
||||
System, das einen Durchlauf über dieselbe Gegnermenge macht, kostet höchstens so viel wie dieses.
|
||||
|
||||
### Geprüft an der Bevy-0.19-Doku (docs.rs über context7, nicht aus dem Gedächtnis)
|
||||
|
||||
- Bevy 0.19 bringt **keine** Struktur für Nachbarschaftsabfragen im Gameplay mit.
|
||||
`bevy::math::bounding` liefert `Aabb2d`, `BoundingCircle`, `Bounded2d` und `IntersectsVolume` —
|
||||
also Volumen und Schnitttests für die **Narrow Phase**, keine Broad Phase. Wer eine
|
||||
Nachbarschaftsabfrage will, baut sie oder holt sie sich als Dependency.
|
||||
- `Query::par_iter` / `par_iter_mut` existieren; `par_iter` ist auf lesende Queries beschränkt.
|
||||
Zusammen mit `Local<Parallel<Vec<_>>>` wäre ein parallel gefüllter Rebuild möglich — er wird hier
|
||||
**nicht** gebaut, siehe Rechnung unten.
|
||||
- `Local<T>` und `Resource` überleben Systemläufe. Die Bucket-Vektoren des Grids werden über
|
||||
`clear()` wiederverwendet; es gibt keine Allokation pro Frame.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
### 1. Ein Uniform Grid als `Resource`, einmal pro Frame neu gebaut
|
||||
|
||||
`SpatialGrid` ist eine `Resource`: ein **dichtes Array fester Größe** über die Arena, je Zelle ein
|
||||
`Vec<Entity>`. Die Arena ist beschränkt (die Spawn-Peripherie muss laut Spec-Feature 1 sichtbar
|
||||
sein) und der Turm steht fest in der Mitte — es gibt keinen Grund für eine Hashmap. Der Zellindex
|
||||
ist zwei Divisionen und zwei `floor`. Gegner außerhalb des Gitters werden in die Randzelle geklemmt.
|
||||
|
||||
Das Grid ist **kein** `Component` an der Turm-Entity: Es beschreibt nicht den Turm, sondern die
|
||||
Welt, und es überlebt keinen Run-Wechsel als Zustand — es wird ohnehin jeden Frame neu gebaut.
|
||||
Damit bleibt die Ablageregel aus ADR-0002 §6 unberührt.
|
||||
|
||||
### 2. Zellgröße: der kleinste Radius, der **pro Gegner** abgefragt wird
|
||||
|
||||
Die Kosten einer Radiusabfrage sind `(⌈2r/c⌉+1)²` Zellen mal der mittleren Belegung. Beide
|
||||
Fehlerrichtungen kosten: `c ≪ r` erzeugt viele leere Zellenköpfe, `c ≫ r` erzeugt Kandidaten, die
|
||||
der Narrow-Phase-Test wieder verwirft.
|
||||
|
||||
**Die Zellgröße folgt dem Separationsradius, nicht der Turm-Reichweite.** Der Grund ist nicht der
|
||||
Radius, sondern die **Häufigkeit**: Die Separationsabfrage läuft einmal je Gegner, also bis zu
|
||||
10 000-mal pro Frame; der Reichweiten-Scan und die Zielauswahl laufen bei genau einem Turm **einmal**
|
||||
pro Frame. Eine Zellgröße, die für die einmalige große Abfrage gewählt wird, verteuert die
|
||||
10 000-fache kleine Abfrage; umgekehrt verteuert eine kleine Zelle die einmalige große Abfrage nur
|
||||
um Zellenköpfe, und die sind kein Narrow-Phase-Test.
|
||||
|
||||
Verbindlich:
|
||||
|
||||
```
|
||||
cell_size = ‹1,0› × max(Separationsradius, Trefferradius)
|
||||
```
|
||||
|
||||
Alle drei Werte sind ‹Platzhalter› und leben in `Balance` (ADR-0001 §4 — keine Schwelle als Literal
|
||||
im Gameplay-Code). Bei `cell_size = Separationsradius` berührt eine Separationsabfrage genau den
|
||||
3×3-Block um die eigene Zelle und nie mehr.
|
||||
|
||||
Die Turm-`Range` bestimmt die Zellgröße ausdrücklich **nicht**. Täte sie es, läge die gesamte in
|
||||
Reichweite befindliche Population in wenigen Zellen — und die Separationsabfrage wäre wieder O(n),
|
||||
also genau das, was das Grid entfernen soll. `Range` ist außerdem ein Stat, der über den Run wächst
|
||||
(ADR-0002 §3); eine Zellgröße, die einem wachsenden Stat folgt, wäre eine Struktur, die sich im
|
||||
laufenden Run umbaut.
|
||||
|
||||
### 3. Rebuild pro Frame, kein inkrementelles Update — gerechnet, nicht geschätzt
|
||||
|
||||
Der Rebuild ist ein Durchlauf über dieselbe Gegnermenge wie `move_enemies`, in derselben
|
||||
Speicherreihenfolge, mit **weniger** Arithmetik: `move_enemies` liest `Transform`, bildet die
|
||||
Richtung zur Mitte (Subtraktion plus Normalisierung, also eine Wurzel), skaliert und **schreibt**
|
||||
`Transform` zurück. Der Rebuild liest `Transform`, rechnet zwei Divisionen und zwei `floor` und
|
||||
schiebt eine `Entity` in einen `Vec`.
|
||||
|
||||
| | Rechnung | Ergebnis |
|
||||
|---|---|---|
|
||||
| Obergrenze Rebuild bei `HARD_ENEMY_CAP` | 10 000 × ≤10 ns | **≤0,10 ms** |
|
||||
| Anteil am Frame-Budget | 0,10 / 16,67 | **≤0,6 %** |
|
||||
| Logik gesamt danach | 0,114 + 0,10 | **≤0,22 ms = ≤1,3 %** |
|
||||
| Selbst am gemessenen Bruchpunkt | 70 000 × 10 ns | 0,7 ms = 4,2 % |
|
||||
|
||||
Die 10 ns sind eine großzügige Obergrenze, weil sie den teureren Nachbarn als Maßstab nimmt.
|
||||
|
||||
**Was ein inkrementelles Update dagegen einspart, ist nach oben durch dieselben ≤0,10 ms begrenzt —
|
||||
und es spart nicht einmal die, denn den Zellindex muss es genauso je Gegner ausrechnen, um einen
|
||||
Zellwechsel überhaupt zu erkennen.** Der Rest der Ersparnis ist der `Vec`-Push. Dagegen steht: ein
|
||||
Rückindex je Entity (in welcher Zelle und an welcher Position liegt sie?), ein Entfernen aus einem
|
||||
`Vec` in der Mitte, und ein Zustand, der zwischen Frames korrekt bleiben muss, während Gegner
|
||||
spawnen, sterben und bei radialer Konvergenz ständig Zellgrenzen überqueren.
|
||||
|
||||
Das ist dieselbe Abwägung, die ADR-0002 §6 für die Akkumulatoren getroffen hat, mit demselben
|
||||
Ergebnis: **vollständige Neuberechnung statt Buchführung über das Rückgängigmachen.** Buchführung
|
||||
schlägt fehl, und hier kauft sie unter 0,6 % Frame-Budget.
|
||||
|
||||
Aus demselben Grund wird der Rebuild **nicht parallelisiert**. `par_iter` plus
|
||||
`Local<Parallel<Vec<_>>>` und ein Merge-Schritt wären in Bevy 0.19 verfügbar; sie kaufen einen
|
||||
Bruchteil von 0,6 %.
|
||||
|
||||
Nebenrechnung zur nicht entschiedenen Spielgeschwindigkeit (#1, Spec „Lücken" Punkt 11): Nur die
|
||||
Simulation skaliert, nicht das Rendering. x4 ergibt ≤0,4 ms Rebuild, also ≤2,4 % des Budgets. Die
|
||||
Entscheidung hier präjudiziert den Multiplikator nicht.
|
||||
|
||||
### 4. Die Projektil-Kollision läuft über dasselbe Grid
|
||||
|
||||
#4 hat gemessen, dass naives O(n·m) bis ~5·10⁵ Paartests pro Frame trägt, und genau das empfohlen.
|
||||
Diese ADR kehrt das um, aus drei Gründen:
|
||||
|
||||
1. **Der gemessene Spielraum ist am Auslegungspunkt aufgebraucht.** 10 000 Gegner × ‹50›
|
||||
gleichzeitige Projektile sind 5·10⁵ Paartests — die Grenze, nicht ein Abstand zu ihr.
|
||||
2. **Beide Faktoren wachsen entlang der Schwierigkeitskurve, das Budget nicht.** `n` wächst mit
|
||||
`target_population(wave)`, `m` wächst, weil `AttackInterval` über Workshop, Cards und
|
||||
In-Run-Upgrades sinkt (ADR-0002 §3, Modus `Interval`). Das Produkt wächst also quadratisch gegen
|
||||
ein festes Budget. Ein Grenzwert, den man im ersten Run einhält und in Run 20 reißt, ist kein
|
||||
Grenzwert.
|
||||
3. **Das Grid steht ohnehin.** Die Abfrage kostet nur noch den 3×3-Block, weil der Trefferradius
|
||||
nach Punkt 2 in die Zellgröße eingegangen ist. Ein zweiter, anders strukturierter Durchlauf über
|
||||
die Gegnermenge wäre genau das Muster, das #12 als Bruchstelle benannt hat.
|
||||
|
||||
Der Narrow-Phase-Test bleibt, was er war: der gemessene 1,5–6,5-ns-Paartest, nur gegen ‹k›
|
||||
Kandidaten aus dem Block statt gegen `n`. `bevy::math::bounding` liefert dafür `BoundingCircle` und
|
||||
`IntersectsVolume`.
|
||||
|
||||
### 5. Weiche Separation: ein geklemmter Versatz, keine Kraft
|
||||
|
||||
Die Separation ist **keine Kollisionsauflösung**. Überlappung bleibt erlaubt; das Ziel ist, den Ring
|
||||
lesbar zu halten, nicht Durchdringung zu verhindern. Wer Durchdringung verbietet, baut den Stau, den
|
||||
die Cap-Analyse als „Stau am Turmring, den keine DPS-Menge auflöst" benannt hat.
|
||||
|
||||
Je Gegner, aus dem 3×3-Block:
|
||||
|
||||
1. **Nachbar-Budget:** höchstens ‹k› Nachbarn werden berücksichtigt. Das deckelt Kosten **und**
|
||||
Kraft in einem — dieselbe Regelform wie das „Ziel-Limit pro Auslösung" der Cap-Analyse, das die
|
||||
Spec in Feature 8 schon trägt. Eine Projektregel statt zweier.
|
||||
2. **Summe der Wegdrück-Vektoren**, je Nachbar normiert und mit dem Abstand abfallend gewichtet.
|
||||
3. **Klemmung:** Der resultierende Versatz wird auf ‹f› × (Speed × dt) mit ‹f› < 1 begrenzt und als
|
||||
Positionsversatz angewandt, nicht als Beschleunigung.
|
||||
|
||||
**Das Problem, das die Klemmung löst, sitzt am Turmring.** Radial auf einen Punkt konvergierende
|
||||
Körper erzeugen dort die höchste Dichte, die es im Spiel gibt. Die Nachbarschaft eines Gegners am
|
||||
Ring ist tangential ungefähr isotrop, radial aber nicht — hinter ihm, zum Turm hin, steht niemand,
|
||||
der zurückdrückt. Der Rest-Vektor zeigt deshalb **nach außen, vom Turm weg**. Ohne Klemmung ist
|
||||
das die Pulsier-Fehlerform: Der Ring drückt sich selbst auseinander, die Gegner laufen wieder heran,
|
||||
der Ring drückt sich wieder auseinander — und dazwischen fallen Gegner aus ihrer Angriffsreichweite.
|
||||
|
||||
Mit der Klemmung kann ein Gegner in einem Frame nie weiter versetzt werden, als er sich bewegt. Der
|
||||
radiale Anlauf wird also nie umgekehrt, ein Gegner verlässt seine Angriffsposition nicht, und Jitter
|
||||
braucht ein Überschwingen, das es konstruktiv nicht mehr gibt. Aus der Kraft wird eine begrenzte
|
||||
Ausdünnung.
|
||||
|
||||
‹k›, ‹f›, Separationsradius und die Abstandsgewichtung sind **‹Platzhalter›** in `Balance`. Sie
|
||||
findet nur Spielen, wie die vier freien Zahlen aus #8. Diese ADR erfindet keine.
|
||||
|
||||
### 6. Einordnung in den Frame
|
||||
|
||||
Verbindliche Reihenfolge, explizit festgelegt, nicht aus der Registrierungsreihenfolge geerbt:
|
||||
|
||||
1. `move_enemies` — schreibt `Transform`
|
||||
2. `rebuild_spatial_grid` — liest `Transform`, schreibt `SpatialGrid`
|
||||
3. alle Konsumenten, lesend auf `SpatialGrid`: weiche Separation, geteilter Reichweiten-Scan,
|
||||
`tower_fire`, Projektil-Kollision
|
||||
|
||||
**Verbindlich: Kein Konsument iteriert die Gegner-Query direkt.** Das ist die schärfere Fassung des
|
||||
Akzeptanzkriteriums, das die Spec für Feature 4 schon führt — aus „es gibt genau einen
|
||||
Reichweiten-Scan" wird „es gibt genau einen Durchlauf über die Gegnermenge, und das ist der
|
||||
Rebuild".
|
||||
|
||||
Eine Folge wird ausdrücklich in Kauf genommen: Die Separation schreibt `Transform`, nachdem das Grid
|
||||
gebaut wurde. Das Grid ist innerhalb desselben Frames also um genau einen Separationsversatz
|
||||
veraltet. Der ist nach Punkt 5 kleiner als ein Bewegungsschritt und damit kleiner als eine Zelle.
|
||||
Die Alternative — Separation vor dem Rebuild — verschiebt dieselbe Ungenauigkeit auf `tower_fire`
|
||||
und die Auslöse-Bedingungen, wo sie sichtbarer wäre.
|
||||
|
||||
## Verworfene Alternativen
|
||||
|
||||
- **Stapeln zulassen, keine Separation** (der Vorschlag, den die Spec unter „Lücken" Punkt 9
|
||||
festgehalten hat). Der billigste Weg, und für sich genommen vertretbar — aber er löst nur eines
|
||||
der vier Probleme. Die Zielauswahl in `tower_fire` bleibt O(n), obwohl #4 sie als nächsten
|
||||
Hotspot nach dem Rendering benannt hat, und der geteilte Reichweiten-Scan bleibt ein linearer
|
||||
Durchlauf. Das Grid trägt sich über drei Konsumenten, nicht über die Separation. Dazu kommt ein
|
||||
Design-Argument: Am Turmring liegen ohne Separation tausende Sprites deckungsgleich übereinander,
|
||||
und die Soll-Population — die Größe, auf der die gesamte Schwierigkeitskurve steht — ist dann
|
||||
nicht mehr abzulesen.
|
||||
- **Echte Kollision über eine Physik-Engine** (`bevy_rapier`, `avian`). Löst Durchdringung, also
|
||||
nicht das Problem; bringt einen Solver, einen Integrationsschritt und eine Dependency, deren
|
||||
Release-Takt an Bevy-Versionen hängt — dieselbe Einwendung, die ADR-0001 gegen
|
||||
`bevy_common_assets` erhoben hat. Bei tausenden radial konvergierenden Körpern produziert ein
|
||||
Solver genau den Stau, den die Cap-Analyse benennt.
|
||||
- **Eine fertige Spatial-Query-Crate** (etwa `bevy_spatial`). Spart den Rebuild-Code, der aus einer
|
||||
Schleife und einem Index besteht, und tauscht ihn gegen eine Dependency plus eine fremde
|
||||
Vorstellung davon, was eine Abfrage kostet. Ob eine 0.19-kompatible Fassung existiert, **wurde
|
||||
nicht geprüft** — die Entscheidung hängt nicht daran.
|
||||
- **Quadtree oder BVH.** Passen sich ungleicher Dichte an. Die Dichte hier ist aber bekannt und
|
||||
gerichtet — ein Attraktor in der Mitte, radiale Anläufe —, und ihr Rebuild ist kein linearer
|
||||
Durchlauf, sondern eine Sortierung mit Vergleichen. Die Rechnung in Punkt 3 gilt für sie nicht.
|
||||
- **Inkrementelles Update des Grids.** Siehe Punkt 3: Die Ersparnis ist nach oben durch ≤0,10 ms
|
||||
begrenzt, der Preis ist ein Rückindex je Entity und eine Bug-Klasse.
|
||||
- **Ein eigenes Grid je Konsument, jeweils mit passender Zellgröße.** Drei bis vier Rebuilds statt
|
||||
einem und drei bis vier Stellen, an denen ein veraltetes Grid entstehen kann. Der gemessene Preis
|
||||
**eines** Rebuilds ist das Argument dagegen.
|
||||
|
||||
## Konsequenzen
|
||||
|
||||
**Positiv**
|
||||
|
||||
- Vier Nachbarschaftsabfragen haben eine Quelle. Ein neuer Konsument (Death Wave, ein späterer
|
||||
Aura-Effekt) bekommt sie, ohne einen fünften Durchlauf über die Gegnermenge einzuführen.
|
||||
- Die Zielauswahl in `tower_fire` — laut #4 der nächste Hotspot nach dem Rendering — wird von einem
|
||||
linearen Scan zu einem Zellenlauf von innen nach außen, der beim ersten Treffer abbricht.
|
||||
- Der Rebuild hat keinen Zustand zwischen Frames. Es gibt kein veraltetes Grid, keinen Rückindex
|
||||
und keine Reparaturroutine.
|
||||
- Die Projektil-Kollision hört auf, entlang der Schwierigkeitskurve zu wachsen.
|
||||
|
||||
**Negativ**
|
||||
|
||||
- **Der erste unbedingte Kostenpunkt des Projekts.** Alles bisherige läuft bei Änderung
|
||||
(`EffectiveStats`, ADR-0002 §7) oder bei Ereignis. Der Rebuild läuft jeden Frame, auch wenn nichts
|
||||
passiert. ≤0,6 % ist bezahlbar, aber es ist eine neue Art von Kosten.
|
||||
- `SpatialGrid` ist eine `Resource`, die sich **jeden Frame ändert**. `is_changed()` auf ihr ist als
|
||||
Signal wertlos. Sie darf insbesondere nie eine Quelle für die `EffectiveStats`-Neuberechnung
|
||||
werden (ADR-0002 §7), sonst läuft die wieder jeden Frame und verliert genau den Beobachtungspunkt,
|
||||
für den sie gebaut wurde.
|
||||
- Eine Zellgröße für vier Radien ist ein Kompromiss. Die Reichweiten-Abfrage durchläuft einen Block,
|
||||
der mit dem `Range`-Stat wächst — siehe Revisions-Auslöser 2.
|
||||
- Die Separation ist innerhalb des Frames um einen Versatz veraltet (Punkt 6).
|
||||
- Die weiche Separation wird nach Gefühl getunt, nicht gegen einen Test. ‹k›, ‹f› und der
|
||||
Separationsradius sind so frei wie die vier Zahlen aus #8 — und der Beleg, dass sie stimmen, ist
|
||||
ein Blick auf den Turmring, keine Zahl.
|
||||
|
||||
## Offene Punkte, die diese ADR nicht entscheidet
|
||||
|
||||
- **Death Wave als vierter Konsument.** Der radiale Sweep fragt einen wachsenden Ring ab und muss
|
||||
Gegner „in der Reihenfolge ihres Abstands" treffen (Spec-Feature 8). Ein zellweiser Lauf von innen
|
||||
nach außen ist die naheliegende Form, und der gebatchte Massentod aus Spec-„Lücken" Punkt 10 wäre
|
||||
dann eine Zellordnung statt tausender Einzelereignisse. Das ist hier **nicht** entschieden — es
|
||||
gehört zu der ADR, die für Massenwirkung und Ziel-Limit noch fehlt.
|
||||
- **Arena-Maße und Zellenzahl.** Die Kantenlänge des Gitters folgt aus der Arena, und die steht
|
||||
nicht fest. Bis dahin ist offen, ob die Spawn-Peripherie im Gitter liegt oder in die Randzellen
|
||||
klemmt.
|
||||
- **`SlowAura` und andere Gegner-modifizierende Fähigkeiten** (ADR-0002 §9) sind naheliegende
|
||||
weitere Konsumenten. Sie fallen mit der Card-Liste.
|
||||
|
||||
## Revisions-Auslöser
|
||||
|
||||
Diese Entscheidung wird neu getroffen, sobald **eines** der folgenden Ereignisse eintritt:
|
||||
|
||||
1. **Der Rebuild ist nicht mehr der billigere Durchlauf.** Eine Messung mit dem Aufbau aus
|
||||
`examples/perf_spike.rs` bei `HARD_ENEMY_CAP` zeigt `rebuild_spatial_grid` teurer als
|
||||
`move_enemies` im selben Frame. Die Entscheidung gegen das inkrementelle Update steht und fällt
|
||||
mit dieser Annahme; ist sie widerlegt, wird sie neu getroffen — nicht vorher.
|
||||
2. **Radien-Spreizung.** Ein Konsument braucht einen Abfrageradius kleiner als eine halbe Zelle oder
|
||||
größer als ‹20› Zellkanten. Dann trägt **eine** Auflösung nicht mehr für alle Abfragen, und zwei
|
||||
Auflösungen oder eine hierarchische Struktur sind zu prüfen.
|
||||
3. **Die Separation frisst den radialen Fortschritt.** Ein Gegner braucht vom Spawn bis zum Turmring
|
||||
mehr als ‹1,2›× die aus `Speed` und Abstand erwartete Zeit. Dann ist die Klemmung zu schwach
|
||||
dosiert oder die Form der Separation falsch — beobachtbar in der Run-Telemetrie, nicht nur im
|
||||
Bild.
|
||||
4. **Das Bewegungsmodell ist nicht mehr rein radial.** Ausweichen, Umkreisen, Knockback oder ein
|
||||
zweiter Anziehungspunkt. Dann fällt die Dichteannahme „Maximum am Turmring", auf der Punkt 5
|
||||
steht, und die Zellgröße ist neu zu bestimmen.
|
||||
51
docs/agents/domain.md
Normal file
51
docs/agents/domain.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Domain Docs
|
||||
|
||||
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
||||
|
||||
## Before exploring, read these
|
||||
|
||||
- **`CONTEXT.md`** at the repo root, or
|
||||
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
|
||||
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
|
||||
|
||||
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
|
||||
|
||||
## File structure
|
||||
|
||||
This repo is **single-context**:
|
||||
|
||||
```
|
||||
/
|
||||
├── CONTEXT.md
|
||||
├── docs/adr/
|
||||
│ ├── 0001-....md
|
||||
│ └── 0002-....md
|
||||
└── src/
|
||||
```
|
||||
|
||||
Multi-context layout (only relevant if a `CONTEXT-MAP.md` ever appears at the root):
|
||||
|
||||
```
|
||||
/
|
||||
├── CONTEXT-MAP.md
|
||||
├── docs/adr/ ← system-wide decisions
|
||||
└── src/
|
||||
├── <context-a>/
|
||||
│ ├── CONTEXT.md
|
||||
│ └── docs/adr/ ← context-specific decisions
|
||||
└── <context-b>/
|
||||
├── CONTEXT.md
|
||||
└── docs/adr/
|
||||
```
|
||||
|
||||
## Use the glossary's vocabulary
|
||||
|
||||
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
|
||||
|
||||
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
||||
|
||||
## Flag ADR conflicts
|
||||
|
||||
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
||||
|
||||
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
|
||||
54
docs/agents/issue-tracker.md
Normal file
54
docs/agents/issue-tracker.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Issue tracker: Gitea
|
||||
|
||||
Issues und Specs für dieses Repo leben als **Gitea-Issues** auf `https://gitlab.hive-brain.de` (Repo `MDietermann/InfiniTower`).
|
||||
|
||||
> **Achtung, irreführender Hostname:** Die Domain heißt `gitlab.hive-brain.de`, dahinter läuft aber **Gitea**, kein GitLab. Nicht `glab` benutzen — die CLI hier ist [`tea`](https://gitea.com/gitea/tea).
|
||||
|
||||
## Setup (einmalig)
|
||||
|
||||
```bash
|
||||
tea login add --name hive --url https://gitlab.hive-brain.de --token <access-token>
|
||||
```
|
||||
|
||||
Token in Gitea unter *Settings → Applications → Generate New Token* mit Scope `issue` (bzw. `repo`) erzeugen. `tea` findet Login und Repo danach selbst, wenn es innerhalb des Clones läuft; sonst explizit `--login hive --repo MDietermann/InfiniTower` anhängen.
|
||||
|
||||
Binary: <https://dl.gitea.com/tea/> oder `go install code.gitea.io/tea@latest`.
|
||||
|
||||
**Mindestversion: 0.15.0** (`tea --version` prüfen). Ältere Builds kennen weder `--labels` beim Anlegen noch `issues edit` / `comment` / `issues close` — damit fällt die halbe Konvention unten aus.
|
||||
|
||||
## Konventionen
|
||||
|
||||
- **Issue anlegen**: `tea issues create --title "..." --description "..."`. Für mehrzeilige Beschreibungen ein Heredoc in `-d` einsetzen. Labels direkt mit `--labels a,b`, Assignees mit `--assignees <user>`.
|
||||
- **Issue lesen**: `tea issues <index>` (Alias `tea issue <index>`). Kommentare separat mit `tea comment list <index>`.
|
||||
- **Issues listen**: `tea issues list --state open --output json`. Filter: `--labels`, `--assignee`, `--milestones`, `--keyword`. Paginierung über `--limit` / `--page` (Default 30 — bei Vollständigkeitsbedarf hochsetzen).
|
||||
- **Kommentieren**: `tea comment add <index> --description "..."`.
|
||||
- **Labels setzen / entfernen**: `tea issues edit <index> --add-labels "..." --remove-labels "..."`. Vorhandene Labels: `tea labels list --output json`; neue anlegen mit `tea labels create --name ... --color ...`.
|
||||
- **Schließen**: `tea issues close <index>`. Nimmt keinen Abschlusskommentar entgegen — erst `tea comment add <index> --description "..."`, dann schließen.
|
||||
- **Pull Requests**: Gitea nennt sie wie GitHub „pull requests". `tea pulls list|create|view|merge|close`, Review-Kommandos `tea pulls approve|reject|review`.
|
||||
|
||||
Bei Unsicherheit über eine Flag: `tea <command> --help` ausführen statt raten.
|
||||
|
||||
## PRs als Request-Surface
|
||||
|
||||
**PRs as a request surface: no.** _(Auf `yes` setzen, wenn externe PRs in diesem Repo als Feature-Requests behandelt werden sollen; `/triage` liest dieses Flag.)_
|
||||
|
||||
Bei `yes` laufen PRs durch dieselben Labels und States wie Issues, über die `tea pulls`-Äquivalente. Gitea nummeriert Issues und PRs in **einem gemeinsamen Zähler** (wie GitHub, anders als GitLab) — `#42` ist also entweder ein Issue oder ein PR, nie beides.
|
||||
|
||||
## Wenn ein Skill sagt „publish to the issue tracker"
|
||||
|
||||
Ein Gitea-Issue anlegen (`tea issues create`).
|
||||
|
||||
## Wenn ein Skill sagt „fetch the relevant ticket"
|
||||
|
||||
`tea issues <index>` plus `tea comment list <index>`.
|
||||
|
||||
## Wayfinding-Operationen
|
||||
|
||||
Für `/wayfinder`. Die **Map** ist ein einzelnes Issue, die Tickets sind **Child**-Issues.
|
||||
|
||||
- **Map**: ein Issue mit Label `wayfinder:map`, das den Notes / Decisions-so-far / Fog-Body trägt. `tea issues create --labels wayfinder:map`.
|
||||
- **Child-Ticket**: ein Issue mit `Part of #<map>` als erster Zeile der Beschreibung und Label `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Beim Claim wird es dem treibenden Dev assigned.
|
||||
- **Blocking**: Gitea hat native Issue-Dependencies („Depends on"), aber `tea` hat dafür **kein** Subcommand. Deshalb hier per Konvention eine `Blocked by: #<n>, #<n>`-Zeile oben in der Beschreibung. Ein Ticket ist unblocked, wenn alle genannten Issues geschlossen sind.
|
||||
- **Frontier-Query**: `tea issues list --state open --output json`, auf die Children der Map einschränken, dann alles rauswerfen, das einen offenen Blocker in der `Blocked by`-Zeile oder einen Assignee hat; das erste in Map-Reihenfolge gewinnt.
|
||||
- **Claim**: `tea issues edit <n> --add-assignees <user>` — der erste Write der Session.
|
||||
- **Resolve**: `tea comment add <n> --description "<answer>"`, dann `tea issues close <n>`, dann einen Context-Pointer (Gist + Link) an die Decisions-so-far der Map anhängen.
|
||||
@@ -6,8 +6,9 @@
|
||||
wir erkennen, dass es fertig ist
|
||||
- **Baut auf:** [ADR-0001](adr/0001-balancing-daten-als-rust-konstanten.md) (Balancing-Daten),
|
||||
[ADR-0002](adr/0002-stat-und-modifier-pipeline.md) (Stat- und Modifier-Pipeline),
|
||||
[ADR-0003](adr/0003-save-format-ort-und-migration.md) (Save-Format), `CONTEXT.md` (Glossar und
|
||||
Slice-Scope), Karte in Issue #1
|
||||
[ADR-0003](adr/0003-save-format-ort-und-migration.md) (Save-Format),
|
||||
[ADR-0004](adr/0004-spatial-grid-fuer-gegner-nachbarschaft.md) (Spatial Grid und weiche
|
||||
Separation), `CONTEXT.md` (Glossar und Slice-Scope), Karte in Issue #1
|
||||
- **Vokabular:** verbindlich aus `CONTEXT.md`. Diese Spec führt keine neuen Begriffe ein, außer den
|
||||
beiden in Abschnitt „Zwei Grenzwerte, zwei Begriffe" ausgewiesenen.
|
||||
|
||||
@@ -71,10 +72,13 @@ Alles hier ist entschieden. Alles andere ist ‹Platzhalter› oder steht unter
|
||||
| Card-Slots | Start **1**, Wachstum auf **5**; die Slot-Zahl ist selbst Progression | #11 |
|
||||
| Card-Quelle | Drop aus Fortschritt (Boss-Kill, neuer Wellen-Rekord). Kein Kauf, keine dritte Währung | #11 |
|
||||
| Card-Randbedingungen | Slot 2 fällt **früh** (erste paar Runs); **mindestens zwei Epics** | #11 |
|
||||
| Slot-Quelle | **Kauf aus der Meta-Währung im Workshop**, exponentiell wachsende Kosten wie bei den Stat-Upgrades. Kein zweiter Freischalt-Mechanismus, keine Meilenstein-Schwelle | diese Spec (löst Widerspruch 2) |
|
||||
| Utility-Tab | **Ökonomie-Upgrades, keine Kampf-Stats** — vier Zeilen, siehe Feature 6 | #3-Kalibrierung, `docs/WikiData.md`; diese Spec (löst Widerspruch 5) |
|
||||
| Gegner-gegen-Gegner | **Spatial Grid mit weicher Separation**; dasselbe Grid trägt Zielauswahl, Reichweiten-Scan und Projektil-Kollision | ADR-0004 (löst Lücke 9) |
|
||||
| Ultimate | genau eine: **Death Wave**, radialer Sweep, automatisch ausgelöst | #9 |
|
||||
| Auslöse-Regel | **Cooldown UND ein Weltzustand**, je Fähigkeit fest verdrahtet, nicht einstellbar; feuern mehrere gleichzeitig, feuern alle | #12 |
|
||||
| Währungen | zwei: `Coins` (In-Run, verfällt) und die Meta-Währung (Workshop). Nicht ineinander konvertierbar | Issue #1, #11 |
|
||||
| Turm-Stats | 12, siehe Tabelle in ADR-0002 §3 | ADR-0002 |
|
||||
| Turm-Stats | 16, siehe Tabelle in ADR-0002 §3 samt Nachtrag vom 2026-08-10 | ADR-0002 |
|
||||
|
||||
**Prüfkriterium für die fertige Kurve** (#8, gilt für die vier freien Zahlen):
|
||||
|
||||
@@ -122,6 +126,14 @@ Der Spawner ist **cap-getrieben, nicht plan-getrieben**: er füllt bis zur `targ
|
||||
auf, statt N Gegner nach Zeitplan zu setzen (Cap-Analyse, Issue #1). Gegner laufen radial von außen
|
||||
auf den Turm zu und greifen ihn im Nahbereich an.
|
||||
|
||||
Gegner **stapeln nicht frei übereinander**: Sie drücken sich weich auseinander, nach
|
||||
[ADR-0004](adr/0004-spatial-grid-fuer-gegner-nachbarschaft.md). Das ist keine Kollisionsauflösung —
|
||||
Überlappung bleibt erlaubt, verboten ist nur, dass tausende Sprites am Turmring deckungsgleich
|
||||
liegen und die Soll-Population unlesbar machen. Der Versatz ist auf einen Bruchteil des
|
||||
Bewegungsschritts geklemmt und kann den radialen Anlauf deshalb nie umkehren. Die Nachbarschaft
|
||||
kommt aus dem `SpatialGrid`, das einmal pro Frame neu gebaut wird und außerdem Feature 4, das
|
||||
Auslöse-System und die Projektil-Kollision bedient.
|
||||
|
||||
**Akzeptanz**
|
||||
- Bei laufender Spawn-Phase pendelt die Zahl lebender Gegner um `target_population(wave)`, mit einer
|
||||
Abweichung von höchstens ‹±10 %› über ein 5-Sekunden-Fenster.
|
||||
@@ -129,6 +141,12 @@ auf den Turm zu und greifen ihn im Nahbereich an.
|
||||
Sprung-zu-Welle auf die höchste im Debug-Panel erreichbare Welle.
|
||||
- Bei 10 000 gleichzeitigen Gegnern bleibt die Frametime unter 16,67 ms (Nachmessung mit dem Aufbau
|
||||
aus `examples/perf_spike.rs`, nach dem Wechsel auf echte Sprites zu wiederholen).
|
||||
- Der Rebuild des `SpatialGrid` kostet bei 10 000 Gegnern **nicht mehr als `move_enemies` im selben
|
||||
Frame** (ADR-0004 §3 — die Entscheidung gegen ein inkrementelles Update steht auf dieser Annahme;
|
||||
reißt sie, ist die ADR fällig, nicht die Zahl).
|
||||
- Kein Gegner bewegt sich in einem Frame vom Turm **weg**, solange ihn keine Fähigkeit dorthin
|
||||
drückt — die Separationsklemmung macht das konstruktiv unmöglich, und der Turmring pulsiert
|
||||
sichtbar nicht.
|
||||
- Jeder Archetyp ist an Sprite-Farbe oder -Form ohne Text unterscheidbar.
|
||||
|
||||
### 4. Turm-Dauerfeuer und geteilter Reichweiten-Scan
|
||||
@@ -138,23 +156,37 @@ den Reichweiten-Zustand; `tower_fire` und **alle** Auslöse-Bedingungen (Feature
|
||||
Ergebnis. Das ist keine Optimierung für später, sondern die Bedingung, unter der die
|
||||
Auslöse-Entscheidung aus #12 tragfähig ist.
|
||||
|
||||
Der Reichweiten-Zustand und die Zielauswahl sind **Abfragen an das `SpatialGrid`**, kein linearer
|
||||
Durchlauf über die Gegner (ADR-0004). #12 hat die siebenfache Ausführung desselben Scans beseitigt;
|
||||
ADR-0004 beseitigt, dass der eine verbleibende Scan noch O(n) ist — die Zielauswahl läuft
|
||||
zellenweise von der Turmmitte nach außen und bricht beim ersten Treffer ab. Damit gilt eine
|
||||
schärfere Regel als bisher: **Genau ein System durchläuft die Gegnermenge, und das ist der
|
||||
Grid-Rebuild.**
|
||||
|
||||
**Akzeptanz**
|
||||
- Bei mindestens einem Gegner in Reichweite feuert der Turm ohne jede Eingabe innerhalb von
|
||||
`EffectiveStats.AttackInterval` Sekunden.
|
||||
- Ohne Gegner in Reichweite feuert er nicht.
|
||||
- Es existiert genau ein System, das die Gegnermenge in Turm-Reichweite bestimmt; ein `grep` nach
|
||||
einem zweiten Reichweiten-Scan über die Gegner-Query findet keinen.
|
||||
- Es existiert genau ein System, das die Gegnermenge in Turm-Reichweite bestimmt, und es fragt das
|
||||
`SpatialGrid`; ein `grep` nach einem zweiten Reichweiten-Scan über die Gegner-Query findet keinen.
|
||||
- **Außer `rebuild_spatial_grid` iteriert kein System die Gegner-Query, um Nachbarn zu finden** —
|
||||
weder die Separation, noch `tower_fire`, noch das Auslöse-System, noch die Projektil-Kollision
|
||||
(ADR-0004 §6).
|
||||
|
||||
### 5. Stat- und Modifier-Pipeline
|
||||
|
||||
Vollständig nach [ADR-0002](adr/0002-stat-und-modifier-pipeline.md): drei Akkumulatoren (`flat`,
|
||||
`points`, `mult`), ein `Direct`/`Interval`-Bit je Stat, Clamp einmal auf die Summe, `EffectiveStats`
|
||||
als `Component` an der Turm-Entity, Neuberechnung bei Änderung. `type Stat = f32`. Alle Zahlen aus
|
||||
`Res<Balance>` (ADR-0001).
|
||||
`points`, `mult`), ein Modus-Feld je Stat mit den drei Varianten `Direct` / `Interval` /
|
||||
`Fraction`, Clamp einmal auf die Prozentpunkt-Summe, `EffectiveStats` als `Component` an der
|
||||
Turm-Entity, Neuberechnung bei Änderung. `type Stat = f32`. Alle Zahlen aus `Res<Balance>`
|
||||
(ADR-0001).
|
||||
|
||||
**Akzeptanz**
|
||||
- Das durchgerechnete Beispiel aus ADR-0002 („Durchgerechnetes Beispiel") ist als Test hinterlegt
|
||||
und grün — mit den dort genannten Platzhalterwerten, nicht mit den späteren echten.
|
||||
und grün — mit den dort genannten Platzhalterwerten, nicht mit den späteren echten. Das schließt
|
||||
den **Crit-Chance-Fall** ein: Drei Quellen mit zusammen 37 pp ergeben 37 %, nicht 0.
|
||||
- Kein Stat im Modus `Direct` hat `base = 0` und `points` als einzige erwartete Quelle
|
||||
(ADR-0002 §1, Prüfregel 3) — prüfbar an der Stat-Tabelle, ohne das Spiel zu starten.
|
||||
- Nach einer Änderung an `Workshop`, `EquippedCards` oder `RunUpgrades` läuft die Neuberechnung
|
||||
genau einmal; ohne Änderung läuft sie nicht (nachweisbar über einen Zähler im Debug-Panel).
|
||||
- `sync_tower_abilities` ist der einzige Schreiber von Fähigkeits-Components; sein `debug_assert`
|
||||
@@ -171,6 +203,28 @@ kauft der Spieler In-Run-Upgrades in drei Tabs (Attack / Defence / Utility), jed
|
||||
(Buy x1). Alle verfallen am Run-Ende, weil sie als `RunUpgrades`-Component an der Turm-Entity hängen
|
||||
(ADR-0002 §6).
|
||||
|
||||
**Der Utility-Tab trägt Ökonomie-Upgrades, keine Kampf-Stats.** Das ist keine Verlegenheitslösung,
|
||||
sondern das, was Utility im Referenzspiel ist: ein Ökonomie-Multiplikator (Kalibrierungsbericht in
|
||||
#3; `docs/WikiData.md`, Workshop-Abschnitt, listet dort Cash Bonus, Cash/Wave, Coins/Kill Bonus und
|
||||
Free Upgrades). Vier Zeilen, jede mit ihrem Grund:
|
||||
|
||||
| Zeile | Stat | Warum sie da ist |
|
||||
|---|---|---|
|
||||
| Coin-Ertrag pro Kill | `CoinGain` | der Grundkanal — Einkommen skaliert linear mit Kills und damit mit der Soll-Population; ohne ihn hat der Tab keinen Anker |
|
||||
| Coins am Wellenende | `CoinsPerWave` | der einzige Ertrag, der **nicht** mit der Gegnerdichte wächst. Die Cap-Analyse (#1) verlangt, die Economy gegen *Kills pro Welle* zu normieren statt gegen Wellen — ein wellengebundener Sockel ist der Gegenpol dazu und trägt früh, wenn die Population noch klein ist |
|
||||
| Coin-Bonus auf Crit-Kills | `CritCoinGain` | koppelt Utility an Attack: Er ist wertlos, solange `CritChance` bei 0 steht, und wird stark, sobald der Attack-Tab bedient wurde. Genau das macht die Kaufreihenfolge zu einer Entscheidung statt zu einer Sortierung (#8, siehe Akzeptanz unten) |
|
||||
| Chance auf ein kostenloses Upgrade | `FreeUpgradeChance` | der einzige Utility-Effekt, der in die anderen Tabs zurückwirkt: Er kauft nicht Coins, sondern **Zeit**, und wirkt damit direkt auf die Tuning-Zielgröße „Zeit zwischen zwei bezahlbaren Käufen". Eine Auswertung je Wellenende, ein kostenloses Level in einem zufällig gewählten Tab — das Original hat drei getrennte Stats dafür, #3 empfiehlt für Utility ausdrücklich „stark vereinfachen" |
|
||||
|
||||
Die vier Stats stehen in ADR-0002 §3 (Nachtrag). **`MetaGain` gehört ausdrücklich nicht in diesen
|
||||
Tab**: Ein aus `Coins` gekaufter Multiplikator auf die Meta-Währung wäre ein Konvertierungspfad
|
||||
zwischen den beiden Währungen, und den schließt Issue #1 aus. `MetaGain` ist ein Workshop-Upgrade
|
||||
(Feature 10).
|
||||
|
||||
Damit stehen im Panel 5 Attack-, 4 Defence- und 4 Utility-Zeilen — **13 von den ~24 Zeilen, die das
|
||||
Layout hergibt.** Die Folgerung des HUD-Kommentars „der Spieler kann in einem Run nicht alles
|
||||
kaufen" trägt deshalb nicht mehr aus dem Layout; sie muss aus den Kosten kommen oder gar nicht
|
||||
(siehe „Widersprüche", Punkt 5).
|
||||
|
||||
**Akzeptanz**
|
||||
- Ein Kauf ist zu jedem Zeitpunkt eines laufenden Runs möglich, auch mitten in der Spawn-Phase, ohne
|
||||
dass die Simulation anhält.
|
||||
@@ -182,6 +236,10 @@ kauft der Spieler In-Run-Upgrades in drei Tabs (Attack / Defence / Utility), jed
|
||||
- Kosten und Wirkung unterscheiden sich zwischen den drei Tabs merklich genug, dass die
|
||||
Telemetrie über zehn Runs **nicht** dieselbe Kaufreihenfolge zeigt (#8: sonst ist die
|
||||
Kaufreihenfolge eine Sortierung, keine Entscheidung).
|
||||
- Der Utility-Tab enthält **keinen** Kauf, der die Meta-Währung, ihre Drop-Chance oder ihre Menge
|
||||
verändert — kein Pfad im Run wandelt `Coins` in Meta-Fortschritt.
|
||||
- Jede der vier Utility-Zeilen hat die Sichtbarkeitsanforderung aus ADR-0002 §11 durchlaufen; für
|
||||
`CritCoinGain` liegt die dort zu nennende Fortschritts-Stelle **hinter** dem ersten Crit-Kauf.
|
||||
|
||||
### 7. Auslöse-System für Fähigkeiten
|
||||
|
||||
@@ -197,7 +255,8 @@ Priorität.
|
||||
- Sind zwei Fähigkeiten im selben Frame auslösebereit, feuern beide im selben Frame.
|
||||
- Die Bedingungswerte stehen in `Balance` und sind über das Debug-Panel zur Laufzeit änderbar; keine
|
||||
Schwelle steht als Literal im Gameplay-Code (ADR-0001 §4).
|
||||
- Das System liest den Reichweiten-Zustand aus Feature 4 und führt keinen eigenen Scan aus.
|
||||
- Das System liest den Reichweiten-Zustand aus Feature 4 und führt keinen eigenen Scan aus — weder
|
||||
über die Gegner-Query noch als eigene Abfrage am `SpatialGrid` (ADR-0004 §6).
|
||||
|
||||
### 8. Ultimate: Death Wave
|
||||
|
||||
@@ -221,6 +280,13 @@ kaufbar und haben keine eigene Währung. Eine Card ist entweder ein Stat-Modifik
|
||||
**Fähigkeit** (`CardEffect::Modifier` / `CardEffect::Ability`, ADR-0002 §8) — Fähigkeits-Cards feuern
|
||||
über dasselbe System wie die Ultimate. Drei Seltenheiten, Slots von 1 auf 5.
|
||||
|
||||
**Slots werden im Workshop aus der Meta-Währung gekauft** (Feature 10) — sie stehen dort neben den
|
||||
Stat-Upgrades, mit derselben exponentiellen Kostenform. Es gibt keinen zweiten Freischalt-Weg: keine
|
||||
Wellen-Meilensteine, keine Boss-Zahl, keinen Drop. Cards selbst bleiben unverkäuflich und fallen
|
||||
weiter als Drop; gekauft wird nur die **Breite** des Loadouts. Damit ist die eigentliche
|
||||
Meta-Entscheidung des Slice ausgesprochen: Breite (ein Slot mehr) gegen Stärke (ein Stat-Level
|
||||
mehr), aus demselben Topf.
|
||||
|
||||
Das **Loadout steht vor dem Run fest**. Im laufenden Run sind Cards über den Pause-Button nur
|
||||
einsehbar, nicht wechselbar (Issue #1). Damit wird im laufenden Run nie eine Fähigkeits-Component
|
||||
entfernt (ADR-0002 §10, Nachtrag zu #6).
|
||||
@@ -233,14 +299,28 @@ entfernt (ADR-0002 §10, Nachtrag zu #6).
|
||||
eine abgelegte erzeugt keine.
|
||||
- Während eines Runs ändert sich `EquippedCards` nicht — kein Pfad in der UI erlaubt es.
|
||||
- Der Card-Pool enthält mindestens zwei Epics, und Slot 2 ist innerhalb der ersten ‹drei› Runs
|
||||
erreichbar (#11; beide Bedingungen sind an das Abnahmekriterium des Slice gebunden).
|
||||
**bezahlbar** (#11; beide Bedingungen sind an das Abnahmekriterium des Slice gebunden).
|
||||
- Es gibt genau einen Pfad, der `card_slots` erhöht, und der ist ein Workshop-Kauf; kein Boss-Kill,
|
||||
kein Wellen-Rekord und kein Drop verändert die Slot-Zahl.
|
||||
|
||||
### 10. Meta-Währung und Workshop
|
||||
|
||||
Die zweite Währung fällt aus denselben Kill-Events wie `Coins`, ist aber nicht in sie konvertierbar.
|
||||
Drop-Chance je Normalgegner, garantiert je Boss, Menge wellenabhängig. Gutschrift beim Tod des
|
||||
Gegners, kein Pickup. Der `Workshop` kauft davon permanente Stat-Upgrades mit exponentiell
|
||||
wachsenden Kosten.
|
||||
wachsenden Kosten. Unter den Stats liegt auch `MetaGain` — der Multiplikator auf die gedroppte
|
||||
Meta-Menge, der aus demselben Grund **nicht** im In-Run-Shop steht (Feature 6).
|
||||
|
||||
**Der Workshop verkauft außerdem Card-Slots.** Sie stehen als eigene Zeile neben den Stat-Upgrades,
|
||||
in derselben Währung, mit derselben exponentiellen Kostenform — vier Käufe insgesamt, von Slot 1 auf
|
||||
Slot 5. Der Spieler entscheidet damit selbst zwischen Breite und Stärke; das ist die eine
|
||||
Meta-Entscheidung, die der Slice zu zeigen hat. Ein zusätzliches persistiertes Feld braucht es
|
||||
nicht: `card_slots: u8` aus ADR-0003 §5 ist zugleich Zustand und Kaufstufe.
|
||||
|
||||
**Der erste Slot-Preis ist keine freie Zahl.** #11 bindet Slot 2 an das Abnahmekriterium — er muss
|
||||
innerhalb der ersten paar Runs fallen. Die Kosten für Slot 2 sind damit an den Meta-Ertrag der
|
||||
ersten ‹drei› Runs gebunden, nicht an die Kostenkurve der Stat-Upgrades; erst ab Slot 3 ist der
|
||||
Exponent frei. Beide Werte bleiben ‹Platzhalter› und leben in `Balance`.
|
||||
|
||||
**Akzeptanz**
|
||||
- Eine Ausgabe von `Coins` im Run verringert den Meta-Währungs-Stand nicht, und umgekehrt.
|
||||
@@ -249,6 +329,9 @@ wachsenden Kosten.
|
||||
- Ein Workshop-Kauf erhöht den `flat`- bzw. `mult`-Beitrag des betroffenen Stats sofort und
|
||||
überlebt ein Run-Ende sowie einen Programm-Neustart.
|
||||
- Über die ersten zehn Runs trägt jeder Run mindestens ein Workshop-Level ein (#8, Prüfkriterium 2).
|
||||
- Slot 2 ist im Workshop nach höchstens ‹drei› Runs bezahlbar, ohne dass der Spieler auf
|
||||
Stat-Upgrades verzichtet hat — die Prüfung ist ein gespielter Durchlauf, keine Rechnung.
|
||||
- Ein gekaufter Slot erhöht `card_slots` um genau 1 und überlebt einen Programm-Neustart.
|
||||
|
||||
### 11. Run-Ende und Meta-Übergang
|
||||
|
||||
@@ -270,8 +353,10 @@ ihr, ohne Aufräum-Code.
|
||||
### 12. In-Run-HUD und Pause
|
||||
|
||||
Links das Gameplay-Fenster, rechts ein vertikales Panel über die volle Höhe. Das Panel trägt oben
|
||||
den Namen des aktiven Tabs, darunter ~8 Zeilen im Schema `<Stat Name>` / `<Stat Value>` links und
|
||||
`Buy x1` / `<Cost>` rechts. Unten vier Tabs: **Attack Shop · Defence Shop · Utility Shop ·
|
||||
den Namen des aktiven Tabs, darunter bis zu ~8 Zeilen im Schema `<Stat Name>` / `<Stat Value>` links
|
||||
und `Buy x1` / `<Cost>` rechts. Belegt sind davon 5 (Attack), 4 (Defence) und 4 (Utility) — das
|
||||
Panel füllt seine Zeilen also **nicht** aus, und das ist eine Folge der Stat-Liste, kein
|
||||
Zwischenstand (Feature 6). Unten vier Tabs: **Attack Shop · Defence Shop · Utility Shop ·
|
||||
Ultimates**. Der Ultimates-Tab trägt die Cooldown-Anzeige (#12). Ein Pause-Button friert die
|
||||
**gesamte** Simulation ein und gibt Einsicht in die ausgerüsteten Cards.
|
||||
|
||||
@@ -294,9 +379,14 @@ Der Zustand zwischen zwei Runs. Er trägt Workshop-Käufe, das Card-Loadout, den
|
||||
den Start-Knopf für den nächsten Run. **Sein Aufbau ist offen** (siehe „Offen"); was hier steht, ist
|
||||
seine Funktion, nicht sein Layout.
|
||||
|
||||
Der Slot-Fortschritt ist dabei **kein eigener Bildschirmteil**: Slots werden im Workshop gekauft
|
||||
(Feature 10) und sind dort eine Zeile neben den Stat-Upgrades. Der Meta-Screen zeigt sie im
|
||||
Card-Bereich nur an, er verkauft sie nicht an zweiter Stelle.
|
||||
|
||||
**Akzeptanz**
|
||||
- Ein neuer Run ist nur von hier aus startbar.
|
||||
- Workshop-Käufe, Card-Loadout und Slot-Stand sind von hier aus erreichbar und änderbar.
|
||||
- Der Slot-Kauf ist genau einmal erreichbar, nämlich im Workshop.
|
||||
- Beim Betreten nach einem Run zeigt der Screen, was dieser Run eingebracht hat: erreichte Welle,
|
||||
verdiente Meta-Währung, gedroppte Cards.
|
||||
- Steht `version` nach dem Laden auf `0`, erscheint hier die Meldung aus ADR-0003 §8 — und bis zur
|
||||
@@ -402,7 +492,8 @@ Für keinen dieser Punkte wird hier eine Antwort erfunden.
|
||||
| **`MaxHealth` mitten im Run erhöht** — wächst die aktuelle HP mit, behält sie ihren Anteil, oder bleibt sie unverändert? | Das Health-System, nicht die Pipeline (ADR-0002, „Offene Punkte") | **Vor dem ersten Defence-Shop-Upgrade.** Die ADR-0002-Stat-Tabelle weist `MaxHealth` ausdrücklich `flat` aus In-Run-Upgrades zu — der Fall tritt bei jedem Health-Kauf ein, nicht theoretisch |
|
||||
| **`SlowAura`** — modifiziert Gegner statt den Turm und ist deshalb kein Stat | Fällt mit der Card-Liste; ADR-0002 §9 legt die Form fest (Component am Turm, das das Gegner-Bewegungssystem liest), nicht den Inhalt | Mit der Card-Liste |
|
||||
| **Zusammensetzung der Card-Liste**: Anzahl, Wirkungen, Verteilung auf die drei Seltenheiten, Drop-Wahrscheinlichkeiten je Stufe, Stufenzahl je Card | Ein Card-Inhalts-Ticket, gebunden an #8 (Zahlen) und #11 (Randbedingungen) | Vor Feature 9. **Zwei Randbedingungen sind bereits verbindlich**: Slot 2 muss früh fallen, und es braucht mindestens zwei Epics — sonst ist die Epic-Stufe eine Behauptung statt einer Mechanik |
|
||||
| **Slot-Kurve**: wodurch ein Card-Slot freigeschaltet wird | Niemand — siehe „Lücken", Punkt 2 | offen |
|
||||
| **Slot-Preise**: was ein Card-Slot kostet | Der Entwickler beim Tunen. Die **Mechanik** ist entschieden — Kauf aus der Meta-Währung im Workshop, siehe Feature 10 und „Widersprüche", Punkt 2. Offen ist nur noch der Preis, und der ist für Slot 2 **nicht frei**: er ist an den Meta-Ertrag der ersten ‹drei› Runs gebunden | Zusammen mit den vier freien Zahlen, nach Feature 15 |
|
||||
| **Zellgröße, Separationsradius, Nachbar-Budget ‹k› und Klemmfaktor ‹f›** | Der Entwickler beim Tunen; Form und Revisions-Auslöser stehen in ADR-0004 | Mit Feature 3. Der Beleg ist ein Blick auf den Turmring, keine Zahl |
|
||||
|
||||
---
|
||||
|
||||
@@ -411,6 +502,10 @@ Für keinen dieser Punkte wird hier eine Antwort erfunden.
|
||||
Beim Zusammentragen aller Entscheidungen sind Stellen aufgefallen, an denen zwei Quellen sich
|
||||
widersprechen oder an denen eine Frage niemandem gehört. Sie stehen hier, statt geglättet zu werden.
|
||||
|
||||
Aufgelöste Punkte werden **nicht gelöscht**, sondern als *entschieden, siehe X* markiert und
|
||||
behalten ihren Befund. Der Weg zu einer Entscheidung ist so lange wertvoll, wie jemand sie in Frage
|
||||
stellen könnte. Aufgelöst sind bisher die Punkte **2**, **5**, **9** und **14**.
|
||||
|
||||
### Widersprüche
|
||||
|
||||
1. **Card-Drops überqueren die Run-Grenze, aber ADR-0003 kennt sie nicht.** ADR-0003 §6 nennt
|
||||
@@ -423,14 +518,19 @@ widersprechen oder an denen eine Frage niemandem gehört. Sie stehen hier, statt
|
||||
zuerst die Ausnahmeliste in ADR-0003 §6 erweitern — nicht stillschweigend ein zweites System
|
||||
schreiben lassen.
|
||||
|
||||
2. **Die Slot-Kurve gehört niemandem, ist aber Abnahmebedingung.** #11 entscheidet „Start bei 1,
|
||||
wächst auf 5, die Slot-Zahl ist selbst Progression" und bindet Slot 2 ausdrücklich ans
|
||||
Abnahmekriterium des Slice. **Wodurch** ein Slot freigeschaltet wird, sagt niemand: nicht #11
|
||||
(schiebt es an #8/#10), nicht #8 (die vier freien Zahlen enthalten keine Slot-Kurve), nicht die
|
||||
Karte (der Fog nennt nur den Meta-Screen-Aufbau). `card_slots: u8` ist in ADR-0003 persistiert,
|
||||
aber es gibt keinen Mechanismus, der ihn erhöht. Das ist keine Zahl, die man beim Tunen findet —
|
||||
es ist eine unentschiedene Mechanik: Kauf aus Meta-Währung? Wellen-Meilenstein? Boss-Zahl? Sie
|
||||
blockiert das Abnahmekriterium und muss vor Feature 9 entschieden werden.
|
||||
2. **Die Slot-Kurve gehört niemandem, ist aber Abnahmebedingung.** — **Entschieden, siehe Feature 10
|
||||
und Feature 9.** *Der Befund bleibt stehen, weil er zeigt, wo die Lücke herkam.* #11 entscheidet
|
||||
„Start bei 1, wächst auf 5, die Slot-Zahl ist selbst Progression" und bindet Slot 2 ausdrücklich
|
||||
ans Abnahmekriterium des Slice. **Wodurch** ein Slot freigeschaltet wird, sagte niemand: nicht
|
||||
#11 (schiebt es an #8/#10), nicht #8 (die vier freien Zahlen enthalten keine Slot-Kurve), nicht
|
||||
die Karte (der Fog nennt nur den Meta-Screen-Aufbau). `card_slots: u8` ist in ADR-0003
|
||||
persistiert, aber es gab keinen Mechanismus, der ihn erhöht.
|
||||
**Auflösung:** Slots werden **aus der Meta-Währung im Workshop gekauft**, neben den
|
||||
Stat-Upgrades, mit derselben exponentiellen Kostenform. Kein zweiter Freischalt-Mechanismus,
|
||||
keine Meilenstein-Schwelle. Der Grund ist nicht Sparsamkeit, sondern dass daraus die
|
||||
Meta-Entscheidung des Slice wird: Breite gegen Stärke aus einem Topf. Die Randbedingung aus #11
|
||||
gilt unverändert weiter und macht den ersten Slot-Preis zu einer gebundenen, nicht zu einer
|
||||
freien Zahl — siehe „Offen". Was diese Auflösung neu aufwirft, steht unten als Punkt 16.
|
||||
|
||||
3. **Die Ticket-Überschrift von #8 verspricht 10–20 Minuten, die Entscheidung liefert 6–9.** #8
|
||||
heißt „Wellen- und Economy-Kurve für einen **10–20-Minuten-Run**", sein erster Kommentar
|
||||
@@ -449,9 +549,10 @@ widersprechen oder an denen eine Frage niemandem gehört. Sie stehen hier, statt
|
||||
Event-Cap (Boss). Die Regel bleibt richtig, ihr Anwendungsbereich ist ein Viertel so groß wie
|
||||
beschrieben — wer sie liest, baut sonst eine Klassifizierung für Klassen, die es nicht gibt.
|
||||
|
||||
5. **~24 Shop-Zeilen stehen gegen 12 Stats.** Der HUD-Kommentar in #10 leitet aus 8 Zeilen × 3 Tabs
|
||||
5. **~24 Shop-Zeilen stehen gegen 12 Stats.** — **Entschieden, siehe Feature 6 und den Nachtrag zu
|
||||
ADR-0002 §3.** *Der Befund bleibt stehen.* Der HUD-Kommentar in #10 leitet aus 8 Zeilen × 3 Tabs
|
||||
„etwa 24 In-Run-Upgrades" ab und macht daraus eine Design-Aussage („der Spieler kann in einem Run
|
||||
nicht alles kaufen"). ADR-0002 §3 kennt aber insgesamt **12** Stats, und davon gehören
|
||||
nicht alles kaufen"). ADR-0002 §3 kannte damals aber insgesamt **12** Stats, und davon gehören
|
||||
`UltimateCooldown` und `WaveCooldown` nicht in die drei Shop-Tabs. Die Aufteilung ergibt grob
|
||||
Attack 5 (`Damage`, `AttackInterval`, `Range`, `CritChance`, `CritFactor`), Defence 4
|
||||
(`MaxHealth`, `DefensePercent`, `DefenseFlat`, `HealthRegen`) und **Utility 1** (`CoinGain`). Ein
|
||||
@@ -460,8 +561,18 @@ widersprechen oder an denen eine Frage niemandem gehört. Sie stehen hier, statt
|
||||
die keine Stats aus ADR-0002 sind, und dann ist ADR-0002 §3 unvollständig. Beides ist eine
|
||||
Entscheidung, keine Detailfrage.
|
||||
Verschärfend: #3 empfiehlt für Utility ausdrücklich „stark vereinfachen", weil das Utility-System
|
||||
des Originals an Labs und Perks hängt — beides im Nebel. Der Utility-Tab ist damit der schwächste
|
||||
des Originals an Labs und Perks hängt — beides im Nebel. Der Utility-Tab war damit der schwächste
|
||||
Teil der HUD-Struktur.
|
||||
**Auflösung:** Der zweite Ast wird gewählt — ADR-0002 §3 **war** unvollständig und ist um vier
|
||||
Utility-Stats gewachsen (`CoinsPerWave`, `CritCoinGain`, `FreeUpgradeChance`, `MetaGain`; der
|
||||
Nachtrag ist in der ADR als solcher markiert). Utility ist damit das, was #3 im Referenzspiel
|
||||
gefunden hat: ein Ökonomie-Multiplikator, kein Kampf-Stat. Der erste Ast trägt trotzdem mit:
|
||||
Bei 5 + 4 + 4 = 13 belegten von ~24 möglichen Zeilen füllt das Panel seine Zeilen nicht aus, und
|
||||
**die Folgerung „der Spieler kann in einem Run nicht alles kaufen" folgt nicht mehr aus dem
|
||||
Layout.** Sie muss aus der Kostenkurve kommen (#8, Tuning-Zielgröße „Zeit zwischen zwei
|
||||
bezahlbaren Käufen") oder fallen. Punkt 6 unten hatte für dieselbe Kette schon festgehalten, dass
|
||||
von der HUD-Rechnung nur noch die Zahl 24 übrig ist — jetzt ist auch die keine Design-Aussage
|
||||
mehr, sondern eine Layout-Obergrenze.
|
||||
|
||||
6. **Die „12–17 Kaufmomente" sind zweimal überholt, stehen aber noch in der Kette.** Der
|
||||
HUD-Kommentar (13:38) rechnet mit 12–17 Kaufmomenten aus einer 20-Minuten-Session, #8 (13:51)
|
||||
@@ -491,14 +602,20 @@ widersprechen oder an denen eine Frage niemandem gehört. Sie stehen hier, statt
|
||||
**eine** Boss-Kill pro Run in der Frühphase der einzige garantierte Drop überhaupt — er trägt
|
||||
Meta-Währung und Card-Chance gleichzeitig.
|
||||
|
||||
9. **Gegner-gegen-Gegner-Kollision ist seit #4 offen und steht auf keiner Liste.** #4 und die
|
||||
Cap-Analyse nennen sie als „offene Architekturfrage": Stapeln zulassen oder Spatial Grid mit
|
||||
weicher Separation. Bei tausenden radial konvergierenden Körpern kommen O(n²) und ein Stau am
|
||||
Turmring zusammen, den keine DPS-Menge auflöst. Sie steht **nicht** im Fog der Karte, in keiner
|
||||
ADR und in keinem der offenen Punkte, die diese Spec zugewiesen bekommen hat. Sie entscheidet
|
||||
aber, ob 10 000 Gegner überhaupt spielbar sind, und sie ist teuer nachzurüsten.
|
||||
*Vorschlag zur Auflösung, nicht Entscheidung:* Stapeln zulassen — bei Programmer-Art in 2D
|
||||
vertretbar, und der Spatial Grid ist der teure Weg, der jederzeit nachrüstbar bleibt.
|
||||
9. **Gegner-gegen-Gegner-Kollision ist seit #4 offen und steht auf keiner Liste.** —
|
||||
**Entschieden, siehe [ADR-0004](adr/0004-spatial-grid-fuer-gegner-nachbarschaft.md).** *Der
|
||||
Befund bleibt stehen.* #4 und die Cap-Analyse nannten sie als „offene Architekturfrage": Stapeln
|
||||
zulassen oder Spatial Grid mit weicher Separation. Bei tausenden radial konvergierenden Körpern
|
||||
kommen O(n²) und ein Stau am Turmring zusammen, den keine DPS-Menge auflöst. Sie stand **nicht**
|
||||
im Fog der Karte, in keiner ADR und in keinem der offenen Punkte, die diese Spec zugewiesen
|
||||
bekommen hat.
|
||||
*Der Vorschlag dieser Spec war „Stapeln zulassen" — er ist verworfen worden.* Nicht wegen der
|
||||
Separation, sondern weil das Grid **drei weitere Konsumenten** hat, die bisher jeder für sich
|
||||
über die Gegnermenge laufen wollten: die Zielauswahl in `tower_fire` (laut #4 der nächste Hotspot
|
||||
nach dem Rendering), der geteilte Reichweiten-Scan aus #12 und die Projektil-Kollision. Genau der
|
||||
Scan, dessen Siebenfach-Ausführung #12 als Bruchstelle benannt hat, wird damit zu einer
|
||||
Grid-Abfrage. Der Rebuild ist gegen die Messungen aus #4 gerechnet, nicht geschätzt: ≤0,10 ms bei
|
||||
`HARD_ENEMY_CAP`, also ≤0,6 % des Frame-Budgets.
|
||||
|
||||
10. **Ziel-Limit für Flächeneffekte und gebatchter Massentod haben keine ADR.** Die Cap-Analyse
|
||||
ordnet beides ausdrücklich „der Architektur" zu, aber ADR-0002 regelt Stats und
|
||||
@@ -523,3 +640,49 @@ widersprechen oder an denen eine Frage niemandem gehört. Sie stehen hier, statt
|
||||
Slice hat nur das In-Run-HUD (entworfen) und den Meta-Screen (offen). Diese Spec hängt die
|
||||
Meldung an den Meta-Screen — womit der offene Meta-Screen-Entwurf eine Anforderung erbt, die
|
||||
aus einer ganz anderen Ecke kommt.
|
||||
|
||||
### Neu aufgefallen beim Einarbeiten der drei Entscheidungen (2026-08-10)
|
||||
|
||||
14. **Die `points`-Stats mit Basis 0 rechnen sich in ADR-0002 auf null.** — **Behoben, siehe
|
||||
ADR-0002 §1.** *Der Befund bleibt stehen, weil er ein ausgelieferter Defekt gewesen wäre, kein
|
||||
Beobachtungsposten.* Die `Direct`-Formel aus ADR-0002 §1 lautete
|
||||
`(base + flat) × (1 + clamp(points)) × mult`. Die Stat-Tabelle in §3 wies `CritChance` und
|
||||
`DefensePercent` aber `base = 0` **und** `points` als einzigen erwarteten Akkumulator zu.
|
||||
Eingesetzt: `(0 + 0) × (1 + 0,30) × 1 = 0`. Crit Chance hätte im Spiel **nie über 0 % steigen
|
||||
können**, egal wie viele Workshop-Level, Cards und In-Run-Upgrades zusammenkommen — und weder
|
||||
Compiler noch ein Formeltest hätten es angezeigt. Es ist exakt dieselbe Falle, die die ADR zwei
|
||||
Absätze weiter unten für `HealthRegen` und `DefenseFlat` benennt und begründet, nur dass sie dort
|
||||
für `mult` gezogen wurde und für `points` nicht. Aufgefallen ist sie beim Einordnen des neuen
|
||||
`FreeUpgradeChance`, der sich korrekt an die Form von `CritChance` gehalten und den Fehler
|
||||
deshalb geerbt hat.
|
||||
**Behebung:** ADR-0002 §1 hat einen dritten Anwendungsmodus **`Fraction`** bekommen —
|
||||
`clamp((base + flat + points) × mult)` — für Stats, die selbst ein Anteil sind. `CritChance`,
|
||||
`DefensePercent` und `FreeUpgradeChance` stehen jetzt dort; an ihren `base`-, „Erwartet"- und
|
||||
`Clamp`-Spalten hat sich nichts geändert. Es ist ein dritter Wert desselben Feldes, kein zweites
|
||||
Bit; drei billigere Wege sind in der ADR geprüft und mit Begründung verworfen. Eine Prüfregel und
|
||||
ein fünfter Revisions-Auslöser verhindern die Wiederkehr, das durchgerechnete Beispiel der ADR
|
||||
trägt den Fall jetzt als Rechnung, und `Interval` ist nicht betroffen.
|
||||
|
||||
15. **Nicht-Konvertierbarkeit ist bisher nur in eine Richtung ausgesprochen.** Issue #1 und #11
|
||||
begründen die zwei Währungen damit, „dass eine In-Run-Ausgabe keinen Meta-Fortschritt kostet" —
|
||||
also `Meta → Coins`. Die Gegenrichtung hat nie jemand benannt: Ein aus `Coins` gekauftes
|
||||
In-Run-Upgrade, das die Meta-Drop-Chance oder die Meta-Menge erhöht, wandelt `Coins` in
|
||||
Meta-Fortschritt und hebelt die Trennung genauso aus, nur unauffälliger. Der Fall wird real,
|
||||
sobald der Utility-Tab Ökonomie-Upgrades trägt (Feature 6) — er ist genau der Tab, in den so ein
|
||||
Upgrade thematisch gehören *würde*. Diese Spec zieht die Konsequenz oben ein (`MetaGain` ist
|
||||
Workshop-only, mit einer Akzeptanzzeile in Feature 6 und einer Zeilen-Anmerkung in ADR-0002 §3),
|
||||
aber die **Regel selbst** steht damit an einer Stat-Tabelle statt in der Entscheidung, aus der
|
||||
sie folgt. Wer die Zwei-Währungs-Entscheidung liest, findet die zweite Richtung dort nicht.
|
||||
|
||||
16. **Slots aus der Meta-Währung erzeugen genau die Konkurrenz, die #11 für Cards ausgeschlossen
|
||||
hat.** #11 verwirft eine gemeinsame Währung für Cards mit dem Argument: „beide Achsen
|
||||
konkurrieren dann linear um denselben Topf, und niemand kauft Cards, bis der Workshop ausgereizt
|
||||
ist — das Original verhindert das ausschließlich über Nicht-Fungibilität, nicht über Preise."
|
||||
Die Slot-Entscheidung (Punkt 2) tut fast das: Sie stellt die Card-Achse — nicht die Karten,
|
||||
aber ihre Breite — in dieselbe Kaufwarteschlange wie die Stat-Upgrades. Zwei Unterschiede halten
|
||||
sie tragfähig: Cards selbst fallen weiterhin gratis, die Achse ist also nie hinter dem Shop
|
||||
verschlossen; und Slot 2 muss laut #11 früh bezahlbar sein, was das Versagensmuster „niemand
|
||||
kauft, bis der Workshop ausgereizt ist" für den einen Kauf ausschließt, der im Abnahmekriterium
|
||||
steht. Für Slot 3 bis 5 gilt dieser Schutz **nicht** — dort ist das Argument aus #11 unverändert
|
||||
gültig, und ob die Slots 3–5 im Slice je gekauft werden, ist eine offene Frage an die
|
||||
Kostenkurve, keine an die Mechanik.
|
||||
|
||||
Reference in New Issue
Block a user