From d5a6feb09193cae635395dc1d9d7a5f8c2561708 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Wed, 12 Aug 2026 10:34:12 +0200 Subject: [PATCH] docs: ticket 08 (event-trigger-system) aufloesen, nachtraege an 01 und 07, trigger bedingung und quest ins glossar --- .../issues/01-effekt-vokabular.md | 13 ++ .../rustkeep-1.0/issues/07-game-state-baum.md | 15 ++ .../issues/08-event-trigger-system.md | 158 +++++++++++++++++- .scratch/rustkeep-1.0/map.md | 1 + CONTEXT.md | 30 ++++ 5 files changed, 216 insertions(+), 1 deletion(-) diff --git a/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md b/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md index 750b450..66c8006 100644 --- a/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md +++ b/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md @@ -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 diff --git a/.scratch/rustkeep-1.0/issues/07-game-state-baum.md b/.scratch/rustkeep-1.0/issues/07-game-state-baum.md index 28cb929..ecbf98b 100644 --- a/.scratch/rustkeep-1.0/issues/07-game-state-baum.md +++ b/.scratch/rustkeep-1.0/issues/07-game-state-baum.md @@ -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, 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` 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 diff --git a/.scratch/rustkeep-1.0/issues/08-event-trigger-system.md b/.scratch/rustkeep-1.0/issues/08-event-trigger-system.md index fb22ecd..29574df 100644 --- a/.scratch/rustkeep-1.0/issues/08-event-trigger-system.md +++ b/.scratch/rustkeep-1.0/issues/08-event-trigger-system.md @@ -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`. 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` 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, 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` 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. diff --git a/.scratch/rustkeep-1.0/map.md b/.scratch/rustkeep-1.0/map.md index a341162..ddc5d84 100644 --- a/.scratch/rustkeep-1.0/map.md +++ b/.scratch/rustkeep-1.0/map.md @@ -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` plus `events: Vec` in 07. ## Not yet specified diff --git a/CONTEXT.md b/CONTEXT.md index 1dd5272..16f8b47 100644 --- a/CONTEXT.md +++ b/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 = , 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),