docs: ticket 08 (event-trigger-system) aufloesen, nachtraege an 01 und 07, trigger bedingung und quest ins glossar
This commit is contained in:
@@ -87,6 +87,19 @@ Event-System.
|
||||
| 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.
|
||||
|
||||
### Bewusst nicht entschieden
|
||||
|
||||
Das **Bedingungsvokabular** für `condition`. Es kommt aus Ticket 08, damit Quests und Modifikatoren
|
||||
|
||||
@@ -147,6 +147,21 @@ Modifikatoren werden ausdrücklich **nicht** beim Ausrüsten in `modifiers` hine
|
||||
das Lesen billiger und das Schreiben gefährlich: Jedes Ablegen müsste exakt die richtigen Einträge
|
||||
wieder herausoperieren, und ein Affix-Wechsel ginge an der Kopie vorbei.
|
||||
|
||||
### Nachtrag aus Ticket 08
|
||||
|
||||
Die Lesesignatur trägt einen **optionalen Gegner**:
|
||||
|
||||
```rust
|
||||
Game::attribute(&self, who: CreatureRef, attr: Attribute, against: Option<CreatureRef>, reg: &Registries) -> f32
|
||||
```
|
||||
|
||||
Ohne ihn ist ein bedingter Modifikator aus 01 („+5 Schaden gegen Untote") beim Auslesen von `damage`
|
||||
nicht auswertbar — die Funktion wüsste nicht, gegen wen geschlagen wird. Der Charakterbogen gibt
|
||||
`None`.
|
||||
|
||||
Dazu kommt `events: Vec<Event>` als Feld in `Game`: Ticket 08 arbeitet Events als Warteschlange ab,
|
||||
weil synchrones Feuern über `self.quests` iterieren müsste, während `self.apply` erneut läuft.
|
||||
|
||||
### Auflagen an andere Tickets
|
||||
|
||||
- **08 Event- und Trigger-System**: Form von `QuestProgress`; das Bedingungsvokabular, das 01 bereits
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 08 — Event- und Trigger-System
|
||||
|
||||
Type: grilling
|
||||
Status: open
|
||||
Status: resolved
|
||||
Blocked by: 07
|
||||
|
||||
## Question
|
||||
@@ -30,3 +30,159 @@ Ergebnis: Eventliste mit Nutzlast, Bedingungs-Grammatik, TOML-Beispiel für eine
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -66,6 +66,7 @@ auch über Ticket 13.
|
||||
- [05 — Research: Script-Engine](issues/05-research-script-engine.md) ([#6](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/6)) — Empfehlung Rhai wegen Fehlermeldungen mit Zeile und Spalte, billiger Typ-Brücke, AST-Hot-Reload und Build ohne `cc`. Gemessen: mlua liest unter `StdLib::ALL_SAFE` per `io.open` echte Dateien — „safe" heißt dort speichersicher, nicht fähigkeitsbeschränkt. Lua gewinnt bei der Modder-Vertrautheit; WASM scheidet für 1.0 aus. **Kernfolge: reden Scripts nur über das Effekt-Vokabular, ist die Engine-Wahl umkehrbar.** Entscheidung offen in Ticket 13.
|
||||
- [06 — Research: ratatui-App-Architektur](issues/06-research-ratatui-architektur.md) ([#7](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/7)) — Screen-Stack über zentralem `Game`-State statt Elm oder Component-Trait; `game/` bleibt ratatui-frei; Popups per `Clear` auf Sub-`Rect` statt Widget-Stack; blockierendes `event::read()` ohne Tick; Dungeon-Grid ist Eigenbau, es gibt kein Widget dafür; ratatui 0.30.2 mit MSRV 1.88; `KeyEventKind::Press`-Filter unter Windows Pflicht. Voller Bericht in [research/06](research/06-ratatui-architektur.md).
|
||||
- [07 — Game-State-Baum](issues/07-game-state-baum.md) ([#8](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/8)) — `Registries` lebt **neben** `Game`, nicht darin: als Feld kollidierte `&mut game.player` mit `&game.registries` bei jedem Methodenaufruf. Verweise per `String`-ID, keine Indizes (die hängen an der Ladereihenfolge). Ein `Creature` für Held und Gegner, aber **zwei Felder** `player` und `enemies` statt einer `Vec` — Solo-Held heißt, „Gegner greift Gegner an" existiert nicht, und zwei Felder sind ein disjunkter Borrow. Ein `Floor`, kein Backtracking; Händler müssen deshalb auf jeder Etage vorkommen können. **Instanzzustand kommt**: `ItemInstance { def, affixes }`, und **Affixe sind die sechste Registry** — dadurch bleibt der Save eine ID-Liste und der Preis `base_price × Π price_factor`. Stapeln bei gleicher `def` und gleicher `affixes`. `enum CreatureRef`, dazu je ein Eingang `Game::apply` und `Game::attribute` — Letzteres auf `Game`, weil Auren beim Träger wohnen und ein `Creature` seine Attribute allein gar nicht ausrechnen kann. Kein Attribut-Cache.
|
||||
- [08 — Event- und Trigger-System](issues/08-event-trigger-system.md) ([#9](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/9)) — **Der Trigger ist die vierte Wirkungsform** neben Modifikator, Aktion und Fähigkeit: `{ on, condition, actions }` darf überall stehen, wo `modifiers` und `actions` stehen — sonst entstünde dieselbe Maschine ein zweites Mal in 14 als `on_hit`. Eine Bedingungsgrammatik für Trigger *und* Modifikatoren: getaggtes Enum, Liste ist UND, `not` je Eintrag, **kein ODER** (zwei Einträge oder ein gemeinsames Tag). Neun feste Events ohne Namensraum, jedes mit `source`/`target`; `attack_hit` und `damage_taken` bleiben getrennt, weil der eine beim Angreifer und der andere beim Getroffenen hängt. Im Trigger ist `self` immer der Träger. **Warteschlange statt synchron** — synchrones Feuern wäre ein Borrow-Fehler (über `self.quests` iterieren, während `self.apply` läuft) — mit Zähler bei 100 Events pro Zug statt Zyklengraph. Trigger werden gesucht, nicht indiziert. **Quest = Trigger mit `count`**, siebte Registry, erledigte bleiben markiert stehen. Zwei Nachträge an geschlossene Tickets: `start_quest` als siebte Aktion in 01, und `against: Option<CreatureRef>` plus `events: Vec<Event>` in 07.
|
||||
|
||||
## Not yet specified
|
||||
|
||||
|
||||
30
CONTEXT.md
30
CONTEXT.md
@@ -50,6 +50,36 @@ Unterscheidung ist wesentlich: Eine Aktion ist eine *Wirkung*, eine Fähigkeit i
|
||||
Fähigkeiten werden von [Charakteroptionen](#charakteroption) und Gegenständen gewährt, nicht im
|
||||
Inventar getragen — sonst könnte ein Magier seine Zaubersprüche verkaufen.
|
||||
|
||||
## Trigger
|
||||
|
||||
Die vierte Wirkungsform: `{ on = <event>, condition = [...], actions = [...] }`. Ein
|
||||
[Modifikator](#modifikator) rechnet um, eine [Aktion](#aktion) wirkt einmal, eine
|
||||
[Fähigkeit](#fähigkeit) bietet an — der Trigger sagt **wann**.
|
||||
|
||||
Er darf überall stehen, wo Modifikatoren und Aktionen stehen: an [Gegenständen](#gegenstand),
|
||||
[Affixen](#affix), [Charakteroptionen](#charakteroption) und [Quests](#quest). Im Trigger ist `self`
|
||||
immer sein *Träger*, `target` die andere Seite des Events.
|
||||
|
||||
Die Events sind Engine-Vokabular ohne Namensraum und eine feste Liste — wie Attribute und
|
||||
Schadenstypen.
|
||||
|
||||
## Bedingung
|
||||
|
||||
Eine Liste von Prüfungen, die *alle* zutreffen müssen — die Liste ist implizit UND, jeder Eintrag
|
||||
über `not` negierbar. Es gibt kein ODER und keine Verschachtelung: Zwei Möglichkeiten werden zu zwei
|
||||
Einträgen, oder der Modder fasst sie über ein gemeinsames [Tag](#tag) zusammen.
|
||||
|
||||
Es gibt genau eine Bedingungsgrammatik, und sie gilt an [Triggern](#trigger) wie an
|
||||
[Modifikatoren](#modifikator).
|
||||
|
||||
## Quest
|
||||
|
||||
Ein [Trigger](#trigger) mit einem Zähler: `count = N` bedeutet, dass das Event N-mal mit erfüllter
|
||||
[Bedingung](#bedingung) feuern muss, bevor die [Aktionen](#aktion) als Belohnung ablaufen. Moddbarer,
|
||||
namespaced Inhalt.
|
||||
|
||||
Eine Quest zählt erst mit, nachdem die [Aktion](#aktion) `start_quest` sie angenommen hat.
|
||||
|
||||
## Gegenstand
|
||||
|
||||
Ein Ding im Spiel, einheitlich strukturiert: Kennung, Name, Grundpreis, optionaler [Slot](#slot),
|
||||
|
||||
Reference in New Issue
Block a user