192 lines
8.6 KiB
Markdown
192 lines
8.6 KiB
Markdown
# 01 — Effekt-Vokabular
|
||
|
||
Type: grilling
|
||
Status: resolved
|
||
Blocked by: —
|
||
|
||
## Question
|
||
|
||
Was kann ein Effekt ausdrücken, und wie sieht er in TOML aus?
|
||
|
||
Das Effekt-Vokabular ist die Mod-API: ein Modder kann exakt das bauen, was hier definiert wird.
|
||
Zu klären:
|
||
|
||
- Welche Wirkungen gibt es? Stat-Modifikator (flach, prozentual), Sofortschaden/Heilung,
|
||
Ressourcenverbrauch, Statuseffekt anwenden/entfernen, Event auslösen.
|
||
- Zeitlichkeit: permanent (Rasse), solange ausgerüstet (Item), N Runden (Statuseffekt), einmalig (Trank).
|
||
- Wie werden Effekte gestapelt und in welcher Reihenfolge verrechnet? Erst flach, dann prozentual?
|
||
- Wie referenziert ein Effekt sein Ziel — Träger, Gegner, alle Gegner?
|
||
- Wie sieht die TOML-Repräsentation aus, und bleibt sie lesbar genug für Modder ohne Doku?
|
||
|
||
Ergebnis: eine Liste der Effekt-Varianten mit TOML-Beispiel je Variante.
|
||
|
||
## Answer
|
||
|
||
Begriffe sind im [Glossar](../../../CONTEXT.md) festgehalten.
|
||
|
||
### Zwei Begriffe, nicht einer
|
||
|
||
**Modifikator** rechnet ein Attribut um, solange er existiert — er wird nie ausgeführt, sondern beim
|
||
Auslesen aufsummiert. **Aktion** ändert Zustand einmalig. Getrennte Maschinen, getrennte TOML-Arrays.
|
||
Ein gemeinsamer Oberbegriff „Effekt" ist ausdrücklich *kein* kanonischer Begriff mehr.
|
||
|
||
### Attributkatalog (Engine-Vokabular, ohne Namensraum)
|
||
|
||
`strength` · `dexterity` · `constitution` · `charisma` · `luck` · `level` · `max_health` ·
|
||
`max_stamina` · `damage` · `hit_chance` · `critical_chance` · `critical_factor` ·
|
||
`resistance` (typ-indiziert) · `shop_price` · `carry_capacity`
|
||
|
||
`max_health` ist ein Attribut und wird von Modifikatoren umgerechnet. Der aktuelle Lebensstand ist
|
||
**kein** Attribut, sondern Zustand — den ändern Aktionen. Diese Trennung muss scharf bleiben.
|
||
|
||
### Modifikator
|
||
|
||
```toml
|
||
modifiers = [
|
||
{ attribute = "damage", flat = 5 },
|
||
{ attribute = "strength", flat = 2, percent = 5 },
|
||
{ attribute = "critical_chance", percent = 10, duration = "turns", turns = 3,
|
||
target = "all_enemies", symbol = "curse", name = "Verflucht" },
|
||
]
|
||
```
|
||
|
||
- `flat` und `percent` beide optional, mindestens eines Pflicht.
|
||
- `duration`: `permanent` · `while_equipped` · `turns` · `until_floor_end`.
|
||
- `target`: `self` · `target` · `all_enemies`. Der Modifikator wohnt **immer beim Träger** und wird
|
||
beim Auslesen des Ziels eingesammelt — stirbt der Träger, verschwindet die Aura von selbst.
|
||
- `symbol` und `name` nur bei befristeten Zuständen; Item-Boni erben beides vom Item.
|
||
`symbol` ist ein Schlüssel, keine Farbe — die Zuordnung macht `ui/theme.rs`.
|
||
|
||
### Aktion
|
||
|
||
```toml
|
||
actions = [
|
||
{ action = "damage", amount = 12, damage_type = "fire", target = "target" },
|
||
{ action = "change_attribute", attribute = "health", amount = 20, target = "self" },
|
||
{ action = "apply_modifier", modifier = "core:poisoned", target = "target" },
|
||
{ action = "remove_modifier", modifier = "core:poisoned", target = "self" },
|
||
{ action = "give_item", item = "core:gold", count = 50, target = "self" },
|
||
{ action = "remove_item", item = "core:torch", count = 1, target = "self" },
|
||
]
|
||
```
|
||
|
||
Sechs Aktionen. `damage` bleibt eigenständig statt `change_attribute health -12`, weil Schaden durch
|
||
eine Kette läuft (Resistenz mindert, Krit vervielfacht, Bedingungen greifen) und „bei erlittenem
|
||
Schaden" ein Event ist. Flache Liste, keine verschachtelten Aktionen — Ketten laufen über das
|
||
Event-System.
|
||
|
||
### Regeln
|
||
|
||
| | |
|
||
|---|---|
|
||
| Stapeln | `(Basis + Σ flach) × (1 + Σ prozentual)`, intern `f32`, gerundet an genau einer Stelle |
|
||
| Uhr | Ein Zug. Ein Feld gehen = ein Zug, eine Kampfaktion = ein Zug. Debuffs ticken auch beim Laufen |
|
||
| Schadenstypen | `fire` `ice` `poison` `shock` `holy` `slash` `blunt` — fest, Engine-Vokabular |
|
||
| Minderung | Ein Mechanismus: `resistance` je Typ. `armor` am Item ist Kurzschreibweise für Resistenz gegen `slash` und `blunt` |
|
||
| Namensraum | Doppelpunkt = Modder-Inhalt (`core:poisoned`). Ohne = Engine-Vokabular (`strength`, `fire`) |
|
||
| Tags | Freie namespaced Strings am Wesen, Grundlage für Bedingungen |
|
||
| Sprache | Schadenstyp `poison` ≠ Modifikator `core:poisoned` („Vergiftet") |
|
||
|
||
### Nachtrag aus Ticket 08: `start_quest`
|
||
|
||
Der Katalog hat **sieben** Aktionen, nicht sechs. `start_quest` kam mit
|
||
[Ticket 08](08-event-trigger-system.md) dazu, weil eine Quest sonst ab dem ersten Zug mitzählte und
|
||
Questketten beim Betreten des Kellers bereits erfüllt wären.
|
||
|
||
```toml
|
||
{ action = "start_quest", quest = "core:rattenplage", target = "self" }
|
||
```
|
||
|
||
Das hier angeforderte Bedingungsvokabular steht ebenfalls in 08, zusammen mit dem **Trigger** als
|
||
vierter Wirkungsform.
|
||
|
||
### Nachtrag aus Ticket 11: `symbol` trägt das Zeichen, nicht einen Schlüssel
|
||
|
||
Oben steht, `symbol` sei ein Schlüssel und die Zuordnung mache `ui/theme.rs`. Darin steckt eine Lücke:
|
||
**`ui/theme.rs` ist Code.** Ein Modder, der `meinmod:frostbite` anlegt, kann dort nichts eintragen —
|
||
sein Statuseffekt hätte weder Zeichen noch Farbe.
|
||
|
||
Stattdessen trägt die TOML beides selbst, die Farbe als Name aus fester Liste:
|
||
|
||
```toml
|
||
symbol = "†"
|
||
color = "cyan"
|
||
```
|
||
|
||
`data/` kennt nur `char` und ein Farbnamen-Enum, `ui/theme.rs` übersetzt in `ratatui::Color` — `game/`
|
||
und `data/` bleiben damit weiterhin ratatui-frei.
|
||
|
||
### Nachtrag aus Ticket 14: `damage` skaliert, und `flee` ist die achte Aktion
|
||
|
||
Die `damage`-Aktion oben kennt nur `amount = 12`, also einen festen Wert. Ein Waffenangriff soll aber
|
||
das `damage`-Attribut des Angreifers verwenden — das war gar nicht ausdrückbar. Die Menge ist deshalb
|
||
immer:
|
||
|
||
```
|
||
menge = amount + scale * damage-Attribut(Verursacher) # beide optional, Standard 0
|
||
```
|
||
|
||
Eine Formel für alles: Basisangriff `scale = 1.0` · Feuerball `amount = 20` · Wuchtschlag
|
||
`scale = 1.5` · Mischformen beides. Keine zwei Modi, kein Sonderfall für `core:attack`.
|
||
|
||
Dazu **`flee` als achte Aktion** — Ticket 11 hat Flucht zu einer Fähigkeit gemacht, und die braucht
|
||
eine Aktion, die sie ausführt.
|
||
|
||
### Nachtrag aus Ticket 13: `run_script`
|
||
|
||
`{ action = "run_script", script = "meinmod:feuerregen" }` als **neunte Aktion**. Ein Script ist damit
|
||
überall verwendbar, wo eine Aktion stehen darf, und braucht keine eigenen Platzierungsregeln. Es gibt
|
||
Aktionen zurück, die in die Warteschlange aus 08 wandern — der Schreibpfad bleibt also genau einer.
|
||
|
||
### Nachtrag aus Ticket 15: `give_exp`
|
||
|
||
`{ action = "give_exp", amount = 50, target = "self" }` als **zehnte Aktion**, für Questbelohnungen.
|
||
Erfahrung aus Tötungen läuft dagegen über das Feld `exp_reward` an der `creatures`-Registry — die
|
||
Kurzform für einen Todes-Trigger, den sonst jede Gegnerdefinition mitschleppen müsste.
|
||
|
||
Ticket 15 hat außerdem geprüft, ob ein Modifikator „je Level" skalieren können muss (`per_level`).
|
||
**Antwort: nein** — Charakteroptionen geben flache Boni, und was sie dauerhaft relevant hält, sind
|
||
Fähigkeiten. Das Vokabular bleibt hier unverändert.
|
||
|
||
### Der Katalog
|
||
|
||
`damage` · `change_attribute` · `apply_modifier` · `remove_modifier` · `give_item` · `remove_item` ·
|
||
`start_quest` (08) · `flee` (14) · `run_script` (13) · `give_exp` (15) — **zehn**.
|
||
|
||
### Nachtrag aus M3: benannte Modifikatoren brauchen eine Registry
|
||
|
||
Oben steht `{ action = "apply_modifier", modifier = "core:poisoned", … }` — eine Aktion referenziert
|
||
einen Modifikator **per ID**. Aufgefallen beim Bauen von M3: **Keine der zwölf Registries definiert
|
||
benannte Modifikatoren.** `core:poisoned` hatte kein Zuhause.
|
||
|
||
Das ist die **dreizehnte Registry**, `[[modifiers]]`:
|
||
|
||
```toml
|
||
[[modifiers]]
|
||
id = "poisoned"
|
||
name = "Vergiftet"
|
||
symbol = "☠"
|
||
turns = 4
|
||
attributes = [ { attribute = "damage", percent = -15 } ]
|
||
triggers = [ { on = "turn_started",
|
||
actions = [ { action = "damage", amount = 2, damage_type = "poison", target = "self" } ] } ]
|
||
```
|
||
|
||
Das innere Feld heißt bewusst `attributes` und nicht `modifiers` — ein Modifikator, der Modifikatoren
|
||
enthält, liest sich wie ein Fehler. Und `effects` wäre der Begriff, den dieses Ticket ausdrücklich
|
||
verworfen hat.
|
||
|
||
Ein Modifikator ist damit ein **Bündel**: mehrere Umrechnungen plus Trigger plus Dauer. Gift senkt den
|
||
Schaden *und* tickt — das wäre mit einer einzelnen Umrechnung nicht ausdrückbar.
|
||
|
||
### Bewusst nicht entschieden
|
||
|
||
Das **Bedingungsvokabular** für `condition`. Es kommt aus Ticket 08, damit Quests und Modifikatoren
|
||
dieselbe Grammatik benutzen statt zweier, die sich subtil unterscheiden. Ticket 01 fordert es nur an.
|
||
|
||
### Folgen für bestehenden Code
|
||
|
||
`ItemArmor.armor` fällt weg und wird ein Modifikator auf `resistance`. `Stats` trennt Attribute von
|
||
Zustand. Der Attributkatalog braucht abgeleitete Größen, die es heute gar nicht gibt (`hit_chance`,
|
||
`carry_capacity`).
|