203 lines
10 KiB
Markdown
203 lines
10 KiB
Markdown
# 08 — Event- und Trigger-System
|
|
|
|
Type: grilling
|
|
Status: resolved
|
|
Blocked by: 07
|
|
|
|
## Question
|
|
|
|
Welche Ereignisse meldet das Spiel, und wie hängen Quests und Effekte daran?
|
|
|
|
Quests sind Bedingung plus Belohnung als Daten; Script-Mods hängen sich später an dieselben Events.
|
|
Zu klären:
|
|
|
|
- Welche Events gibt es, und welche Daten trägt jedes? `enemy_killed`, `item_picked_up`, `floor_entered`,
|
|
`npc_talked`, `turn_started`, `damage_taken`, `player_died`.
|
|
- Wie werden Events benannt und für Modder erweiterbar gemacht — feste Liste oder auch namespaced IDs?
|
|
- Bedingungen: reines Zählen (töte 5 Ratten) oder auch Zustandsabfragen (habe 100 Gold, Level ≥ 3)?
|
|
Braucht es Verknüpfung mit UND/ODER?
|
|
- Wie wird ein Questfortschritt gespeichert und wie überlebt er das Deaktivieren des Quest-Mods?
|
|
- Reihenfolge und Rekursion: Was passiert, wenn eine Belohnung selbst ein Event auslöst?
|
|
- Ist das synchrone Aufrufe (Handler direkt ausführen) oder eine Warteschlange?
|
|
|
|
Ergebnis: Eventliste mit Nutzlast, Bedingungs-Grammatik, TOML-Beispiel für eine Quest.
|
|
|
|
## Vorgaben aus anderen Tickets
|
|
|
|
- **01**: Das **Bedingungsvokabular** für `condition` wurde hierher delegiert, damit Quests und
|
|
Modifikatoren dieselbe Grammatik benutzen statt zweier, die sich subtil unterscheiden.
|
|
- **07**: `Game` hält `quests: Vec<QuestProgress>` — die Form von `QuestProgress` entscheidet dieses
|
|
Ticket. Wesen werden über `enum CreatureRef { Player, Enemy(usize) }` adressiert, Mutationen laufen
|
|
durch den einen Eingang `Game::apply(action, source, target, reg)`. Ein Event-Handler, der etwas
|
|
bewirkt, geht durch dieselbe Tür.
|
|
|
|
## Answer
|
|
|
|
Begriffe im [Glossar](../../../CONTEXT.md).
|
|
|
|
### Der Trigger ist die vierte Wirkungsform
|
|
|
|
```toml
|
|
{ on = "attack_hit", condition = [ … ], actions = [ … ] }
|
|
```
|
|
|
|
Modifikator rechnet um, Aktion wirkt einmal, Fähigkeit bietet an — der **Trigger sagt wann**. Er darf
|
|
überall stehen, wo heute `modifiers` und `actions` stehen: an Gegenständen, Affixen,
|
|
Charakteroptionen, Quests und später an Scripts.
|
|
|
|
Das kostet nichts: Ein Trigger ist eine Aktionsliste plus zwei Felder, und Ticket 01 hat Aktionen
|
|
bereits vollständig definiert. Die Alternative wäre nicht weniger Code, sondern dieselbe Maschine ein
|
|
zweites Mal — in Ticket 14, unter dem Namen `on_hit`. Ohne Trigger wäre „Flammendes Schwert: 20 %
|
|
Chance, Brennen auszulösen" schlicht nicht ausdrückbar.
|
|
|
|
### Bedingungen — eine Grammatik für alles
|
|
|
|
```toml
|
|
condition = [
|
|
{ check = "has_tag", who = "target", value = "core:undead" },
|
|
{ check = "attribute", who = "self", attribute = "level", at_least = 3 },
|
|
{ check = "has_item", who = "self", item = "core:key", count = 1, not = true },
|
|
]
|
|
```
|
|
|
|
Einheitliche Form, damit serde sie als getaggtes Enum liest. Die Liste ist implizit **UND**, jeder
|
|
Eintrag ist über `not` negierbar. **Kein ODER, kein Verschachteln.**
|
|
|
|
Ein rekursiver Ausdrucksbaum bräuchte eine Klammer-Syntax, die in TOML niemand freiwillig schreibt,
|
|
plus eine Auswertung mit Kurzschlusslogik. Die Decke wird offen benannt: „(untot ODER dämonisch) UND
|
|
Level ≥ 3" wird zu zwei Einträgen dupliziert — oder der Modder hängt beiden Wesen ein Tag
|
|
`core:unheilig` an, was ohnehin das billigere Mittel ist.
|
|
|
|
Bedingungen werden an **zwei** Stellen ausgewertet: beim Feuern eines Triggers und beim Auslesen eines
|
|
Attributs (bedingte Modifikatoren aus 01). Letzteres erzwingt den Nachtrag an Ticket 07, siehe unten.
|
|
|
|
### Neun Events, feste Liste
|
|
|
|
| Event | Nutzlast über `source`/`target` hinaus |
|
|
|---|---|
|
|
| `turn_started` | — |
|
|
| `attack_hit` | Waffe, Schadenstyp |
|
|
| `damage_taken` | Menge, Schadenstyp |
|
|
| `creature_died` | — |
|
|
| `item_picked_up` | `ItemInstance` |
|
|
| `item_used` | `ItemInstance` |
|
|
| `floor_entered` | Etagennummer |
|
|
| `npc_talked` | NPC-ID |
|
|
| `quest_completed` | Quest-ID |
|
|
|
|
Jedes Event trägt `source: CreatureRef` und `target: Option<CreatureRef>`. Kein eigenes `player_died`
|
|
— das ist `creature_died` mit `source = Player`.
|
|
|
|
`attack_hit` und `damage_taken` bleiben getrennt: Ersteres feuert beim Angreifer (dort hängt das
|
|
flammende Schwert), Letzteres beim Getroffenen (dort hängt der Dornenpanzer). Ein gemeinsames Event
|
|
zwänge jeden Trigger, erst zu prüfen, auf welcher Seite er steht.
|
|
|
|
**Auflösung: `self` ist der Träger des Triggers**, `target` die andere Seite des Events. Für einen
|
|
Gegenstand ist der Träger sein Besitzer, für eine Charakteroption das Wesen, für eine Quest der Held.
|
|
Damit ist eine Quest kein Sonderfall, sondern ein Trigger mit festem Träger.
|
|
|
|
**Keine Mod-Events für 1.0.** Die Liste ist Engine-Vokabular ohne Namensraum, wie Attribute und
|
|
Schadenstypen aus 01. Der Grund ist konkret: Mod-Stufen sind Daten → Typen → Scripting, und ohne
|
|
Stufe (c) kann niemand ein eigenes Event *auslösen* — ein namespaced Event wäre Vokabular ohne Sender.
|
|
Kommt Scripting (Ticket 13), wird `emit_event` eine gewöhnliche Aktion und die Liste bekommt eine
|
|
`Custom(String)`-Variante. Ein Zusatz, kein Umbau.
|
|
|
|
### Nachtrag aus Ticket 14: elf Events statt neun
|
|
|
|
`combat_started` und `combat_ended { outcome: Victory | Fled }` kommen dazu. Ohne sie ist „gewinne drei
|
|
Kämpfe" nicht formulierbar, und `combat_ended` **braucht** die Unterscheidung — sonst zählt Weglaufen
|
|
als Erfolg.
|
|
|
|
Beide mit `source = Player`, ohne `target`. Platzierung im Zugablauf aus 14: `combat_started` bei
|
|
Schritt 1 neben `turn_started`, `combat_ended` bei Schritt 5 direkt nach dem `retain` — dann ist die
|
|
Gegnerliste bereits leer und ein Trigger sieht den Endzustand statt einer halb aufgeräumten
|
|
Zwischenstufe.
|
|
|
|
Keine dritte Variante für den Tod des Helden: `creature_died` mit `source = Player` deckt ihn ab, und
|
|
danach gibt es ohnehin keinen Trigger mehr, der etwas davon hätte.
|
|
|
|
### Warteschlange statt Rekursion
|
|
|
|
Events landen in `Game.events: Vec<Event>` und werden nach der laufenden Aktion in einer Schleife
|
|
leergearbeitet.
|
|
|
|
Der Grund ist zur Hälfte ein Rust-Fakt: `Game::apply` hält bereits `&mut self`. Synchron zu feuern
|
|
hieße, über `self.quests` zu iterieren, während `self.apply` erneut aufgerufen wird — ein
|
|
Borrow-Fehler, kein Stilproblem. Die andere Hälfte ist die Rekursionsfrage aus dem Ticketkörper: Eine
|
|
Belohnung, die selbst ein Event auslöst, hängt es hinten an, statt den Stack zu vertiefen.
|
|
|
|
Gegen Zyklen — Trigger A löst B aus, B löst A aus — ein schlichter Zähler: **maximal 100 Events pro
|
|
Zug**, danach Abbruch mit Warnzeile. Kein Zyklenerkennungs-Graph; der Zähler fängt denselben Fall und
|
|
ist eine Zeile.
|
|
|
|
Trigger werden bei jedem Event **gesucht, nicht indiziert** — dieselbe Entscheidung wie „kein
|
|
Attribut-Cache" in 07 und aus demselben Grund: Ein Index müsste bei jedem An- und Ablegen, jedem Loot
|
|
und jedem Affix-Wechsel gepflegt werden, also genau das Invalidierungsproblem, das 07 vermieden hat.
|
|
Die Decke ist hier konkret — der 100-Events-Zähler mal dreißig Trigger mal zwanzig Gegner ist die
|
|
Grenze, ab der indiziert wird.
|
|
|
|
### Quests
|
|
|
|
Eine Quest **ist ein Trigger mit `count = N`** (Standard 1) und damit die **siebte Registry**:
|
|
|
|
```toml
|
|
[[quests]]
|
|
id = "core:rattenplage"
|
|
name = "Rattenplage"
|
|
on = "creature_died"
|
|
count = 5
|
|
condition = [ { check = "has_tag", who = "target", value = "core:rat" } ]
|
|
actions = [ { action = "give_item", item = "core:gold", count = 50, target = "self" } ]
|
|
```
|
|
|
|
Jedes Mal, wenn das Event feuert *und* die Bedingung hält, wird hochgezählt; bei `N` feuern die
|
|
`actions` als Belohnung. „Besitze 100 Gold" braucht keine zweite Mechanik, sondern fällt unter
|
|
dieselbe Form: `on = "item_picked_up"`, `condition = has_item core:gold ≥ 100`, `count = 1`.
|
|
|
|
`QuestProgress { quest: QuestId, count: u32, done: bool }`. Erledigte Quests bleiben mit `done = true`
|
|
in der Liste stehen statt entfernt zu werden — sonst kann eine Kette, die auf `quest_completed` hört,
|
|
nicht unterscheiden, ob eine Quest nie lief oder gerade fertig wurde, und ein wiederauflebender Zähler
|
|
schüttete die Belohnung erneut aus.
|
|
|
|
Ein `QuestProgress`, dessen ID nach dem Laden in keiner Registry steht, wird mit Warnzeile verworfen —
|
|
die Regel steht in der Charter, das Detail gehört Ticket 09.
|
|
|
|
### Nachtrag an Ticket 01: `start_quest` als siebte Aktion
|
|
|
|
Der Katalog aus 01 hatte sechs Aktionen. `start_quest` kommt dazu, und die Erweiterung steht hier
|
|
offen statt versteckt.
|
|
|
|
Alle Quests ab dem ersten Zug mitzählen zu lassen wäre lazier gewesen und macht das Spiel kaputt: Eine
|
|
Kette „bring mir 5 Rattenschwänze" → „jetzt geh in den Keller" wäre beim Betreten des Kellers bereits
|
|
erfüllt, bevor sie jemand erwähnt hat. Ein Handelsmod mit 40 Quests ließe 40 Zähler pro Event
|
|
mitlaufen.
|
|
|
|
Als Aktion fügt sich `start_quest` bruchlos ein — im NPC-Dialog, als Questbelohnung (das *ist* die
|
|
Kette), in `on_create` einer Charakteroption für eine Klassenquest. Ticket 01 hat den Katalog nicht
|
|
für heilig erklärt, es kannte Quests nur noch nicht.
|
|
|
|
### Nachtrag an Ticket 07: `against` in der Attribut-Signatur
|
|
|
|
Ticket 01 erlaubt Modifikatoren eine `condition` und nennt „+5 Schaden gegen Untote" als Motivation
|
|
für Tags. Ticket 07 hat den Lesezugang als `Game::attribute(who, attr, reg)` festgelegt — **darin
|
|
steckt kein Gegner**, die Bedingung ist also nicht auswertbar. Neue Signatur:
|
|
|
|
```rust
|
|
Game::attribute(&self, who: CreatureRef, attr: Attribute, against: Option<CreatureRef>, reg: &Registries) -> f32
|
|
```
|
|
|
|
Der Charakterbogen gibt `None`. Die Alternative — Modifikator-Bedingungen auf den Träger beschränken
|
|
und „gegen Untote" als Trigger mit Zusatzschaden lösen — sieht lazier aus und ist es nicht: Der
|
|
Zusatzschaden liefe ein zweites Mal durch die Minderungskette aus 01, Resistenz und Krit griffen
|
|
getrennt. Das ist nicht dasselbe Ergebnis, sondern ein anderes. Und der Charakterbogen könnte
|
|
„Schaden 12 (17 gegen Untote)" nicht mehr anzeigen, weil der Bonus kein Modifikator mehr wäre.
|
|
|
|
Dazu `Game.events: Vec<Event>` als neues Feld im Baum.
|
|
|
|
### Auflagen an andere Tickets
|
|
|
|
- **10 Raum-Prefab-Format**: Raumbetreten als Trigger-Ort — `floor_entered` deckt nur die Etage ab,
|
|
nicht den einzelnen Raum.
|
|
- **13 Script-Engine wählen**: `emit_event` als Aktion und namespaced Events, sobald Stufe (c) steht.
|
|
- **14 Kampfformeln und Rundenablauf**: wo genau `attack_hit` und `damage_taken` im Zug feuern.
|