docs: ticket 11 (ui-geruest) aufloesen, nachtrag an 01, command ins glossar

This commit is contained in:
2026-08-12 10:54:09 +02:00
parent d5a6feb091
commit 2c19cb2aaf
4 changed files with 175 additions and 2 deletions

View File

@@ -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

View File

@@ -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<Screen>` 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<Popup>` 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<KeyCode, Command>`-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<Screen> } + 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.

View File

@@ -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<CreatureRef>` plus `events: Vec<Event>` 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<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.
## 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

View File

@@ -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.