docs: ticket 13 (script-engine) aufloesen, rhai gewaehlt, stufe c hinter 1.0, nachtrag an 01
This commit is contained in:
@@ -130,7 +130,18 @@ Eine Formel für alles: Basisangriff `scale = 1.0` · Feuerball `amount = 20` ·
|
||||
`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. Der Katalog hat damit acht Einträge.
|
||||
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.
|
||||
|
||||
### Der Katalog
|
||||
|
||||
`damage` · `change_attribute` · `apply_modifier` · `remove_modifier` · `give_item` · `remove_item` ·
|
||||
`start_quest` (08) · `flee` (14) · `run_script` (13) — **neun**.
|
||||
|
||||
### Bewusst nicht entschieden
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 13 — Script-Engine wählen und Sandbox härten
|
||||
|
||||
Type: grilling
|
||||
Status: open
|
||||
Status: resolved
|
||||
Blocked by: 01
|
||||
|
||||
## Question
|
||||
@@ -27,3 +27,146 @@ reichen sie tiefer in den Game-State, ist sie es nicht. Zu klären:
|
||||
- **Wann fällt die Entscheidung endgültig** — jetzt, oder erst wenn Mod-Stufe (c) gebaut wird?
|
||||
|
||||
Ergebnis: gewählte Engine mit Begründung, Härtungs-Checkliste, und die Regel, was Scripts sehen dürfen.
|
||||
|
||||
## Answer
|
||||
|
||||
Begriffe im [Glossar](../../../CONTEXT.md).
|
||||
|
||||
### Die Wahl ist eine Implementierungsentscheidung — deshalb fällt sie leicht
|
||||
|
||||
Scripts lesen über **eine Handvoll Funktionen** und schreiben ausschließlich über **Aktionen**. Weder
|
||||
lesen sie direkt im `Game`-Baum noch halten sie Zustand außerhalb des Vokabulars.
|
||||
|
||||
Damit ist die Schnittstelle klein genug, um sie in jeder Engine nachzubauen — genau die Leitplanke aus
|
||||
Ticket 05. Ein Engine-Wechsel kostet die Bindungsschicht, nicht die Architektur.
|
||||
|
||||
Die strikte Variante — Script bekommt Daten, gibt Aktionen zurück, liest gar nichts — wäre sauberer
|
||||
gewesen und macht Scripts sinnlos: Was ein Script gegenüber einem Trigger *hinzufügt*, ist Rechnen
|
||||
(„Schaden skaliert mit der Zahl der Untoten im Raum"), und dafür muss es lesen können.
|
||||
|
||||
### Rhai — unbequem begründet
|
||||
|
||||
**Für ein unveröffentlichtes Terminal-Roguelike schreibt niemand Mods.** Lua gewinnt bei der
|
||||
Vertrautheit deutlich (Factorio, GMod, WoW, Noita, Don't Starve), aber das zahlt sich erst aus, wenn es
|
||||
eine Modder-Gemeinde gibt — und die entsteht nicht durch die Sprachwahl, sondern durch ein Spiel, das
|
||||
Leute spielen wollen. Bis dahin ist der Modder, den es zu bedienen gilt, der Autor selbst, und die
|
||||
Charter nennt „Rust lernen" als halbe Motivation.
|
||||
|
||||
Dazu der schärfste Einzelbefund aus 05: Ein Lua-Script hat unter `StdLib::ALL_SAFE` per `io.open` eine
|
||||
echte Datei gelesen — „safe" heißt dort speichersicher, nicht fähigkeitsbeschränkt. Rhai hat Datei-,
|
||||
Netz- und Prozesszugriff gar nicht erst in der Standard-Engine.
|
||||
|
||||
**Mods gelten als vertrauenswürdig.** Kein Spiel dieser Größenordnung behandelt sie anders — Factorio,
|
||||
Minecraft, Skyrim und Noita vertrauen alle. Der Spieler entpackt einen Ordner in sein Profil, dieselbe
|
||||
Vertrauensentscheidung wie bei jeder heruntergeladenen Software; eine Sandbox schützt ihn nicht davor,
|
||||
sondern verschiebt nur, wo er sie trifft. Damit scheidet Luau samt C++17-Toolchain und Lua-*Dialekt*
|
||||
aus — der Dialekt, den die vertrauten Lua-Modder gerade nicht können.
|
||||
|
||||
### Härtung — gegen Unfälle, nicht gegen Absicht
|
||||
|
||||
```rust
|
||||
engine.set_module_resolver(DummyModuleResolver::new()); // kein ./*.rhai von der Platte
|
||||
engine.disable_symbol("eval"); // kein Code aus Strings
|
||||
engine.set_max_operations(100_000); // CPU pro Aufruf
|
||||
engine.set_max_call_levels(32); // Rekursion
|
||||
engine.set_max_string_size(8_000);
|
||||
engine.set_max_array_size(1_000);
|
||||
engine.set_max_map_size(1_000);
|
||||
engine.set_max_expr_depths(64, 32); // Parser-Tiefe
|
||||
```
|
||||
|
||||
Alle Methodennamen gegen die Rhai-Doku geprüft. `on_progress` bleibt weg — es ist der Abbruchhaken für
|
||||
lang laufende Scripts, und `set_max_operations` deckt denselben Fall mit einem Wert statt mit einer
|
||||
Callback-Schleife ab.
|
||||
|
||||
**`set_max_operations` gehört ausdrücklich *nicht* in `[balance]`** aus Ticket 14: Ein Mod, der sein
|
||||
eigenes CPU-Limit hochsetzen kann, invertiert die Härtung.
|
||||
|
||||
Ein Script, das an ein Limit stößt, ist ein **Eintrags-Fehler** nach der Regel aus 04: Warnzeile mit
|
||||
Mod-ID, Script-Name und Zeile, Trigger verworfen, Spiel läuft weiter. Rhais Fehlermeldungen mit Zeile
|
||||
*und* Spalte sind laut 05 genau dafür da.
|
||||
|
||||
### Die Schnittstelle — was Scripts sehen dürfen
|
||||
|
||||
```
|
||||
attribute(wer, name) -> f64 has_tag(wer, tag) -> bool
|
||||
health(wer) -> f64 has_item(wer, id) -> i64
|
||||
stamina(wer) -> f64 floor() -> i64
|
||||
rand() -> f64
|
||||
```
|
||||
|
||||
Alle Leseoperationen sind an der Bedingungsgrammatik aus 08 gespiegelt — **Scripts sehen nichts, was
|
||||
eine Bedingung nicht auch sähe**, sie können nur damit rechnen. Das ist die Regel; die Liste selbst
|
||||
wird endgültig, wenn Stufe (c) gebaut wird.
|
||||
|
||||
Bewusst gestrichen: `turn()` und `enemy_count()`. Für beide gibt es kein benanntes Beispiel, und
|
||||
`enemy_count()` deckt sein eigenes Motivbeispiel („Schaden skaliert mit der Zahl der Untoten im Raum")
|
||||
nicht einmal ab — dafür bräuchte es Iteration. Die Umkehrbarkeit aus Q1 hängt daran, dass diese
|
||||
Schnittstelle klein bleibt; sie auf Vorrat wachsen zu lassen, während die Stufe verschoben ist, wäre
|
||||
das Gegenteil.
|
||||
|
||||
`health` und `stamina` sind **eigene** Funktionen statt `attribute("health")`, weil Ticket 01 scharf
|
||||
trennt: `max_health` ist ein Attribut, der Lebensstand ist Zustand. Diese Trennung darf im Script-API
|
||||
nicht verwaschen.
|
||||
|
||||
**`rand()` läuft durch den Generator aus `Game`**, und Rhais eigenes `rand`-Paket wird nicht
|
||||
registriert. Sonst gilt „Nachladen würfelt nicht neu" aus Ticket 14 für alles außer für Scripts — und
|
||||
ausgerechnet die interessanten Effekte würfelten beim Laden neu.
|
||||
|
||||
Geschrieben wird ausschließlich über Aktionen: Ein Script **gibt Aktionen zurück**, die in die
|
||||
Warteschlange aus 08 wandern. Damit deckelt der 100-Events-Zähler auch ein Script, das sich selbst
|
||||
aufruft.
|
||||
|
||||
### Scripts sind die elfte Registry
|
||||
|
||||
```toml
|
||||
{ action = "run_script", script = "meinmod:feuerregen" }
|
||||
```
|
||||
|
||||
`run_script` als **neunte Aktion**. Dadurch entstehen keine neuen Platzierungsregeln: Ein Script ist
|
||||
überall verwendbar, wo eine Aktion stehen darf — an einem Gegenstand, einer Fähigkeit, einer
|
||||
Questbelohnung, einem `ai`-Eintrag, einem Raum-Trigger. Dieselbe Bewegung wie `core:attack` in Ticket
|
||||
11 und `flee` in 14: Was wie Engine-Logik aussieht, ist Inhalt.
|
||||
|
||||
Die Alternative — Scripts registrieren sich selbst auf Events — wäre genau die Variante, bei der ein
|
||||
Script Zustand außerhalb des Vokabulars hält, und die macht die Engine-Wahl unumkehrbar.
|
||||
|
||||
### Risiken: beobachtet, mit benanntem Ausstiegssignal
|
||||
|
||||
„Beobachten" ohne Kriterium ist ein Wort ohne Folge, deshalb ausgeschrieben:
|
||||
|
||||
| Signal | Reaktion |
|
||||
|---|---|
|
||||
| Keine Rhai-Release in zwölf Monaten | wechseln — Stand 1.25.1 vom 29.05.2026, frisch, 3,4 Mio. Downloads in 90 Tagen |
|
||||
| Unsoundness in einem Pfad, den wir tatsächlich nutzen | wechseln — Issue #894 ist das bisher nicht |
|
||||
| `smartstring` unmaintained (RUSTSEC-2026-0249) | **kein** Ausstiegsgrund |
|
||||
|
||||
Zu `smartstring`: Das Repo wurde am 03.05.2026 archiviert, die Advisory kam am 10.08.2026 und ist ein
|
||||
INFO-Eintrag über fehlende Wartung, keine Verwundbarkeit (genannte Alternativen: `compact_str`,
|
||||
`smol_str`). Rhai 1.25.1 erschien danach ohne Wechsel. `cargo audit` wird das melden, und das ist die
|
||||
angemessene Menge Ärger dafür.
|
||||
|
||||
### Stufe (c) wandert hinter 1.0
|
||||
|
||||
Die **Entscheidung** steht jetzt, die **Umsetzung** nicht.
|
||||
|
||||
Stufen (a) und (b) sind durch 01–03 vollständig beschrieben und liefern bereits zehn Registries,
|
||||
Trigger, Bedingungen, Affixe und `[balance]` — sehr viel Moddbarkeit ohne eine Zeile Script. Stufe (c)
|
||||
ist die einzige, die eine neue Dependency, eine Bindungsschicht und eine Härtungsliste braucht.
|
||||
|
||||
Ticket 02 hat die Regel selbst formuliert: „Braucht ein Gegenstand etwas, das Modifikator, Aktion oder
|
||||
Fähigkeit nicht ausdrücken können, ist das eine Lücke im Vokabular." Eine Script-Engine einzubauen,
|
||||
*bevor* jemand an diese Lücke gestoßen ist, baut die Antwort vor der Frage — und jede Lücke, die ein
|
||||
Script stopfen würde, ist zuerst ein Hinweis, dass das Vokabular sie stopfen sollte.
|
||||
|
||||
Die Entscheidung jetzt zu treffen kostet dagegen nichts und legt fest, **wogegen** das Vokabular gebaut
|
||||
wird. Genau das ist die Leitplanke aus 05.
|
||||
|
||||
**Folge: Die Destination der Map ändert sich** — 1.0 liefert Mod-Stufen (a) und (b), Stufe (c) kommt
|
||||
danach.
|
||||
|
||||
### Nachtrag an Ticket 01
|
||||
|
||||
`run_script` als **neunte Aktion**. Der Katalog hat damit: `damage`, `change_attribute`,
|
||||
`apply_modifier`, `remove_modifier`, `give_item`, `remove_item`, `start_quest` (08), `flee` (14),
|
||||
`run_script` (13).
|
||||
|
||||
@@ -21,7 +21,11 @@ dort als Konvention im Body. Zuordnung Ticket → Gitea-Issue:
|
||||
|
||||
Alle Architektur- und Design-Entscheidungen für rustkeep 1.0 sind getroffen — nicht gebaut, entschieden.
|
||||
1.0 heißt: 5 Etagen mit Boss, Solo-Held mit Rasse und Klasse, Grid-Erkundung mit Menü-Kampf,
|
||||
Inventar und Ausrüstung, Quests, NPCs, Handel, Speichern/Laden, Mod-Support auf drei Stufen.
|
||||
Inventar und Ausrüstung, Quests, NPCs, Handel, Speichern/Laden, **Mod-Support auf den Stufen (a) und
|
||||
(b)**.
|
||||
|
||||
Stufe (c) — Scripting — ist entschieden, aber nicht Teil von 1.0: Engine (Rhai) und Schnittstelle
|
||||
stehen, gebaut wird sie, wenn ein Modder an eine Lücke im Vokabular stößt (13).
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -44,7 +48,7 @@ Feststehende Rahmenentscheidungen aus der Charter-Session:
|
||||
| Etagen | Hybrid — Raum-Prefabs in TOML, prozedural zusammengesetzt |
|
||||
| Daten | Alles TOML, zur Laufzeit geladen. `include_str!` und `enum ItemType` fallen |
|
||||
| Mod-IDs | Namespaced (`core:iron_sword`), Präfix aus dem Manifest und automatisch vorangestellt. Ladereihenfolge aus einer Spielerliste, nicht aus dem Manifest (03). Das Kernspiel ist selbst ein Mod |
|
||||
| Mod-Stufen | Daten → Typen → Scripting, in dieser Reihenfolge. Scripting setzt auf der Typ-Registry auf |
|
||||
| Mod-Stufen | Daten → Typen → Scripting, in dieser Reihenfolge. Scripting setzt auf der Typ-Registry auf. *13: Rhai gewählt, Umsetzung aber hinter 1.0 — gebaut wird sie, wenn das Vokabular an eine Grenze stößt* |
|
||||
| Effekte | Ein generisches Effekt-System mit Wirkung auf Stats. Rasse, Klasse, Items, Statuseffekte, Quest-Belohnungen und später Scripts nutzen dasselbe Vokabular — dieses Vokabular *ist* die Mod-API |
|
||||
| Quests | Bedingung + Belohnung als Daten, generisches Event-/Trigger-System |
|
||||
| Weltzustand | `Game`-Ownership-Baum, Verweise über `String`-IDs (keine Indizes, 07). `Registries` liegt daneben, nicht darin. Kein ECS |
|
||||
@@ -71,6 +75,7 @@ auch über Ticket 13.
|
||||
- [10 — Raum-Prefab-Format](issues/10-raum-prefab-format.md) ([#11](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/11)) — Freie Platzierung mit L-Korridoren; verbunden wird **Raum `i` mit Raum `i−1`**, weil eine Kette Erreichbarkeit *strukturell* garantiert statt sie nachtraeglich zu pruefen. Prefab als ASCII-Block mit Markern im Block, **Tueren explizit** (sonst stanzt der Generator Loecher in gezeichnete Waende); Rotation und Spiegelung erlaubt, `rotate = false` als Opt-out. **Ein `e` sagt „hier steht ein Gegner", nicht welcher** — das entscheidet die Etage, sonst waere jedes Prefab an eine Schwierigkeitsstufe gebunden. `[[prefabs]]` und `[[floors]]` sind die neunte und zehnte Registry; Auswahl durchgehend ueber **Tags**, Pflichtraeume zuerst platziert (das *ist* die Garantie), Flaechenpruefung beim Laden. **Kampf beginnt bei `room_entered`**, Gegner gehoeren zum Raum — das erspart Pfadsuche, Sichtlinien und Verfolgung. `Floor` mit `rooms`, `player_room: Option<usize>` (Korridor ist ein echter Zustand). **Siegbedingung erstmals definiert**: kein `[[floors]]` mit der naechsten Nummer = gewonnen, die Zahl „5" steht nirgends im Code. Nachtraege: `room_entered` an 08 (zwoelf Events), `GameOver { outcome }` an 14.
|
||||
- [11 — UI-Gerüst und Screen-Modell](issues/11-ui-geruest-screen-modell.md) ([#12](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/12)) — **„Aktion" hiess zwei Dinge**: `Action` bleibt das TOML-Effektvokabular aus 01, `Command` wird das UI-Kommando, Eingang `Game::execute(cmd, reg)` — die eine Stelle, an der ein Zug endet (Modifikatoren ticken, Event-Warteschlange leerarbeiten). Zwölf Commands, **alle kosten einen Zug** (ein `costs_turn`-Flag waere eine Ausnahmeliste); Angriff und Flucht sind keine Varianten, sondern `UseAbility("core:attack")` und `UseAbility("core:flee")` — damit moddbar. Ein gemeinsamer `Vec<Screen>` fuer Screens und Popups; Screens **geben `Response` zurueck statt selbst zu schieben**, sonst haelt ein Screen ein `&mut` auf die `Vec`, in der er liegt. Ein `Response::Command` schliesst alle Overlays — dasselbe Flag, das schon fuers Rendern existiert. Zehn Screens, `ListState` in der Variante (der Stack erhaelt den Cursor gratis, solange der Screen liegt). Nur `KeyEventKind::Press`: bei `Repeat` bestimmte die Tastaturwiederholrate die Spielzeit; gegen zaehes Laufen hilft ein Renn-Kommando in `game/`. Tastenbelegung fest, Pfeile und `hjkl`. ratatui-Freiheit von `game/` prueft ein `#[test]` statt eines Workspace. Nachtrag an 01: `symbol` traegt das Zeichen und einen Farbnamen selbst — `ui/theme.rs` ist Code und damit fuer Modder unerreichbar.
|
||||
- [12 — Prototyp: ratatui-Durchstich](issues/12-prototyp-ratatui-durchstich.md) ([#13](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/13)) — **Das Modell aus 11 traegt.** Primaerquelle: Branch `prototyp/12-ratatui-durchstich`, `cargo run --example durchstich`. Bestaetigt: `mod game` bleibt ratatui-frei ohne Disziplin, Grid als eigenes `impl Widget` ueber `buf[(x,y)].set_symbol(…)`, `Vec<Screen>` mit Overlay-Flag fuer Rendern *und* Auto-Schliessen, `Response` statt Stack-Mutation (kein Borrow-Aerger). **Der Windows-Fallstrick ist jetzt eine Zahl**: `Tasten 14 → Press 7 → Zuege 7` — ohne Filter kostete jeder Schritt zwei Zuege und damit doppelte Spielzeit. Zwei Korrekturen: 11 sagte „alle Commands kosten einen Zug", richtig ist **alle ausgefuehrten** (`Outcome { TurnPassed, Rejected(&str) }`, Grund reist mit zurueck); und `Command` muss `Clone` sein, damit `Confirm` es herausgeben kann. Ungetestet blieb `Registries` neben `Game` — der Durchstich laedt keine.
|
||||
- [13 — Script-Engine wählen](issues/13-script-engine-waehlen.md) ([#14](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/14)) — **Rhai**, aber die Wahl ist dank der Leitplanke aus 05 nur noch eine *Implementierungs*-Entscheidung: Scripts lesen ueber eine Handvoll Funktionen (gespiegelt an der Bedingungsgrammatik aus 08 — sie sehen nichts, was eine Bedingung nicht auch saehe) und schreiben ausschliesslich ueber Aktionen. Begruendung unbequem, aber ehrlich — **fuer ein unveroeffentlichtes Terminal-Roguelike schreibt niemand Mods**, der Lua-Vorsprung zahlt sich erst mit einer Gemeinde aus. Mods gelten als **vertrauenswuerdig** (wie bei Factorio, Minecraft, Skyrim), gehaertet wird gegen Unfaelle: acht Engine-Schalter, gegen die Doku geprueft; `set_max_operations` ausdruecklich **nicht** in `[balance]`, sonst hebt ein Mod sein eigenes Limit auf. `rand()` laeuft durch den Seed aus 14, sonst wuerfelten ausgerechnet Scripts beim Laden neu. `run_script` als neunte Aktion, Scripts als **elfte Registry**. Risiken beobachtet mit benanntem Ausstiegssignal; `smartstring` ist ein INFO-Advisory, kein Ausstiegsgrund. **Stufe (c) wandert hinter 1.0** — eine Script-Engine zu bauen, bevor jemand an eine Vokabular-Luecke gestossen ist, baut die Antwort vor der Frage.
|
||||
- [14 — Kampfformeln und Rundenablauf](issues/14-kampfformeln-und-rundenablauf.md) ([#15](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/15)) — **Ein Zug = Spielerhandlung plus alle Gegnerreaktionen**, sonst tickte ein Debuff bei drei Gegnern viermal. Initiative als `i32` **am Wesen** statt als Reihenfolgeliste (die waere eine zweite Indexsammlung mit demselben Ungueltigkeitsproblem); tote Gegner erst am Ende von `execute`, nach dem Leeren der Warteschlange. Trefferformel und Schadenskette mit drei Klemmbereichen — `0.8` bei Resistenz, weil `1.0` Immunitaet und damit ein Spielabbruch waere. **Die Krit-Reihenfolge-Frage loest sich auf**: multiplikativ ist kommutativ. Eigener xorshift, **Seed im Save** — Nachladen wuerfelt nicht neu. Faehigkeiten kosten Ausdauer, Abklingzeit in Zuegen, gesperrt ⇒ `Rejected` ohne Zug. **Achte Registry: Wesen** — die Luecke lag seit 02 offen; `ai` als geordnete Bedingungsliste. `[balance]` verschmilzt **feldweise**, ausdrueckliche Ausnahme zu 03 (festes Schema). Vier Nachtraege: `scale` und `flee` an 01, drei Felder an 07, zwei Events an 08, `GameOver` an 11.
|
||||
|
||||
## Not yet specified
|
||||
|
||||
Reference in New Issue
Block a user