//! 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, } #[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, /// Die `n`-Plaetze, schon aufgeloest. Anders als bei `e` nennt das Prefab /// den NPC — ein Schmied skaliert nicht mit der Tiefe. pub npcs: Vec, } #[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, /// `None` ist ein echter Zustand: Korridore gehoeren zu keinem Raum. #[serde(default, skip_serializing_if = "Option::is_none")] pub player_room: Option, pub rooms: Vec, } 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, D::Error> { let rows = Vec::::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(&self, s: S) -> Result { 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 { 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 { self.rooms.iter().position(|r| r.rect.contains(x, y)) } }