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:
@@ -6,7 +6,10 @@
|
||||
//! hiesse, die Prefabs zweimal zu parsen.
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Tile {
|
||||
Wall,
|
||||
Floor,
|
||||
@@ -15,7 +18,7 @@ pub enum Tile {
|
||||
Entrance,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
@@ -41,50 +44,112 @@ impl Rect {
|
||||
/// Ein NPC auf der Etage. Sein Warenkorb wird **einmal bei der Erzeugung**
|
||||
/// gezogen und liegt danach fest — sonst wuerfelte jedes Oeffnen des Ladens neu
|
||||
/// und der Spieler koennte sich das Angebot herbeischliessen (M4).
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct NpcInstance {
|
||||
pub pos: (u16, u16),
|
||||
pub def: String,
|
||||
/// Tabelle, also zuletzt (TOML).
|
||||
pub stock: Vec<super::creature::ItemInstance>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Room {
|
||||
/// Fuer `room_entered` und die Trigger am Prefab (Ticket 08/10).
|
||||
pub prefab: String,
|
||||
pub rect: Rect,
|
||||
/// Aus welcher Tabelle die Haufen dieses Raums ziehen — Prefab-`loot` oder,
|
||||
/// wenn es fehlt, die Etagentabelle (Ticket 16).
|
||||
pub loot_table: String,
|
||||
pub cleared: bool,
|
||||
/// Die `e`-Plaetze aus dem Prefab — bleiben erhalten, damit die Anzeige
|
||||
/// weiss, wo etwas stand.
|
||||
pub enemy_spawns: Vec<(u16, u16)>,
|
||||
/// Die Gegner dieses Raums. Wandern beim Betreten nach `Game.enemies` —
|
||||
/// **verschoben, nicht kopiert** (Ticket 10).
|
||||
pub enemies: Vec<super::creature::Creature>,
|
||||
/// Aus welcher Tabelle die Haufen dieses Raums ziehen — Prefab-`loot` oder,
|
||||
/// wenn es fehlt, die Etagentabelle. **Eine** ID je Raum: Alle Haufen ziehen
|
||||
/// ohnehin aus derselben, sie je Haufen zu wiederholen waere Redundanz
|
||||
/// (Ticket 16).
|
||||
pub loot_table: String,
|
||||
/// Die `l`-Plaetze. Ticket 16: erst beim **Aufheben** aufgeloest, deshalb
|
||||
/// stehen hier Positionen und keine Gegenstaende.
|
||||
pub loot: Vec<(u16, u16)>,
|
||||
|
||||
// Tabellen zuletzt (TOML).
|
||||
pub rect: Rect,
|
||||
/// Die Gegner dieses Raums. Wandern beim Betreten nach `Game.enemies` —
|
||||
/// **verschoben, nicht kopiert** (Ticket 10).
|
||||
pub enemies: Vec<super::creature::Creature>,
|
||||
/// Die `n`-Plaetze, schon aufgeloest. Anders als bei `e` nennt das Prefab
|
||||
/// den NPC — ein Schmied skaliert nicht mit der Tiefe.
|
||||
pub npcs: Vec<NpcInstance>,
|
||||
pub cleared: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Floor {
|
||||
pub number: u8,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
/// **Die eine handgeschriebene Ausnahme** (Ticket 09): Im Speicherstand
|
||||
/// stehen ASCII-Zeilen wie in einem Prefab, nicht 3200 Enum-Eintraege. Rund
|
||||
/// 3 KB, lesbar, im Notfall von Hand reparierbar.
|
||||
#[serde(deserialize_with = "rows_to_tiles")]
|
||||
pub tiles: Vec<Tile>,
|
||||
pub rooms: Vec<Room>,
|
||||
/// `None` ist ein echter Zustand: Korridore gehoeren zu keinem Raum.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub player_room: Option<usize>,
|
||||
pub rooms: Vec<Room>,
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
fn symbol(self) -> char {
|
||||
match self {
|
||||
Tile::Wall => '#',
|
||||
Tile::Floor => '.',
|
||||
Tile::Door => '+',
|
||||
Tile::Stairs => '>',
|
||||
Tile::Entrance => '<',
|
||||
}
|
||||
}
|
||||
|
||||
/// Unbekannte Zeichen werden zur Wand. Ein handverbogener Speicherstand
|
||||
/// soll den Helden einmauern, nicht das Spiel abbrechen.
|
||||
fn from_symbol(c: char) -> Tile {
|
||||
match c {
|
||||
'.' => Tile::Floor,
|
||||
'+' => Tile::Door,
|
||||
'>' => Tile::Stairs,
|
||||
'<' => Tile::Entrance,
|
||||
_ => Tile::Wall,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn rows_to_tiles<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<Tile>, D::Error> {
|
||||
let rows = Vec::<String>::deserialize(d)?;
|
||||
Ok(rows.iter().flat_map(|r| r.chars()).map(Tile::from_symbol).collect())
|
||||
}
|
||||
|
||||
/// `Serialize` von Hand — und **nur** deshalb, weil der Zeilenumbruch die
|
||||
/// Breite braucht, die ein feldweises `serde(with)` nicht sieht. Der Rest ist
|
||||
/// eins zu eins der Baum; eigene Save-Structs hiessen, jede Struktur zweimal zu
|
||||
/// pflegen (Ticket 09).
|
||||
impl Serialize for Floor {
|
||||
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||
use serde::ser::SerializeStruct;
|
||||
let mut st = s.serialize_struct("Floor", 6)?;
|
||||
st.serialize_field("number", &self.number)?;
|
||||
st.serialize_field("width", &self.width)?;
|
||||
st.serialize_field("height", &self.height)?;
|
||||
st.serialize_field("tiles", &self.rows())?;
|
||||
st.serialize_field("player_room", &self.player_room)?;
|
||||
// Tabellen zuletzt: TOML verlangt Werte vor Tabellen.
|
||||
st.serialize_field("rooms", &self.rooms)?;
|
||||
st.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl Floor {
|
||||
/// Das Kachelgitter als Textzeilen — dasselbe Format wie ein Prefab.
|
||||
pub fn rows(&self) -> Vec<String> {
|
||||
self.tiles
|
||||
.chunks(self.width as usize)
|
||||
.map(|z| z.iter().map(|t| t.symbol()).collect())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn tile(&self, x: u16, y: u16) -> Tile {
|
||||
if x >= self.width || y >= self.height {
|
||||
return Tile::Wall;
|
||||
|
||||
Reference in New Issue
Block a user