feat(save): Speicherstand als Game-Baum

Autosave beim Betreten einer Etage und beim Beenden, ein Slot, bei Tod
geloescht. Der Save ist der Baum selbst — keine eigenen Save-Structs, die
man zweimal pflegt.

- Kacheln als ASCII-Zeilen wie ein Prefab, nicht 3200 Enum-Eintraege
- atomar geschrieben: tmp plus rename, der einzige Stand ueberlebt einen
  Abbruch
- fremdes Schema wird abgelehnt, die Datei bleibt liegen
- fehlende Mods verwerfen Eintraege, nie den Lauf
- Confirm-Screen vor dem Spiel, wenn der Save Eintraege verloren hat

Fuenf Befunde: die Ereigniswarteschlange gehoert nicht in den Save, die
Feldreihenfolge ist Teil des TOML-Formats, Serialize fuer Floor muss von
Hand, Confirm braucht on_no, und die Rueckfrage galt zuerst auch fuer
Mod-Ladewarnungen.
This commit is contained in:
2026-08-12 22:01:12 +02:00
parent 7fdd8bbb00
commit d09fef7069
15 changed files with 698 additions and 64 deletions

View File

@@ -147,3 +147,36 @@ nie eine halbe.
|---|---|
| **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* |
### Befunde aus M6
**Die Ereigniswarteschlange gehört nicht in den Save.** „Der Kampfzustand fällt bei serde-Ableitung
gratis mit ab" stimmt für `enemies`, `initiative` und `cooldowns` — nicht für `Game.events`. Die ist
Zwischenstand *innerhalb* eines Zuges: `drain_events` leert sie nach jeder einzelnen Handlung, und
gespeichert wird ausschließlich zwischen Zügen. Sie mitzuschreiben hieße, immer eine leere Liste zu
speichern. Sie steht jetzt auf `#[serde(skip)]`, mit einem `debug_assert` in `save()`, das die
Behauptung festhält statt sie nur zu behaupten.
Angenehmer Nebeneffekt: Ohne `events` taucht `CreatureRef` im Save gar nicht auf — und damit auch kein
`{ Enemy = 3 }` als Inline-Tabelle in einer Datei, die lesbar sein soll.
**Die Feldreihenfolge ist Teil des Formats.** TOML verlangt Werte vor Tabellen. „Der Save *ist* der
Baum" heißt deshalb: Die Reihenfolge der Felder in `Game`, `Creature`, `Room` und `Floor` ist keine
Kosmetik mehr, sondern eine Bedingung. `Game`, `Creature` und `Room` mussten umsortiert werden — alles
Skalare nach oben, alles Verschachtelte nach unten. Der Rundlauf-Test fällt sofort um, wenn jemand das
wieder vermischt; das ist die einzige Absicherung, die es dafür gibt.
**`Serialize` für `Floor` muss von Hand.** Der Zeilenumbruch im Kachelgitter braucht die Breite, und
ein feldweises `#[serde(with = …)]` sieht nur sein eigenes Feld. `Deserialize` bleibt abgeleitet (die
Zeilen werden einfach aneinandergehängt), `Serialize` sind fünfzehn Zeilen. Das ist genau die eine
handgeschriebene Ausnahme, die das Ticket vorgesehen hat — nur an einer anderen Stelle als gedacht.
**Die Rückfrage braucht `on_no`.** Ticket 11 hatte `Confirm { text, on_yes: Box<Response> }` beschlossen
und „Nein" implizit als Wegklicken gedacht. Im Ladefall heißt Nein aber *beenden und den Mod
nachinstallieren* — genau die Wahl, die es nach dem Laden nicht mehr gibt. `on_no` ist ein Feld mehr
und spart eine ungeschriebene Regel.
**Die Rückfrage gilt nur für Verworfenes aus dem Save.** Beim ersten Bauen hing sie an *allen*
Warnungen, also auch an denen des Mod-Ladelaufs — und damit hätte ein kaputter TOML-Eintrag in einem
Mod den Start blockiert. Ticket 04 hat dafür ausdrücklich „eine Zeile, kein Screen" entschieden. Beide
Zahlen werden jetzt getrennt gezählt, obwohl beide in dieselbe Logdatei laufen.