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.
184 lines
6.1 KiB
Rust
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))
|
|
}
|
|
}
|