From 2c19cb2aaf2c1c2c567925e8c164d022ff7885ac Mon Sep 17 00:00:00 2001 From: MDietermann Date: Wed, 12 Aug 2026 10:54:09 +0200 Subject: [PATCH] docs: ticket 11 (ui-geruest) aufloesen, nachtrag an 01, command ins glossar --- .../issues/01-effekt-vokabular.md | 16 ++ .../issues/11-ui-geruest-screen-modell.md | 149 +++++++++++++++++- .scratch/rustkeep-1.0/map.md | 3 +- CONTEXT.md | 9 ++ 4 files changed, 175 insertions(+), 2 deletions(-) diff --git a/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md b/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md index 66c8006..d119fb1 100644 --- a/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md +++ b/.scratch/rustkeep-1.0/issues/01-effekt-vokabular.md @@ -100,6 +100,22 @@ Questketten beim Betreten des Kellers bereits erfüllt wären. 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. + ### 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/11-ui-geruest-screen-modell.md b/.scratch/rustkeep-1.0/issues/11-ui-geruest-screen-modell.md index 86e817c..481166c 100644 --- a/.scratch/rustkeep-1.0/issues/11-ui-geruest-screen-modell.md +++ b/.scratch/rustkeep-1.0/issues/11-ui-geruest-screen-modell.md @@ -1,7 +1,7 @@ # 11 — UI-Gerüst und Screen-Modell Type: grilling -Status: open +Status: resolved Blocked by: — ## Question @@ -32,3 +32,150 @@ Ergebnis: Screen-Enum, Stack-Modell, Action-Liste und die Regel, was `game/` nic - **04**: Der Startbildschirm trägt eine Zeile mit Warnungsanzahl und Logdateiname („12 Ladewarnungen — `logs/load-20260812-112305.log`"). **Eine Zeile, kein Screen** — der Inhalt gehört in einen Texteditor, nicht in ein TUI mit Scrolling und Navigation. + +## Answer + +Begriffe im [Glossar](../../../CONTEXT.md). + +### Zwei Vokabulare, zwei Namen + +Der Bericht aus 06 empfahl „genau einen Eintrittspunkt `Game::apply(Action)`" — Ticket 07 hat +`Game::apply` inzwischen aber für die Aktionen aus 01 vergeben. „Geh nach Norden" ist keine der sieben +Aktionen und wird nie eine. + +| | | +|---|---| +| **Action** | TOML-Effektvokabular aus 01 — `Game::apply(action, source, target, reg)` | +| **Command** | Kommando der UI ans Spiel — `Game::execute(cmd, reg)` | + +`Game::execute` ist die eine Stelle, an der ein **Zug endet**: Zähler hoch, befristete Modifikatoren +ticken, `turn_started` feuern, Event-Warteschlange aus 08 leerarbeiten. Direkte Methoden auf `Game` +(`game.move_player(dir)`) wären lazier gewesen und hätten dasselbe zwölfmal gebraucht — eine Methode +hätte es vergessen. + +### Zwölf Commands, alle kosten einen Zug + +`Move(Direction)` · `UseAbility(AbilityId, CreatureRef)` · `UseItem(usize)` · `Equip(usize)` · +`Unequip(SlotId)` · `Drop(usize, u32)` · `PickUp(usize)` · `Descend` · `Talk(NpcId)` · +`Buy(ItemId, u32)` · `Sell(usize, u32)` · `Wait` + +Weder Angriff noch Flucht sind **eigene Varianten** — beides sind Fähigkeiten: +`UseAbility("core:attack", ziel)` und `UseAbility("core:flee", …)`. Ticket 02 hat Fähigkeiten als +Registry festgelegt, also gehören sie dorthin, und ein Mod kann beide ersetzen: eine Klasse, die +besser flieht, ist damit eine Datenänderung statt eines Engine-Sonderfalls. + +Auch Handel kostet einen Zug. Ein `fn costs_turn(&self) -> bool` wäre eine Ausnahmeliste, die mit +jeder neuen Variante gepflegt werden muss und bei der irgendwann jemand falsch rät. Zehn Züge beim +Einkauf stören nur, wenn im Laden gekämpft wird; dagegen hilft, dass `Buy(item, 10)` bereits *ein* +Command ist. + +**Screen-Navigation ist reine UI und fasst `Game` nie an** — Inventar öffnen ist +`push(Screen::Inventory)`, kein Command. Speichern und Laden sind ebenfalls keine Commands: Sie kosten +keinen Zug und bekommen ihren Weg von Ticket 09. + +### Ein Stack, eine Antwort + +```rust +enum Response { None, Pop, Push(Screen), Command(Command), Quit } +fn handle_key(&mut self, key: KeyEvent) -> Response +``` + +`Vec` für Screens und Popups gemeinsam. Popup über Popup ist real und kommt früher als +gedacht — Inventar → Gegenstandsdetail → „wirklich wegwerfen?"; getrennte Felder (`current` plus +`Option` wie chess-tui) bräuchten für den dritten Layer eine zweite Mechanik. Der +Rendering-Befund aus 06 bleibt unberührt: `Clear` auf ein zentriertes Sub-`Rect` ist eine Eigenschaft +des Screens, nicht des Stacks. Gerendert wird vom letzten Vollbild-Screen aufwärts. + +Der Screen **gibt zurück, statt selbst zu schieben** — sonst hielte er ein `&mut` auf genau die `Vec`, +in der er liegt, und die Schleife müsste ihn vorher herausnehmen und danach zurücklegen. Dasselbe +Muster wie die Event-Warteschlange in 08: nicht mitten in der Iteration mutieren, sondern zurückgeben +und danach ausführen. Nebeneffekt: `Response::Command` ist der einzige Weg nach `game/`, ein Screen +ohne Command hat den Spielzustand also nachweislich nicht angefasst. + +**Ein `Response::Command` schließt alle Overlay-Screens.** Das Flag existiert bereits fürs Rendern und +beantwortet damit auch „schließt sich nach einer Rückfrage von selbst" — eine Zeile in der Schleife, +keine zusätzliche `Response`-Variante und keine Antwortliste. `Confirm { text, on_yes: Command }` wird +dadurch für jede Rückfrage wiederverwendbar, ohne eigene Variante je Anlass. Dass `Inventory` als +Vollbild-Screen nach `UseItem` offen bleibt, fällt aus derselben Regel — man trinkt selten genau einen +Trank. + +### Zehn Screens + +`MainMenu` (trägt die Ladewarnungs-Zeile aus 04) · `CharacterCreation` (Registry-Schleife aus 02) · +`Explore` · `Combat` · `Inventory` · `CharacterSheet` · `QuestLog` · `Shop` · `Dialog` · `Confirm` + +Kein `ItemDetail`-Popup: Die Inventarliste zeigt Details in einer Seitenspalte, das ist ein Layout und +kein Screen. `Shop` bleibt getrennt von `Dialog`, weil Handel zwei Listen nebeneinander braucht und +der Dialog eine Textfläche mit Antwortoptionen. + +`ListState` lebt in der Screen-Variante und wird beim `pop()` mitverworfen. Der Stack macht den +wichtigen Fall gratis richtig: Solange ein Screen im Stack *liegt*, existiert sein Zustand weiter — +Inventar → Detail → zurück landet wieder auf demselben Eintrag. Zurückgesetzt wird nur beim echten +Schließen und Neuöffnen, und das hat noch nie jemanden gestört. Eine Map `ScreenKind → ListState` +wäre eine Zustandsverwaltung neben dem Stack für genau diesen Randfall. + +### Eingabe + +**Nur `KeyEventKind::Press`.** Der Filter ist unter Windows ohnehin Pflicht (06), aber der Grund gegen +`Repeat` ist schwerer als die UX: Ein Schritt ist ein Zug, also bestimmte die Tastaturwiederholrate des +Betriebssystems die Spielzeit — ein gedrückt gehaltener Pfeil verbrennte ein Gift-Debuff mit dreißig +Zügen pro Sekunde und feuerte `turn_started` ebenso oft. + +Die Decke, falls sich Laufen zäh anfühlt: ein Renn-Kommando („laufe, bis etwas passiert"), das *ein* +Tastendruck ist und in `game/` viele Züge auflöst — dort kann es an Gegnern und Abzweigungen anhalten. +Das ist die Lösung, die Roguelikes seit Jahrzehnten nehmen, und sie gehört nicht in die +Eingabeschleife. + +Tastenbelegung fest im Code, Pfeiltasten **und** `hjkl` — drei zusätzliche `match`-Arme sind die +billige Höflichkeit gegenüber Vim-Fingern. Eine `keybindings.toml` bräuchte Parser, Konfliktprüfung +(„zwei Commands auf `i`") und einen Einstellungs-Screen, damit sie überhaupt auffindbar ist. Kommt sie +doch, wird der `match` zu einem `HashMap`-Lookup und sonst ändert sich nichts. + +### Modulstruktur + +``` +src/ + main.rs # Laden, Fehlerpfad aus 04, dann ui::run + game/ # kennt ratatui nicht — Test prueft das + creature.rs floor.rs command.rs action.rs event.rs condition.rs + data/ # Definitionen, Registries, Mod-Lader aus 03/04 + loader.rs registries.rs item.rs affix.rs quest.rs + ui/ + app.rs # App { game, registries, screens: Vec } + Schleife + input.rs theme.rs + screens/ widgets/grid.rs +``` + +`App` hält `game` und `registries` als **getrennte Felder**, damit die disjunkten Borrows aus 07 +funktionieren. `Command` liegt in `game/`, obwohl er von der UI kommt — er ist Vokabular des Spiels, +nicht der Darstellung, und `game/` muss ihn ausführen können. Andersherum wäre `ui/` die Heimat eines +Typs, den `game/` importieren muss, und die Abhängigkeit zeigte in die falsche Richtung. + +Die ratatui-Freiheit prüft ein `#[test]`, der `src/game/**/*.rs` liest und auf `ratatui` sucht. Fünf +Zeilen; ein Cargo-Workspace mit `game` als eigener Crate wäre echte Compiler-Durchsetzung und ist die +Decke, falls es je ernst wird. + +### Nachtrag an Ticket 01: Darstellung muss moddbar sein + +01 hat `symbol` als *Schlüssel* definiert, dessen Zuordnung `ui/theme.rs` macht. 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. Das widerspricht dem Grundsatz aus 02 +und 03, dass Inhalt moddbar ist. + +Stattdessen trägt die TOML das Zeichen selbst und einen Farbnamen 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 ratatui-frei, weil ein Farbname eine Zeichenkette ist. + +### Auflagen an andere Tickets + +- **09 Save-Schema**: Speichern und Laden sind keine Commands und brauchen einen eigenen Weg. +- **12 Prototyp**: Der Durchstich baut `MainMenu`, `Explore` und das Grid-Widget; alles andere ist + Kulisse. +- **14 Kampfformeln**: `core:attack` und `core:flee` müssen als Fähigkeiten im Kernspiel existieren — + ohne sie hat `UseAbility` weder Basisangriff noch Flucht. diff --git a/.scratch/rustkeep-1.0/map.md b/.scratch/rustkeep-1.0/map.md index ddc5d84..8325424 100644 --- a/.scratch/rustkeep-1.0/map.md +++ b/.scratch/rustkeep-1.0/map.md @@ -67,13 +67,14 @@ auch über 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. +- [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` 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. ## Not yet specified - **Balancing** — konkrete Zahlen für Attribute, Schaden, Preise und Erfahrung über fünf Etagen. Wartet auf Kampfformeln (14), Progression (15) und Loot (16); vorher gibt es nichts zu balancieren. - **Boss-Design** — ob Bosse nur Gegner mit besseren Werten sind oder eigene Mechanik brauchen (Phasen, Skripte). Berührt Stufe (c) des Mod-Supports. - **NPC- und Handelsmodell** — Warenbestand, Dialogdaten. Wartet auf das Event-System (08). Die Preisformel steht bereits in 07; weil 07 Backtracking ausgeschlossen hat, muss ein Händler auf **jeder** Etage erreichbar sein. -- **Inventar-UX** — Slots, Gewicht oder Kapazität, Sortierung, Vergleichsansicht. Wartet jetzt auf das Screen-Modell (11), nicht mehr auf 06. +- **Inventar-UX** — Gewicht oder Kapazität (`carry_capacity` ist seit 01 ein Attribut ohne Regel), Sortierung, Vergleichsansicht. 11 hat nur den Screen und die Commands festgelegt, nicht was darin steht. - **Mod-Verteilung** — durch 03 auf „Ordner ins Profil entpacken" geschrumpft, ohne Versionsprüfung. Offen ist nur noch, ob 1.0 mehr braucht. ## Out of scope diff --git a/CONTEXT.md b/CONTEXT.md index 16f8b47..0fbdf16 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -40,6 +40,15 @@ Beispiele: „12 Schaden zufügen", „20 Leben heilen", „50 Gold geben", „G Eine Aktion kann einen befristeten [Modifikator](#modifikator) erzeugen — so hängen die beiden Begriffe zusammen, ohne dasselbe zu sein. +## Command + +Ein Kommando der Oberfläche an das Spiel — „geh nach Norden", „leg das an", „kauf drei Tränke". +**Nicht dasselbe wie eine [Aktion](#aktion)**, und der Unterschied ist der Grund für den englischen +Namen: Eine Aktion ist Effektvokabular aus einer TOML, ein Command kommt von einem Tastendruck. + +Jeder Command kostet einen [Zug](#zug), auch Handel — es gibt keine Ausnahmeliste. Bildschirme zu +öffnen ist dagegen kein Command, sondern reine Oberfläche und fasst den Spielzustand nicht an. + ## Fähigkeit Ein benanntes, wiederholt auslösbares Angebot an den Spieler, das [Aktionen](#aktion) ausführt.