use crate::log::LogLine; use color::Rgba8; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::hash::{Hash, Hasher}; /// The visual representation of a single board cell. /// /// `Glyph` holds everything needed to draw one cell on screen: which tile /// index to display and what colors to use. It is stored per-cell (not per /// archetype), so individual cells can vary their appearance independently. /// /// `tile` is a left-to-right, top-to-bottom index into the board's bitmap /// font. For the default CP437 font this matches the ASCII/CP437 code point. /// /// `Glyph` values come from the map file palette and are set at load time. /// The player is the only entity whose glyph is hardcoded at runtime /// (see [`Glyph::player`]). #[derive(Clone, Copy, PartialEq, Eq)] pub struct Glyph { /// Tile index into the board's bitmap font (left-to-right, top-to-bottom). pub tile: u32, /// Foreground color, applied to non-background pixels of the tile. pub fg: Rgba8, /// Background color, drawn as a filled rectangle behind the tile. pub bg: Rgba8, } impl Hash for Glyph { /// Hash via packed u32 representations so the impl stays in sync with Eq. fn hash(&self, state: &mut H) { self.tile.hash(state); self.fg.to_u32().hash(state); self.bg.to_u32().hash(state); } } impl Glyph { /// Returns the glyph used to render the player: tile 64 (`@`) in white on dark blue. /// /// This is the only hardcoded glyph; all other glyphs come from the map /// file palette. It will be removed once the player becomes a scripted /// object with its own palette entry. pub const fn player() -> Self { Self { tile: 64, fg: Rgba8 { r: 255, g: 255, b: 255, a: 255 }, // white bg: Rgba8 { r: 0, g: 0, b: 200, a: 255 }, // dark blue } } } /// Specifies a bitmap font for a board. /// /// Each board can optionally specify a font image and tile dimensions. /// When absent, the app's default embedded CP437 font is used. #[derive(Clone, PartialEq, Eq)] pub struct FontSpec { /// Path to the PNG font image, relative to the working directory. pub path: String, /// Width of each tile in the font image, in pixels. pub tile_w: u32, /// Height of each tile in the font image, in pixels. pub tile_h: u32, } /// The behavioral properties of a board cell at runtime. /// /// `Behavior` is a plain data struct returned by [`Archetype::behavior`]. It /// contains the properties the engine needs to simulate a cell — currently /// passability and opacity. Future properties (pushable, shootable, etc.) can /// be added here without changing call sites. /// /// For scripted objects, passability and opacity are stored directly on /// [`ObjectDef`] and will eventually be overridable by Rhai scripts at runtime. #[derive(Copy, Clone, Debug)] pub struct Behavior { /// Whether an entity can walk through this cell. pub passable: bool, /// Whether this cell blocks line of sight (reserved for future rendering). pub opaque: bool, } /// A class of board cell, encoding its default behavior and appearance. /// /// `Archetype` is an enum of the element types the engine knows about. Each /// variant provides a default [`Behavior`] (via [`Archetype::behavior`]) and a /// default [`Glyph`] (via [`Archetype::default_glyph`]) used when the editor /// stamps a cell. /// /// Map files reference archetypes by [`name`](Archetype::name) (e.g. `"wall"`), /// so the list of variants can be reordered without breaking saved games. /// /// ## Object special case /// /// `Archetype::Object` currently returns a static default `Behavior`. /// TODO: In the future, Object passability and opacity will come from the cell's Rhai script /// rather than from this enum. [`Board::is_passable`] will need a special branch /// at that point. `ErrorBlock` is used as a sentinel for unrecognized archetype /// names in map files — it should never appear in a valid board. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum Archetype { /// An open cell; the player and other entities can pass through it. Empty, /// A solid wall; impassable and opaque. Wall, /// Sentinel for map files that reference an unknown archetype name. /// Renders as a yellow `?` on red to make the error visible in-game. ErrorBlock, } impl Archetype { /// Returns the default [`Behavior`] for this archetype. /// /// For `Object`, this is a placeholder until Rhai scripts drive the behavior. pub fn behavior(&self) -> Behavior { match self { Archetype::Empty => Behavior { passable: true, opaque: false, }, Archetype::Wall => Behavior { passable: false, opaque: true, }, Archetype::ErrorBlock => Behavior { passable: false, opaque: true, }, } } /// Returns the canonical name used to reference this archetype in map files. pub fn name(&self) -> &'static str { match self { Archetype::Empty => "empty", Archetype::Wall => "wall", Archetype::ErrorBlock => "error_block", } } /// Returns the default glyph painted when the editor stamps this archetype. /// /// This glyph is used only for new cells created in the editor; existing /// cells retain their own per-cell glyph. pub fn default_glyph(&self) -> Glyph { match self { Archetype::Empty => Glyph { tile: 32, fg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, }, Archetype::Wall => Glyph { tile: 35, fg: Rgba8 { r: 0x80, g: 0x80, b: 0x80, a: 255 }, bg: Rgba8 { r: 0x60, g: 0x60, b: 0x60, a: 255 }, }, // Visually distinct so malformed map files are immediately obvious. Archetype::ErrorBlock => Glyph { tile: 63, fg: Rgba8 { r: 255, g: 255, b: 0, a: 255 }, // yellow on red bg: Rgba8 { r: 255, g: 0, b: 0, a: 255 }, }, } } } impl TryFrom<&str> for Archetype { type Error = String; /// Parses an archetype by its map-file name. /// /// Returns an error for unrecognized names; the caller should substitute /// [`Archetype::ErrorBlock`] and log the error so the problem is visible. fn try_from(name: &str) -> Result { match name { "empty" => Ok(Archetype::Empty), "wall" => Ok(Archetype::Wall), // "object" is intentionally absent: objects are not valid palette // entries in map files. They live in [[objects]] with their own glyph. _ => Err(format!("unknown archetype: {name}")), } } } /// The archetypes available for placement in the editor, in display order. /// /// `ErrorBlock` is excluded — it is a sentinel for load errors, not a valid /// editing choice. pub const ALL_ARCHETYPES: &[Archetype] = &[Archetype::Empty, Archetype::Wall]; /// A scripted object placed on the board, loaded from a map file. /// /// `ObjectDef` represents a tile that has Rhai script attached to it. /// Scripts can respond to events like the player touching or shooting the /// tile. Objects are parsed from `[[objects]]` entries in `.toml` map files /// and stored on [`Board`]. /// /// Objects are rendered as an overlay on top of the board grid — the grid /// cell at `(x, y)` holds the background (floor) that is revealed if the /// object moves away. `glyph` is owned by the object itself and may be /// mutated by its Rhai script at runtime. /// /// Script text lives in [`Board::scripts`]; this struct holds only the name /// used to look it up. Two `ObjectDef`s with the same `script_name` share /// source text but will run with independent Rhai scopes when the scripting /// runtime is wired. /// /// **Not yet runtime-wired.** Objects are loaded and stored but their scripts /// are not yet executed. Scripting dispatch is a future feature. pub struct ObjectDef { /// Column of this object on the board (0-indexed). pub x: usize, /// Row of this object on the board (0-indexed). pub y: usize, /// Visual representation of this object. Owned by the object (not derived /// from the grid cell), so scripts can change tile, fg, and bg at runtime. pub glyph: Glyph, /// Whether the object blocks movement, of players or other objects pub passable: bool, /// Whether the object blocks line of sight / FOV for the player pub opaque: bool, /// Name of the Rhai script in [`Board::scripts`] that drives this object. /// `None` means this object has no script yet. pub script_name: Option, } impl ObjectDef { /// Returns the default glyph for a newly placed object: tile 63 (`?`) in yellow on black. pub fn default_glyph() -> Glyph { Glyph { tile: 63, fg: Rgba8 { r: 255, g: 255, b: 0, a: 255 }, // yellow bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, } } /// Creates a new object at `(x, y)` with default glyph and blocking behavior. /// /// Defaults: `passable = false`, `opaque = true`, no script. These match /// the serde defaults in the map file format so new objects round-trip correctly. pub fn new(x: usize, y: usize) -> Self { Self { x, y, glyph: Self::default_glyph(), passable: false, opaque: true, script_name: None, } } } /// A portal that teleports the player to a named entry point on another board. /// /// Portals are loaded from `[[portals]]` entries in `.toml` map files and /// stored on [`Board`]. When the player steps onto a portal's cell, they /// should be moved to `target_entry` on the board named by `target_map`. /// /// **Not yet runtime-wired.** Portal navigation (multi-board loading and /// switching) is a future feature. #[derive(Deserialize, Serialize)] pub struct PortalDef { /// Column of this portal on the board (0-indexed). pub x: usize, /// Row of this portal on the board (0-indexed). pub y: usize, /// File name (without extension) of the target board, e.g. `"cave"`. pub target_map: String, /// Named entry point on the target board where the player arrives. pub target_entry: String, } /// The player's current position on the board. /// /// The player is currently a special entity rendered on top of the board /// rather than being stored as a board cell. This is expected to change: /// the player will eventually become a scripted object that responds to /// input events, at which point this struct may be removed or made optional. /// See `ARCHITECTURE.md` for details. #[derive(Copy, Clone)] pub struct Player { /// Column position (0-indexed, increasing rightward). pub x: i32, /// Row position (0-indexed, increasing downward). pub y: i32, } /// The complete state of one game board (a single room or screen). /// /// `Board` is the central data structure of the engine, equivalent to a /// "board" in ZZT. It contains everything needed to represent and run one /// self-contained area of the game world: /// /// - A grid of cells, each with a visual representation ([`Glyph`]) and an [`Archetype`] /// - The current player position /// - Scripted objects and portals (loaded but not yet active) /// /// ## Cell storage /// /// Cells are stored as `(Glyph, Archetype)` tuples in a row-major `Vec`. /// Each cell directly owns its visual and behavioral class — there is no /// separate element palette or index indirection. Access cells with /// [`Board::get`] and [`Board::get_mut`] using `(x, y)` coordinates. /// Use [`Board::is_passable`] for collision checks. pub struct Board { /// Human-readable name for this board, loaded from the map file and round-tripped on save. pub name: String, /// Width of the board in cells. pub width: usize, /// Height of the board in cells. pub height: usize, /// Row-major grid of `(Glyph, Archetype)` pairs. Use [`Board::get`] to /// access by `(x, y)` coordinates. pub(crate) cells: Vec<(Glyph, Archetype)>, /// Current player position. See [`Player`] for caveats about its future. pub player: Player, /// Scripted objects on this board. Parsed from the map file; not yet active. pub objects: Vec, /// Portals on this board. Parsed from the map file; not yet active. pub portals: Vec, /// Optional font override for this board. When `None`, the app default is used. pub font: Option, /// Integer scale factor applied to tiles when rendering this board. 1 = natural size. pub zoom: u32, /// Named Rhai scripts available on this board: script name → source text. /// /// All script source lives here; [`ObjectDef`]s and [`Board::board_script_name`] /// reference entries by name. This means multiple objects can share the same /// source while each running instance gets its own Rhai scope at runtime. pub scripts: HashMap, /// Name of the board-level script in [`Board::scripts`], if any. /// /// A board script runs on the board as a whole (e.g. `on_enter`, `on_tick`) /// rather than being tied to a specific object cell. pub board_script_name: Option, } impl Board { /// Returns a reference to the cell at `(x, y)`. /// /// The cell is a `(Glyph, Archetype)` tuple. Panics if `x` or `y` are /// out of bounds. pub fn get(&self, x: usize, y: usize) -> &(Glyph, Archetype) { &self.cells[y * self.width + x] } /// Returns a mutable reference to the cell at `(x, y)`. /// /// Panics if `x` or `y` are out of bounds. #[allow(dead_code)] pub fn get_mut(&mut self, x: usize, y: usize) -> &mut (Glyph, Archetype) { &mut self.cells[y * self.width + x] } /// Returns a slice of all cells in row-major order. /// /// Useful for iterating over the full board (e.g. to collect unique glyphs). pub fn cells(&self) -> &[(Glyph, Archetype)] { &self.cells } /// Returns `true` if the cell at `(x, y)` allows entities to pass through. /// /// Objects block movement before the grid cell archetype is consulted. /// Panics if `x` or `y` are out of bounds. pub fn is_passable(&self, x: usize, y: usize) -> bool { // An object on this cell blocks if it's unpassable if let Some(obj) = self.object_at(x, y) && !obj.passable { false } else { self.get(x, y).1.behavior().passable } } /// Returns the index into [`Board::objects`] of the object at `(x, y)`, if any. pub fn object_index_at(&self, x: usize, y: usize) -> Option { self.objects.iter().position(|o| o.x == x && o.y == y) } /// Returns a borrow of the actual object at `(x, y)` if any pub fn object_at(&self, x: usize, y: usize) -> Option<&ObjectDef> { self.object_index_at(x, y).map(|idx| &self.objects[idx]) } } /// Holds the active game board and provides game-logic operations. /// /// `GameState` is the boundary between the engine (rendering, input) and the /// game data ([`Board`]). It owns the current board and exposes methods for /// actions that involve game rules — currently just player movement. /// /// As scripting and event dispatch are added, `GameState` will grow to handle /// routing input events to the appropriate Rhai scripts. pub struct GameState { /// The currently active board. pub board: Board, /// The in-game message log, oldest first (newest pushed at the end). pub log: Vec, } impl GameState { /// Creates a `GameState` from a pre-loaded [`Board`]. pub fn new(board: Board) -> Self { Self { board, log: Vec::new(), } } /// Appends a styled message to the log. pub fn log(&mut self, line: LogLine) { self.log.push(line); } /// Attempts to move the player by `(dx, dy)` cells. /// /// The move is ignored if the target cell is out of bounds or its behavior /// is not passable. No-ops silently (the caller does not need to check). pub fn try_move(&mut self, dx: i32, dy: i32) { let nx = self.board.player.x + dx; let ny = self.board.player.y + dy; if nx >= 0 && ny >= 0 { let nx = nx as usize; let ny = ny as usize; if nx < self.board.width && ny < self.board.height && self.board.is_passable(nx, ny) { self.board.player.x = nx as i32; self.board.player.y = ny as i32; } } } }