Files
rustkeep/src/game/floor.rs
MDietermann d09fef7069 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.
2026-08-12 22:01:12 +02:00

184 lines
6.1 KiB
Rust

//! Etage — Innenbau aus Ticket 10.
//!
//! `Room.enemy_spawns`, `loot` und `cleared` liest in M2 noch niemand: Sie
//! brauchen Kampf (M3) und Beutetabellen (M4). Der Generator fuellt sie aber
//! bereits, weil die Marker im Prefab stehen — sie erst spaeter zu lesen
//! hiesse, die Prefabs zweimal zu parsen.
#![allow(dead_code)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Tile {
Wall,
Floor,
Door,
Stairs,
Entrance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rect {
pub x: u16,
pub y: u16,
pub w: u16,
pub h: u16,
}
impl Rect {
/// Ueberlappt oder beruehrt — inklusive einem Feld Rand, damit zwei Raeume
/// nicht Wand an Wand kleben.
pub fn touches(&self, other: &Rect) -> bool {
self.x < other.x + other.w + 1
&& other.x < self.x + self.w + 1
&& self.y < other.y + other.h + 1
&& other.y < self.y + self.h + 1
}
pub fn contains(&self, x: u16, y: u16) -> bool {
x >= self.x && y >= self.y && x < self.x + self.w && y < self.y + self.h
}
}
/// 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, Serialize, Deserialize)]
pub struct NpcInstance {
pub pos: (u16, u16),
pub def: String,
/// Tabelle, also zuletzt (TOML).
pub stock: Vec<super::creature::ItemInstance>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Room {
/// Fuer `room_entered` und die Trigger am Prefab (Ticket 08/10).
pub prefab: String,
/// 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 `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>,
}
#[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>,
/// `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;
}
self.tiles[y as usize * self.width as usize + x as usize]
}
/// Startfeld: das `<` aus dem Eingangs-Prefab, sonst das erste begehbare.
pub fn entrance(&self) -> (u16, u16) {
for y in 0..self.height {
for x in 0..self.width {
if self.tile(x, y) == Tile::Entrance {
return (x, y);
}
}
}
for y in 0..self.height {
for x in 0..self.width {
if self.tile(x, y) != Tile::Wall {
return (x, y);
}
}
}
(1, 1)
}
/// In welchem Raum steht diese Position — `None` heisst Korridor.
pub fn room_at(&self, x: u16, y: u16) -> Option<usize> {
self.rooms.iter().position(|r| r.rect.contains(x, y))
}
}