docs: ticket 09 (save-schema) aufloesen, nachtraege an 02 und 11, speicherstand ins glossar
This commit is contained in:
@@ -93,6 +93,17 @@ Engine den Begriff „Startausrüstung" kennt.
|
||||
`Item.value` → `base_price` (Grundwert des Gegenstands). Attribut `shop_price` → `price_modifier`
|
||||
(Charaktereigenschaft, über die Charisma wirkt). Zwei Dinge, zwei Namen.
|
||||
|
||||
### Nachtrag aus Ticket 09: zwei Fähigkeiten sind implizit
|
||||
|
||||
Oben steht, Fähigkeiten würden „von Charakteroptionen und Gegenständen gewährt". Genau das erzeugt eine
|
||||
Lücke: Ein Held, dessen Klasse nach einem Mod-Wechsel fehlt und der nichts trägt, hätte **gar keine**
|
||||
Fähigkeit — und könnte seit Ticket 11 nicht einmal angreifen, weil `core:attack` eine Fähigkeit ist.
|
||||
|
||||
Deshalb: **`core:attack` und `core:flee` sind jedem [Wesen](../../../CONTEXT.md) implizit gewährt.**
|
||||
|
||||
Das ist keine Sonderregel für den Ladefall. Es trifft ebenso jeden Gegner ohne `abilities`-Eintrag, vor
|
||||
dessen leerer Liste die KI aus Ticket 14 sonst stünde.
|
||||
|
||||
### Auflagen an andere Tickets
|
||||
|
||||
- **11 UI-Gerüst und Screen-Modell**: Charakterbogen und Charakterbau sind Schleifen über Registries,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 09 — Save-Schema
|
||||
|
||||
Type: grilling
|
||||
Status: open
|
||||
Status: resolved
|
||||
Blocked by: 07
|
||||
|
||||
## Question
|
||||
@@ -33,3 +33,117 @@ Ergebnis: Save-Schema als kommentiertes TOML-Beispiel plus Regeln für fehlende
|
||||
**eine** Etage, keine besuchten Vorgänger. `Creature.options` (Rasse, Klasse) muss mit, sonst hat der
|
||||
Held nach dem Laden keine Herkunft mehr — womit die Frage „was, wenn die Klasse fehlt?" oben scharf
|
||||
wird.
|
||||
|
||||
## Answer
|
||||
|
||||
Begriffe im [Glossar](../../../CONTEXT.md).
|
||||
|
||||
### Die Etage wird gespeichert, nicht gewürfelt
|
||||
|
||||
Ticket 14 hat einen seedbaren Generator geliefert, Regenerieren *wäre* also möglich. Drei Dinge
|
||||
sprechen dagegen:
|
||||
|
||||
1. Regenerieren verlangt **dieselben Prefabs und denselben Generator-Code**. Ein Mod-Update oder eine
|
||||
Engine-Änderung erzeugt ein anderes Layout — und der gespeicherte Held steht dann in einer Wand.
|
||||
2. Der Raumzustand aus 10 (`cleared`, gefallene Gegner, genommene Beute, Bodenloot) lässt sich nicht
|
||||
regenerieren und muss ohnehin mit. Der Seed spart also nur das Kachelgitter.
|
||||
3. Die Rechnung: 80×40 Felder sind als ASCII-Zeilen **rund 3 KB** — im selben Format wie Prefabs, also
|
||||
lesbar und im Notfall von Hand reparierbar.
|
||||
|
||||
Der Seed bleibt trotzdem im Save, aber für künftige Würfe (14), nicht zum Regenerieren.
|
||||
|
||||
### Ein Speicherstand, Permadeath
|
||||
|
||||
Autosave beim **Betreten einer Etage** und beim **Beenden**. Kein manuelles Speichern: Es brächte nur
|
||||
die Wahl des Zeitpunkts, und genau die hat Ticket 14 mit dem Seed im Save bereits abgeräumt. Mehrere
|
||||
benannte Slots wären eine Verwaltungsansicht für ein Spiel mit einem einzigen Helden.
|
||||
|
||||
Speichern **im Kampf ist erlaubt**, weil es beim Beenden ohnehin passieren muss; der Kampfzustand fällt
|
||||
bei serde-Ableitung gratis mit ab.
|
||||
|
||||
**Bei Tod wird der Speicherstand gelöscht** — aber erst beim Erscheinen des `GameOver`-Screens, nicht
|
||||
beim Fallen der Lebenspunkte. Stürzt das Spiel in der Sekunde dazwischen ab, ist der letzte Autosave
|
||||
noch da, und da er vom Betreten der Etage stammt, verschenkt das nichts.
|
||||
|
||||
### Der Save *ist* der `Game`-Baum
|
||||
|
||||
`#[derive(Serialize, Deserialize)]` durchgehend, mit **einer** handgeschriebenen Ausnahme: dem
|
||||
Kachelgitter als ASCII-Zeilen statt eines Arrays mit 3200 Einträgen.
|
||||
|
||||
Eigene Save-Structs hießen, jede Struktur zweimal zu pflegen und bei jeder Änderung daran zu denken —
|
||||
die Sorte Duplikat, die genau dann auseinanderläuft, wenn es niemand merkt. Der Preis ist ehrlich: Der
|
||||
Save hängt an internen Feldnamen, ein Umbenennen bricht alte Stände. Dafür gibt es die Schema-Version.
|
||||
|
||||
```toml
|
||||
[save]
|
||||
schema = 1 # das einzige Feld, das die Engine auswertet
|
||||
game = "0.1.0" # Diagnose bei Fehlermeldungen
|
||||
|
||||
[[save.mods]]
|
||||
id = "core"
|
||||
version = "1.0.0"
|
||||
```
|
||||
|
||||
Bewusst **kein** `floor` und `turn` im Header: Der `Game`-Baum serialisiert beide ohnehin ein paar
|
||||
Zeilen weiter unten. Sie oben zu wiederholen hätte nur einem Speicherstand-Browser genützt, der sie
|
||||
ohne Parsen des Rumpfs anzeigen will — und den gibt es bei einem Slot nicht.
|
||||
|
||||
Bei abweichender `schema`: **ablehnen** mit Klartextmeldung („Speicherstand stammt aus Schema 1, dieses
|
||||
Spiel liest 2"), und die **Datei bleibt liegen**. Migration zu schreiben, bevor ein einziger
|
||||
Speicherstand ausgeliefert wurde, ist Code für ein Problem, das noch niemand hat; Best-Effort auf einem
|
||||
strukturell anderen Schema erzeugt stille Verfälschung statt eines Fehlers. Und eine Datei zu löschen,
|
||||
weil man sie nicht lesen kann, wäre die schlechteste aller Reaktionen.
|
||||
|
||||
### Fehlende Mods: verwerfen, nie ablehnen
|
||||
|
||||
| Fehlt | Folge |
|
||||
|---|---|
|
||||
| Gegenstand im Inventar | Eintrag entfällt |
|
||||
| Gegenstand in der Ausrüstung | Slot bleibt leer |
|
||||
| Affix | entfällt, Gegenstand bleibt |
|
||||
| Quest | Fortschritt entfällt (bereits in 08) |
|
||||
| Prefab | egal — das Layout ist gespeichert |
|
||||
| Charakteroption | entfällt, Held bleibt spielbar |
|
||||
|
||||
Ein Spieler soll nicht seinen ganzen Lauf verlieren, weil er einen Mod deinstalliert hat.
|
||||
|
||||
**Die Mod-Version wird verglichen und protokolliert, mehr nicht.** Verschwundene IDs fangen die Regeln
|
||||
oben ab, geänderte Werte gelten einfach. Der Vergleich leistet reine Diagnose — eine Zeile
|
||||
„`meinmod` 1.0.0 → 1.2.0 seit dem Speichern", die genau dann Gold wert ist, wenn jemand meldet „mein
|
||||
Spielstand ist seit dem Update kaputt". Das ist die ehrliche Bilanz des `version`-Felds aus Ticket 03:
|
||||
Fiele es weg, verlöre man diese Zeile und sonst nichts.
|
||||
|
||||
Warnungen gehen in **dieselbe Sitzungsdatei** wie die des Mod-Ladelaufs, nicht in eine eigene
|
||||
`save-*.log`-Familie: Zwei Dateinamensmuster hießen zwei Rotationsregeln und zwei Orte zum Nachsehen.
|
||||
Die Warnliste aus 04 lebt stattdessen für die ganze Sitzung und wird neu geschrieben, wenn sie wächst
|
||||
— das ist sogar weniger Logik als zwei getrennte Läufe.
|
||||
|
||||
Dazu eine **Rückfrage vor dem Betreten des Spiels**: „12 Einträge fehlen (siehe `logs/load-….log`).
|
||||
Fortfahren?"
|
||||
|
||||
Das weicht bewusst von 04s Regel „eine Zeile, kein Screen" ab, und zwar aus einem Grund: Der Spieler
|
||||
will an dieser Stelle vielleicht gar nicht weiterspielen, sondern den fehlenden Mod nachinstallieren.
|
||||
Diese Wahl hat er nur *vor* dem Laden — danach ist der verkrüppelte Held gespeichert.
|
||||
|
||||
### Atomar schreiben
|
||||
|
||||
`data_dir()/rustkeep/save.toml`, dasselbe Verzeichnis wie Mods und `load_order.toml` (03 hat die
|
||||
Aufteilung in Daten und Konfiguration verworfen). Geschrieben wird nach `save.toml.tmp` und dann per
|
||||
`fs::rename` darübergezogen — das ersetzt die Zieldatei auf Windows 10 1607+ ebenso wie auf Unix
|
||||
(gegen die std-Doku geprüft).
|
||||
|
||||
**Das ist die eine Stelle in diesem Ticket, an der bewusst nicht lazy entschieden wurde.** Der Autosave
|
||||
überschreibt den einzigen Speicherstand, den es gibt; bricht das Schreiben ab — Absturz, voller
|
||||
Datenträger, geschlossener Deckel —, ist ein Lauf von einer Stunde weg. Temporärdatei plus `rename`
|
||||
sind zwei Zeilen und machen den Verlust unmöglich: Es liegt entweder die alte Datei da oder die neue,
|
||||
nie eine halbe.
|
||||
|
||||
**Gold braucht kein eigenes Feld** — es ist seit Ticket 01 ein gewöhnlicher stapelbarer Gegenstand
|
||||
(`core:gold`) und liegt im Inventar wie alles andere.
|
||||
|
||||
### Zwei Nachträge an geschlossene Tickets
|
||||
|
||||
| Ticket | Nachtrag |
|
||||
|---|---|
|
||||
| **02** | **`core:attack` und `core:flee` sind jedem Wesen implizit gewährt.** Fähigkeiten kommen bisher ausschließlich von Charakteroptionen und Gegenständen — ein Held, dessen Klasse fehlt und der nichts trägt, hätte gar keine und könnte nicht einmal angreifen. Das ist keine Sonderregel für den Ladefall: Auch jeder Gegner ohne `abilities`-Eintrag trifft es, und die KI aus 14 stünde vor einer leeren Liste |
|
||||
| **11** | `Confirm { text, on_yes: Box<Response> }` statt `on_yes: Command`. Die Rückfrage beim Laden braucht keinen Command, sondern einen Screen-Wechsel. `Response::Command` ist bereits eine Variante, der bisherige Fall bleibt also erhalten — der Sonderfall entfällt ersatzlos, die Struktur wird dabei *kleiner* |
|
||||
|
||||
@@ -108,10 +108,16 @@ 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
|
||||
keine zusätzliche `Response`-Variante und keine Antwortliste. `Confirm { text, on_yes: Box<Response> }` wird
|
||||
dadurch für jede Rückfrage wiederverwendbar, ohne eigene Variante je Anlass — **`Command` muss dafür
|
||||
`Clone` sein**, sonst kommt das Kommando nicht aus dem Screen heraus, ohne ihn zu zerstören
|
||||
(Befund aus Ticket 12). Dass `Inventory` als
|
||||
(Befund aus Ticket 12).
|
||||
|
||||
*Nachtrag aus Ticket 09:* Das Feld hieß ursprünglich `on_yes: Command`. Die Rückfrage beim Laden eines
|
||||
Speicherstands („Einträge fehlen, fortfahren?") braucht aber keinen Command, sondern einen
|
||||
Screen-Wechsel. `Response` ist die allgemeinere Form und enthält `Command` bereits als Variante — der
|
||||
bisherige Fall bleibt erhalten, der Sonderfall entfällt, und die Struktur wird dabei kleiner statt
|
||||
größer. Dass `Inventory` als
|
||||
Vollbild-Screen nach `UseItem` offen bleibt, fällt aus derselben Regel — man trinkt selten genau einen
|
||||
Trank.
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ 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.
|
||||
- [09 — Save-Schema](issues/09-save-schema.md) ([#10](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/10)) — **Layout wird gespeichert, nicht gewuerfelt**: Regenerieren aus dem Seed braeuchte dieselben Prefabs und denselben Generator-Code, sonst steht der Held nach einem Mod-Update in einer Wand — und der Raumzustand muss ohnehin mit, der Seed spart also nur 3 KB ASCII. **Ein Slot, Autosave bei Etagenbetreten und Beenden, Permadeath** (geloescht erst beim `GameOver`-Screen, nicht beim Tod). Der Save *ist* der `Game`-Baum per serde-Ableitung, eine Ausnahme fuers Kachelgitter. Schema-Version ist das einzige ausgewertete Header-Feld; abweichend ⇒ **ablehnen, Datei bleibt liegen** (Migration vor dem ersten ausgelieferten Save waere Code ohne Problem). Fehlende Mods: **verwerfen und warnen, nie ablehnen** — auch bei der Klasse. Mod-Version wird nur protokolliert, das ist die ehrliche Bilanz des Felds aus 03. Warnungen in dieselbe Sitzungsdatei wie der Mod-Ladelauf, plus **Rueckfrage vor dem Betreten** (der Spieler will vielleicht nachinstallieren statt weiterzuspielen). **Atomar geschrieben** ueber `.tmp` und `fs::rename` — die eine Stelle, an der bewusst nicht lazy entschieden wurde. Nachtraege: `core:attack`/`core:flee` implizit an 02, `Confirm { on_yes: Box<Response> }` an 11.
|
||||
- [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.
|
||||
|
||||
12
CONTEXT.md
12
CONTEXT.md
@@ -196,6 +196,18 @@ zuerst.
|
||||
Initiative ist bewusst eine Zahl *am Wesen* und keine Reihenfolgeliste — eine Liste wäre eine zweite
|
||||
Sammlung von Verweisen, die beim Tod eines Gegners ungültig würde.
|
||||
|
||||
## Speicherstand
|
||||
|
||||
Der serialisierte Spielzustand, TOML, **genau einer**. Er wird beim Betreten einer [Etage](#etage) und
|
||||
beim Beenden automatisch geschrieben; von Hand speichern kann man nicht, und bei Tod wird er
|
||||
**gelöscht** — der Lauf ist dann vorbei.
|
||||
|
||||
Das Etagen-Layout steht als Text darin, nicht als Generator-Seed: Ein Mod-Update würde sonst ein
|
||||
anderes Layout erzeugen und den Helden in eine Wand stellen.
|
||||
|
||||
Fehlt beim Laden ein [Mod](#mod), wird der betroffene Eintrag verworfen und gewarnt — nie der ganze
|
||||
Speicherstand abgelehnt.
|
||||
|
||||
## Registry
|
||||
|
||||
Eine benannte Sammlung moddbarer Definitionen, geschlüsselt nach namespaced ID. Es gibt **zehn**, und
|
||||
|
||||
Reference in New Issue
Block a user