docs: ticket 11 (ui-geruest) aufloesen, nachtrag an 01, command ins glossar
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user