From 5146cc9bcc93da4929444863526b28e32c1fe656 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Tue, 14 Jul 2026 23:48:50 -0500 Subject: [PATCH] wip --- Cargo.lock | 3 + kiln-core/Cargo.toml | 2 +- kiln-core/src/api/object_info.rs | 4 +- kiln-core/src/api/queue.rs | 2 +- kiln-core/src/archetype.rs | 134 +++++++---------- kiln-core/src/board.rs | 139 +++++++++--------- kiln-core/src/game.rs | 40 ++--- kiln-core/src/glyph.rs | 3 +- kiln-core/src/layer.rs | 7 +- kiln-core/src/lib.rs | 1 + kiln-core/src/map_file.rs | 42 +++--- kiln-core/src/object_def.rs | 35 ++--- kiln-core/src/tests/actions.rs | 3 +- kiln-core/src/tests/game_portals.rs | 1 + kiln-core/src/tests/map_file/fill_sparse.rs | 12 +- .../src/tests/map_file/object_placement.rs | 2 +- kiln-core/src/tests/map_file/pushers.rs | 7 +- kiln-core/src/tests/map_file/transporters.rs | 5 +- kiln-core/src/tests/mod.rs | 3 +- kiln-core/src/tests/movement.rs | 39 ++--- kiln-core/src/tile.rs | 128 ++++++++++++++++ kiln-core/src/utils.rs | 19 ++- kiln-core/src/world.rs | 7 +- kiln-tui/src/editor.rs | 6 +- kiln-tui/src/editor_menu.rs | 8 +- 25 files changed, 380 insertions(+), 272 deletions(-) create mode 100644 kiln-core/src/tile.rs diff --git a/Cargo.lock b/Cargo.lock index c825cae..f8e583c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,9 @@ name = "color" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "serde", +] [[package]] name = "compact_str" diff --git a/kiln-core/Cargo.toml b/kiln-core/Cargo.toml index fe548dc..2378efc 100644 --- a/kiln-core/Cargo.toml +++ b/kiln-core/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -color = "0.3.3" +color = { version = "0.3.3", features = ["serde"] } doryen-fov = "0.1" rhai = "1" serde = { version = "1", features = ["derive"] } diff --git a/kiln-core/src/api/object_info.rs b/kiln-core/src/api/object_info.rs index d68f849..0347196 100644 --- a/kiln-core/src/api/object_info.rs +++ b/kiln-core/src/api/object_info.rs @@ -28,7 +28,7 @@ use crate::Direction; use crate::action::BoardAction; use crate::object_def::ObjectDef; use crate::script::Registerable; -use crate::utils::{LogSink, ObjectId}; +use crate::utils::{Behavior, LogSink, ObjectId}; /// A snapshot of one board object, returned by `Board.tagged`, `Board.named`, /// and `Board.get`. Passed by value — scripts read fields, not a live reference. @@ -116,7 +116,7 @@ impl Registerable for ObjectInfo { // me.light: the object's current emitted light radius in cells (0 = none). engine.register_get("light", |o: &mut ObjectInfo| -> i64 { match o.board.borrow().objects.get(&o.id) { - Some(ObjectDef { light, .. }) => *light as i64, + Some(ObjectDef { behavior: Behavior { glow, .. }, .. }) => *glow as i64, None => 0, } }); diff --git a/kiln-core/src/api/queue.rs b/kiln-core/src/api/queue.rs index e8f6ac6..659a084 100644 --- a/kiln-core/src/api/queue.rs +++ b/kiln-core/src/api/queue.rs @@ -11,7 +11,7 @@ use crate::script::Registerable; use crate::utils::{LogSink, ObjectId}; /// A single object's output queue. -#[derive(Clone)] +#[derive(Clone, Default)] pub struct ObjQueue(Rc>>); impl ObjQueue { diff --git a/kiln-core/src/archetype.rs b/kiln-core/src/archetype.rs index 1669361..527017e 100644 --- a/kiln-core/src/archetype.rs +++ b/kiln-core/src/archetype.rs @@ -2,6 +2,8 @@ use crate::glyph::Glyph; use crate::utils::{Behavior, Pushable}; use color::Rgba8; use crate::keys::KeyType; +use serde::{Serialize, Deserialize}; +use crate::tile::EnterResponse; /// Declares the set of script-backed archetype families. /// @@ -40,7 +42,7 @@ macro_rules! builtins { /// /// Add one entry to the `builtins!` invocation in `archetype.rs` and write /// `kiln-core/src/scripts/.rhai`. No other files need to change. - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Builtin { $( $variant ),+ } @@ -49,7 +51,7 @@ macro_rules! builtins { /// Returns `(family_variant, matched_alias)` for `name`, or `None` if `name` /// is not a known builtin keyword. The returned `&'static str` is the exact /// literal from the macro (always valid for a `Archetype::Builtin` field). - pub(crate) fn from_name(name: &str) -> Option<(Self, &'static str)> { + pub fn from_name(name: &str) -> Option<(Self, &'static str)> { match name { $( $( $name => Some((Builtin::$variant, $name)), )+ @@ -59,7 +61,7 @@ macro_rules! builtins { } /// Returns the uniform behavior shared by all aliases in this family. - pub(crate) fn behavior(self) -> Behavior { + pub fn behavior(self) -> Behavior { match self { $( Builtin::$variant => $behavior, )+ } @@ -69,7 +71,7 @@ macro_rules! builtins { /// so aliases within a family can differ in color (e.g. colored keys). /// Falls back to a transparent glyph for unrecognized aliases (shouldn't /// happen in practice since aliases are all from the macro). - pub(crate) fn default_glyph_for(self, alias: &str) -> Glyph { + pub fn default_glyph_for(self, alias: &str) -> Glyph { match alias { $( $( $name => $glyph, )+ @@ -79,7 +81,7 @@ macro_rules! builtins { } /// Returns the embedded Rhai source shared by all aliases in this family. - pub(crate) fn script(self) -> &'static str { + pub fn script(self) -> &'static str { match self { $( Builtin::$variant => $script, )+ } @@ -100,11 +102,11 @@ const fn g(tile: u32, r: u8, gr: u8, b: u8) -> Glyph { builtins! { Gem => ["gem" => g(4, 0x50, 0x50, 0xFF)] { - behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true }, + behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true, glow: 0 }, script: include_str!("scripts/gem.rhai"), }, Heart => ["heart" => g(3, 0xCC, 0x22, 0x22)] { - behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true }, + behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true, glow: 0 }, script: include_str!("scripts/heart.rhai"), }, Pusher => [ @@ -113,14 +115,14 @@ builtins! { "pusher_east" => g(16, 0xAA, 0xAA, 0xAA), "pusher_west" => g(17, 0xAA, 0xAA, 0xAA), ] { - behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false }, + behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false, glow: 0 }, script: include_str!("scripts/pusher.rhai"), }, Spinner => [ "spinner_cw" => g(47, 0xAA, 0xAA, 0xAA), "spinner_ccw" => g(92, 0xAA, 0xAA, 0xAA), ] { - behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false }, + behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false, glow: 0 }, script: include_str!("scripts/spinner.rhai"), }, // Solid, see-through (opaque: false), unpushable teleporters. Each direction's @@ -131,7 +133,7 @@ builtins! { "transporter_east" => g(41, 0x55, 0xFF, 0xFF), // ')' "transporter_west" => g(40, 0x55, 0xFF, 0xFF), // '(' ] { - behavior: Behavior { solid: true, opaque: false, pushable: Pushable::No, grab: false }, + behavior: Behavior { solid: true, opaque: false, pushable: Pushable::No, grab: false, glow: 0 }, script: include_str!("scripts/transporter.rhai"), }, Key => [ // TODO these should refer to the key colors in Keyring @@ -144,9 +146,28 @@ builtins! { "key_yellow" => KeyType::Yellow.glyph(), "key_white" => KeyType::White.glyph(), ] { - behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true }, + behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true, glow: 0 }, script: include_str!("scripts/key.rhai"), - } + }, + Wall => ["wall" => Glyph { + tile: 35, + fg: Rgba8 { r: 0x80, g: 0x80, b: 0x80, a: 255 }, + bg: Rgba8 { r: 0x60, g: 0x60, b: 0x60, a: 255 }}] { + behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false, glow: 0 }, + script: "" + }, + Crate => ["crate" => g(254, 0xaa, 0xaa, 0xaa)] { // CP437 ■ (small filled square) + behavior: Behavior { solid: true, opaque: true, pushable: Pushable::Any, grab: false, glow: 0 }, + script: "" + }, + HCrate => ["hcrate" => g(29, 0xaa, 0xaa, 0xaa)] { // CP437 ↔ (left-right arrow) — pushable east/west + behavior: Behavior { solid: true, opaque: true, pushable: Pushable::Horizontal, grab: false, glow: 0 }, + script: "" + }, + VCrate => ["vcrate" => g(18, 0xaa, 0xaa, 0xaa)] { // CP437 ↕ (up-down arrow) — pushable north/south + behavior: Behavior { solid: true, opaque: true, pushable: Pushable::Vertical, grab: false, glow: 0 }, + script: "" + }, } /// A class of board cell, encoding its default behavior and appearance. @@ -171,22 +192,17 @@ builtins! { /// /// [`ObjectDef`]: crate::object_def::ObjectDef /// [`Board::expand_builtin_archetypes`]: crate::board::Board::expand_builtin_archetypes -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum Archetype { /// An open cell; the player and other entities can pass through it. Empty, - /// A solid wall; impassable and opaque. - Wall, - /// A solid, pushable box. Walking into it shoves it one cell in the same - /// direction (cascading through a chain of crates) if there is open space. - Crate, - /// A crate that can only be pushed east/west (horizontal axis). Glyph ↔. - HCrate, - /// A crate that can only be pushed north/south (vertical axis). Glyph ↕. - VCrate, /// A wall-mounted torch: non-solid, non-opaque decorative terrain that emits /// warm light on a `dark` board (radius from [`Archetype::light`]). Glyph ☼. Torch, + /// Sentinel for map files that reference an unknown archetype name. + /// Renders as a yellow `?` on red to make the error visible in-game. + ErrorBlock, /// A script-backed archetype expanded from a map-file keyword. /// /// - `Builtin` is the family (e.g. `Builtin::Pusher`), which selects the @@ -196,10 +212,8 @@ pub enum Archetype { /// `BUILTIN_` tag on the expanded object. /// /// See [`Builtin`] and the [`builtins!`] invocation for the full registry. + #[serde(untagged)] Builtin(Builtin, &'static str), - /// 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 { @@ -212,30 +226,7 @@ impl Archetype { opaque: false, pushable: Pushable::No, grab: false, - }, - Archetype::Wall => Behavior { - solid: true, - opaque: true, - pushable: Pushable::No, - grab: false, - }, - Archetype::Crate => Behavior { - solid: true, - opaque: true, - pushable: Pushable::Any, - grab: false, - }, - Archetype::HCrate => Behavior { - solid: true, - opaque: true, - pushable: Pushable::Horizontal, - grab: false, - }, - Archetype::VCrate => Behavior { - solid: true, - opaque: true, - pushable: Pushable::Vertical, - grab: false, + glow: 0, }, // A torch you can walk past and see through; it only lights the room. Archetype::Torch => Behavior { @@ -243,12 +234,14 @@ impl Archetype { opaque: false, pushable: Pushable::No, grab: false, + glow: 6, }, Archetype::ErrorBlock => Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false, + glow: 0, }, } } @@ -258,10 +251,6 @@ impl Archetype { match self { Archetype::Builtin(_, alias) => alias, Archetype::Empty => "empty", - Archetype::Wall => "wall", - Archetype::Crate => "crate", - Archetype::HCrate => "hcrate", - Archetype::VCrate => "vcrate", Archetype::Torch => "torch", Archetype::ErrorBlock => "error_block", } @@ -292,26 +281,6 @@ impl Archetype { 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 }, - }, - Archetype::Crate => Glyph { - tile: 254, // CP437 ■ (small filled square) - fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 }, - bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, - }, - Archetype::HCrate => Glyph { - tile: 29, // CP437 ↔ (left-right arrow) — pushable east/west - fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 }, - bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, - }, - Archetype::VCrate => Glyph { - tile: 18, // CP437 ↕ (up-down arrow) — pushable north/south - fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 }, - bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, - }, Archetype::Torch => Glyph { tile: 15, // CP437 ☼ (sun) — a warm point of light fg: Rgba8 { r: 0xFF, g: 0xB0, b: 0x40, a: 255 }, // warm amber (also its light color) @@ -343,14 +312,17 @@ impl TryFrom<&str> for Archetype { } match name { "empty" => Ok(Archetype::Empty), - "wall" => Ok(Archetype::Wall), - "crate" => Ok(Archetype::Crate), - "hcrate" => Ok(Archetype::HCrate), - "vcrate" => Ok(Archetype::VCrate), "torch" => Ok(Archetype::Torch), // "object", "portal", "player" are intentionally absent: they are // meta-kinds handled by the layer builder, not Archetype variants. - _ => Err(format!("unknown archetype: {name}")), + _ => { + // is it a valid builtin? + if let Some((b, s)) = Builtin::from_name(name) { + Ok(Archetype::Builtin(b, s)) + } else { + Err(format!("unknown archetype: {name}")) + } + }, } } } @@ -359,12 +331,6 @@ impl TryFrom<&str> for Archetype { mod tests { use super::Archetype; - #[test] - fn directional_crate_default_glyph_tiles() { - assert_eq!(Archetype::HCrate.default_glyph().tile, 29); - assert_eq!(Archetype::VCrate.default_glyph().tile, 18); - } - #[test] fn builtin_names_glyphs_and_round_trip() { // All known aliases must parse, round-trip via name(), and give the right tile. diff --git a/kiln-core/src/board.rs b/kiln-core/src/board.rs index c4b3414..7d7a9b4 100644 --- a/kiln-core/src/board.rs +++ b/kiln-core/src/board.rs @@ -5,8 +5,9 @@ use crate::glyph::Glyph; use crate::log::LogLine; use crate::object_def::ObjectDef; use crate::utils::Direction; -use crate::utils::{Behavior, ObjectId, PlayerPos, PortalDef, Pushable, RegistryValue, Solid}; +use crate::utils::{ObjectId, PlayerPos, PortalDef, Pushable, RegistryValue, Solid}; use std::collections::{BTreeMap, HashMap, HashSet}; +use crate::tile::{EnterResponse, Sensor}; /// The result of [`Board::apply_shift`]: any error lines to log, plus the /// `(from, to)` cell relocations the shift actually performed. @@ -84,6 +85,8 @@ pub struct Board { /// Non-solid things placed off the main grid, drawn only where the grid cell is /// empty (see [`Decoration`]). Normally empty; populated by save files. pub(crate) decorations: Vec, + /// Non-solid things placed off the main grid, can't affect movement but see other hooks + pub sensors: Vec, /// Current player position on this board. See [`PlayerPos`] for caveats /// about its future. Game-global player *stats* live in [`crate::player::Player`]. pub player: PlayerPos, @@ -176,7 +179,7 @@ impl Board { // non-solid object (lets invisible trigger objects exist). let mut nonsolid: Option = None; for o in self.objects.values().filter(|o| o.x == x && o.y == y) { - if o.solid { + if o.behavior.solid { return o.glyph; } if nonsolid.is_none() && o.glyph.tile != 0 { @@ -245,17 +248,7 @@ impl Board { // A solid object shadows the cell it sits on; capture its behavior now. if let Some(id) = self.solid_object_id_at(x, y) { let obj = &self.objects[&id]; - let behavior = Behavior { - solid: obj.solid, - opaque: obj.opaque, - // ObjectDef stores pushability as a bool meaning "any direction". - pushable: if obj.pushable { - Pushable::Any - } else { - Pushable::No - }, - grab: obj.grab, - }; + let behavior = obj.behavior; return Some(Solid::object_at(x, y, id, behavior)); } // Otherwise the grid cell's terrain archetype may be solid (e.g. a wall). @@ -282,7 +275,7 @@ impl Board { /// Panics if `x` or `y` are out of bounds. pub fn is_opaque_at(&self, x: usize, y: usize) -> bool { self.get(x, y).1.behavior().opaque - || self.objects.values().any(|o| o.x == x && o.y == y && o.opaque) + || self.objects.values().any(|o| o.x == x && o.y == y && o.behavior.opaque) } /// Computes lighting + line-of-sight for the player on this board. @@ -327,8 +320,8 @@ impl Board { } // Light-emitting objects: color = their own glyph foreground. for o in self.objects.values() { - if o.light > 0 { - add_source(&mut lighting, o.x, o.y, o.light, color_to_rgb(o.glyph.fg)); + if o.behavior.glow > 0 { + add_source(&mut lighting, o.x, o.y, o.behavior.glow, color_to_rgb(o.glyph.fg)); } } // Glowing terrain (e.g. a `Torch` cell): color = the cell's glyph foreground. @@ -377,7 +370,16 @@ impl Board { let solid = self.solid_at(cx, cy)?; // A solid object — directly, or at the end of a crate chain — is the target. if let Some(id) = solid.object_id() { - return Some(id); + let obj = self.objects.get(&id)?; + let resp = EnterResponse::from(obj.behavior); + if let EnterResponse::Push(p) = resp && !p.allows(dir) { + // This object can't be pushed that way, so, this is a bump. If it's pushable, the + // default push behavior happens (it just gets pushed) + return Some(id); + } else if resp == EnterResponse::Block { + // It's just a wall, block everything + return Some(id); + } } // The player is never itself "bumped"; a wall (non-pushable terrain) stops // the chain with no object behind it. Only a pushable crate is walked @@ -528,7 +530,7 @@ impl Board { pub fn non_solid_object_ids_at(&self, x: usize, y: usize) -> Vec { self.objects .iter() - .filter(|(_, o)| o.x == x && o.y == y && !o.solid) + .filter(|(_, o)| o.x == x && o.y == y && !o.behavior.solid) .map(|(&id, _)| id) .collect() } @@ -536,7 +538,7 @@ impl Board { /// Returns a borrow of the actual object at `(x, y)` if any pub fn solid_object_id_at(&self, x: usize, y: usize) -> Option { self.objects.iter().find_map(|(&id, o)| { - if o.x == x && o.y == y && o.solid { + if o.x == x && o.y == y && o.behavior.solid { Some(id) } else { None @@ -551,7 +553,7 @@ impl Board { /// the object's `grab()` hook fires instead. pub fn grab_object_at(&self, x: usize, y: usize) -> Option { self.objects.iter().find_map(|(&id, o)| { - if o.x == x && o.y == y && o.solid && o.grab { + if o.x == x && o.y == y && EnterResponse::from(o.behavior) == EnterResponse::Grab { Some(id) } else { None @@ -645,13 +647,7 @@ impl Board { let beh = b.behavior(); let mut obj = ObjectDef::new(x, y); obj.glyph = glyph; - obj.solid = beh.solid; - obj.opaque = beh.opaque; - // Carry the archetype's pushability/grab onto the object (pushers and - // spinners are Pushable::No, so they stay unpushable; gems are pushable - // and grabbable). - obj.pushable = beh.pushable != crate::utils::Pushable::No; - obj.grab = beh.grab; + obj.behavior = beh; obj.builtin_script = Some(b.script()); // The compile-cache key and the BUILTIN_* tag are both derived from the // alias (e.g. "BUILTIN_pusher_east"), so each alias gets its own cached @@ -758,7 +754,7 @@ pub(crate) mod tests { use crate::floor::Floor; use crate::glyph::Glyph; use crate::object_def::ObjectDef; - use crate::utils::Direction; + use crate::utils::{Direction, Pushable}; use crate::utils::{ObjectId, PlayerPos}; use color::Rgba8; use std::collections::{BTreeMap, HashMap}; @@ -789,6 +785,7 @@ pub(crate) mod tests { grid: vec![(Glyph::transparent(), Archetype::Empty); w * h], floor: Floor::Blank, decorations: Vec::new(), + sensors: Vec::new(), player: PlayerPos { x: player.0, y: player.1, @@ -811,12 +808,12 @@ pub(crate) mod tests { /// Stamps a crate cell onto the grid. pub(crate) fn crate_at(board: &mut Board, x: usize, y: usize) { - *board.get_mut(x, y) = (Archetype::Crate.default_glyph(), Archetype::Crate); + *board.get_mut(x, y) = (Archetype::Builtin(Builtin::Crate, "crate").default_glyph(), Archetype::Builtin(Builtin::Crate, "crate")); } /// Stamps a wall cell onto the grid. pub(crate) fn wall_at(board: &mut Board, x: usize, y: usize) { - *board.get_mut(x, y) = (Archetype::Wall.default_glyph(), Archetype::Wall); + *board.get_mut(x, y) = (Builtin::Wall.default_glyph_for("wall"), Archetype::Builtin(Builtin::Wall, "wall")); } /// Stamps an arbitrary archetype cell onto the grid. @@ -824,7 +821,7 @@ pub(crate) mod tests { *board.get_mut(x, y) = (arch.default_glyph(), arch); } - #[test] + #[test] fn solid_at_reports_wall_object_and_empty() { let mut board = open_board(4, 1, (3, 0), vec![]); wall_at(&mut board, 1, 0); @@ -834,7 +831,7 @@ pub(crate) mod tests { assert!(board.is_passable(0, 0)); let wall = board.solid_at(1, 0).expect("a wall is solid"); - assert_eq!(wall.archetype(), Some(Archetype::Wall)); + assert_eq!(wall.archetype(), Some(Archetype::Builtin(Builtin::Wall, "wall"))); assert!(!board.is_passable(1, 0)); let obj = board.solid_at(2, 0).expect("an object is solid"); @@ -847,8 +844,8 @@ pub(crate) mod tests { fn grab_object_at_detects_a_gem() { // A grabbable gem object at (1,0); the player at (2,0). let mut gem = ObjectDef::new(1, 0); - gem.grab = true; - gem.pushable = true; + gem.behavior.grab = true; + gem.behavior.pushable = Pushable::Any; let board = open_board(3, 1, (2, 0), vec![gem]); // grab_object_at finds the gem on its own cell, nowhere else. @@ -859,7 +856,7 @@ pub(crate) mod tests { #[test] fn non_solid_object_does_not_block() { let mut obj = ObjectDef::new(1, 0); - obj.solid = false; + obj.behavior.solid = false; let board = open_board(3, 1, (0, 0), vec![obj]); assert!(board.solid_at(1, 0).is_none()); assert!(board.is_passable(1, 0)); @@ -891,7 +888,7 @@ pub(crate) mod tests { let mut board = open_board(3, 1, (0, 0), vec![]); crate_at(&mut board, 1, 0); assert!(board.can_push(1, 0, Direction::East)); - assert_eq!(board.get(1, 0).1, Archetype::Crate); // no mutation + assert_eq!(board.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // no mutation assert_eq!(board.get(2, 0).1, Archetype::Empty); let mut board = open_board(3, 1, (0, 0), vec![]); @@ -909,7 +906,7 @@ pub(crate) mod tests { // Crate with open space ahead: shiftable. crate_at(&mut board, 0, 0); assert!(board.can_shift(0, 0, Direction::East)); - assert_eq!(board.get(0, 0).1, Archetype::Crate); // read-only + assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // read-only // Crate with another pushable crate ahead: still shiftable (unlike can_push, // which would follow the chain to the wall and fail). @@ -937,8 +934,8 @@ pub(crate) mod tests { // The player is always a blocker for a shift — even a grab gem may not shift // onto it (grab now fires only on player movement, not on being shifted in). let mut gem = ObjectDef::new(0, 0); - gem.grab = true; - gem.pushable = true; + gem.behavior.grab = true; + gem.behavior.pushable = Pushable::Any; let board = open_board(2, 1, (1, 0), vec![gem]); assert!(!board.can_shift(0, 0, Direction::East)); @@ -956,7 +953,7 @@ pub(crate) mod tests { assert!(board.can_push(1, 0, Direction::East)); board.push(1, 0, Direction::East); assert_eq!(board.get(1, 0).1, Archetype::Empty); - assert_eq!(board.get(2, 0).1, Archetype::Crate); + assert_eq!(board.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); assert_eq!((board.player.x, board.player.y), (3, 0)); } @@ -968,7 +965,7 @@ pub(crate) mod tests { wall_at(&mut board, 3, 0); assert!(!board.can_push(1, 0, Direction::East)); board.push(1, 0, Direction::East); // no-op - assert_eq!(board.get(1, 0).1, Archetype::Crate); + assert_eq!(board.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); assert_eq!((board.player.x, board.player.y), (2, 0)); } @@ -995,7 +992,7 @@ pub(crate) mod tests { add_floor(&mut board, floor_glyph); wall_at(&mut board, 0, 0); // The wall (solid) draws over the floor; the empty cell reveals the floor. - assert_eq!(board.glyph_at(0, 0), Archetype::Wall.default_glyph()); + assert_eq!(board.glyph_at(0, 0), Builtin::Wall.default_glyph_for("wall")); assert_eq!(board.glyph_at(1, 0), floor_glyph); } @@ -1009,11 +1006,11 @@ pub(crate) mod tests { }; add_floor(&mut board, floor); - let wall = Archetype::Wall.default_glyph(); - board.place_archetype(1, 0, Archetype::Wall, wall); + let wall = Builtin::Wall.default_glyph_for("wall"); + board.place_archetype(1, 0, Archetype::Builtin(Builtin::Wall, "wall"), wall); // The wall landed on the grid; the floor attribute is untouched. - assert_eq!(board.get(1, 0), &(wall, Archetype::Wall)); + assert_eq!(board.get(1, 0), &(wall, Archetype::Builtin(Builtin::Wall, "wall"))); // The solid object that was there is gone. assert!(board.object_ids_at(1, 0).is_empty()); assert_eq!(board.glyph_at(1, 0), wall); @@ -1024,16 +1021,16 @@ pub(crate) mod tests { // A crate already occupies the grid at (1,0). let mut board = open_board(3, 1, (2, 0), vec![]); crate_at(&mut board, 1, 0); - let wall = Archetype::Wall.default_glyph(); - board.place_archetype(1, 0, Archetype::Wall, wall); - assert_eq!(board.get(1, 0), &(wall, Archetype::Wall)); + let wall = Builtin::Wall.default_glyph_for("wall"); + board.place_archetype(1, 0, Archetype::Builtin(Builtin::Wall, "wall"), wall); + assert_eq!(board.get(1, 0), &(wall, Archetype::Builtin(Builtin::Wall, "wall"))); } #[test] fn erase_removes_terrain_and_objects_but_keeps_floor() { // A fixed floor attribute, a wall on the grid, and a (non-solid) object at (1,0). let mut obj = ObjectDef::new(1, 0); - obj.solid = false; + obj.behavior.solid = false; let mut board = open_board(3, 1, (2, 0), vec![obj]); let floor = Glyph { tile: '.' as u32, @@ -1072,7 +1069,7 @@ pub(crate) mod tests { assert_eq!(board.get(0, 0).1, Archetype::Empty); let obj = board.objects.values().next().expect("spinner object"); assert_eq!((obj.x, obj.y), (0, 0)); - assert!(obj.solid); + assert!(obj.behavior.solid); assert!(obj.builtin_script.is_some()); assert!(obj.tags.contains("BUILTIN_spinner_cw")); @@ -1098,7 +1095,7 @@ pub(crate) mod tests { crate_at(&mut board, 0, 0); let errs = board.apply_shift(&[(0, 0), (9, 0)]); assert_eq!(errs.errors.len(), 1); - assert_eq!(board.get(0, 0).1, Archetype::Crate); // unchanged + assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // unchanged } #[test] @@ -1122,11 +1119,11 @@ pub(crate) mod tests { // Crate(3,0)→(4,0), Crate(4,0)→(0,0) wrap let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]); - assert_eq!(board.get(0, 0).1, Archetype::Crate); // wrapped from (4,0) - assert_eq!(board.get(1, 0).1, Archetype::Crate); // moved from (0,0) - assert_eq!(board.get(2, 0).1, Archetype::Wall); // blocked, immobile + assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // wrapped from (4,0) + assert_eq!(board.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (0,0) + assert_eq!(board.get(2, 0).1, Archetype::Builtin(Builtin::Wall, "wall")); // blocked, immobile assert_eq!(board.get(3, 0).1, Archetype::Empty); // cleared, crate moved - assert_eq!(board.get(4, 0).1, Archetype::Crate); // moved from (3,0) + assert_eq!(board.get(4, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (3,0) } #[test] @@ -1139,21 +1136,21 @@ pub(crate) mod tests { // Subcase A: HCrate moves { let mut board = open_board(4, 1, (3, 0), vec![]); - stamp(&mut board, 0, 0, Archetype::HCrate); + stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::HCrate, "hcrate")); crate_at(&mut board, 1, 0); // (2,0) empty let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0)]); assert_eq!(board.get(0, 0).1, Archetype::Empty); // HCrate moved out - assert_eq!(board.get(1, 0).1, Archetype::HCrate); // moved from (0,0) - assert_eq!(board.get(2, 0).1, Archetype::Crate); // moved from (1,0) + assert_eq!(board.get(1, 0).1, Archetype::Builtin(Builtin::HCrate, "hcrate")); // moved from (0,0) + assert_eq!(board.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (1,0) } // Subcase B: VCrate stays; Crate behind it still moves { let mut board = open_board(4, 1, (3, 0), vec![]); - stamp(&mut board, 0, 0, Archetype::VCrate); + stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::VCrate, "vcrate")); crate_at(&mut board, 1, 0); // (2,0) empty @@ -1163,9 +1160,9 @@ pub(crate) mod tests { // Crate at (1,0) is NOT in blocked, so it moves. let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0)]); - assert_eq!(board.get(0, 0).1, Archetype::VCrate); // immobile + assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::VCrate, "vcrate")); // immobile assert_eq!(board.get(1, 0).1, Archetype::Empty); // crate moved out - assert_eq!(board.get(2, 0).1, Archetype::Crate); // moved from (1,0) + assert_eq!(board.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (1,0) } } @@ -1178,22 +1175,22 @@ pub(crate) mod tests { // Subcase A: VCrate moves { let mut board = open_board(1, 4, (0, 3), vec![]); - stamp(&mut board, 0, 0, Archetype::VCrate); - stamp(&mut board, 0, 1, Archetype::Crate); + stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::VCrate, "vcrate")); + stamp(&mut board, 0, 1, Archetype::Builtin(Builtin::Crate, "crate")); // (0,2) empty let _errs = board.apply_shift(&[(0, 0), (0, 1), (0, 2)]); assert_eq!(board.get(0, 0).1, Archetype::Empty); // VCrate moved out - assert_eq!(board.get(0, 1).1, Archetype::VCrate); // moved from (0,0) - assert_eq!(board.get(0, 2).1, Archetype::Crate); // moved from (0,1) + assert_eq!(board.get(0, 1).1, Archetype::Builtin(Builtin::VCrate, "vcrate")); // moved from (0,0) + assert_eq!(board.get(0, 2).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (0,1) } // Subcase B: HCrate stays; Crate behind it still moves { let mut board = open_board(1, 4, (0, 3), vec![]); - stamp(&mut board, 0, 0, Archetype::HCrate); - stamp(&mut board, 0, 1, Archetype::Crate); + stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::HCrate, "hcrate")); + stamp(&mut board, 0, 1, Archetype::Builtin(Builtin::Crate, "crate")); // (0,2) empty // HCrate (idx 0): target (0,1), origin (0,0), vmove=true. @@ -1202,9 +1199,9 @@ pub(crate) mod tests { // Crate at (0,1) is NOT in blocked, so it moves. let _errs = board.apply_shift(&[(0, 0), (0, 1), (0, 2)]); - assert_eq!(board.get(0, 0).1, Archetype::HCrate); // immobile + assert_eq!(board.get(0, 0).1, Archetype::Builtin(Builtin::HCrate, "hcrate")); // immobile assert_eq!(board.get(0, 1).1, Archetype::Empty); // crate moved out - assert_eq!(board.get(0, 2).1, Archetype::Crate); // moved from (0,1) + assert_eq!(board.get(0, 2).1, Archetype::Builtin(Builtin::Crate, "crate")); // moved from (0,1) } } @@ -1261,8 +1258,8 @@ pub(crate) mod tests { let mut board = open_board(3, 1, (1, 0), vec![]); board.dark = true; let mut lamp = ObjectDef::new(1, 0); - lamp.solid = false; - lamp.light = 4; + lamp.behavior.solid = false; + lamp.behavior.glow = 4; lamp.glyph = Glyph { tile: 1, fg: Rgba8 { r: 255, g: 0, b: 0, a: 255 }, bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 } }; board.add_object(lamp); diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index 64fa0f6..fe728aa 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -313,7 +313,7 @@ impl GameState { } Action::SetLight(radius) => { if let Some(obj) = board.objects.get_mut(&ba.source) { - obj.light = radius; + obj.behavior.glow = radius; } } Action::SetTag { @@ -405,7 +405,7 @@ impl GameState { "teleport({target},{x},{y}): no such object" )), Some(obj) => { - let source_solid = obj.solid; + let source_solid = obj.behavior.solid; let (old_x, old_y) = (obj.x as i64, obj.y as i64); let blocked = source_solid && matches!( @@ -742,7 +742,7 @@ fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> StepOutcome { entered: Vec::new(), }; let (dx, dy): (i64, i64) = dir.into(); - let Some((ox, oy, solid)) = board.objects.get(&id).map(|o| (o.x, o.y, o.solid)) else { + let Some((ox, oy, solid)) = board.objects.get(&id).map(|o| (o.x, o.y, o.behavior.solid)) else { return out; }; let target = (ox as i64 + dx, oy as i64 + dy); @@ -805,7 +805,7 @@ mod tests { // ordinary solid here: the chain can't move (the player is backed against // the board edge), so nothing happens and the gem is not collected. let mut sobj = ObjectDef::new(0, 0); - sobj.solid = false; + sobj.behavior.solid = false; sobj.script_name = Some("s".to_string()); let mut board = open_board(3, 1, (2, 0), vec![sobj]); stamp(&mut board, 1, 0, Archetype::Builtin(Builtin::Gem, "gem")); @@ -822,14 +822,14 @@ mod tests { // No grab: the gem is untouched and the player never moved. assert_eq!(game.player.borrow().gems, 0); - assert!(game.board().objects.values().any(|o| o.grab)); + assert!(game.board().objects.values().any(|o| o.behavior.grab)); assert_eq!((game.board().player.x, game.board().player.y), (2, 0)); } /// Builds a `GameState` with one non-solid object running `src` as its script. fn game_with_object_script(board_w: usize, src: &str) -> GameState { let mut obj = ObjectDef::new(0, 0); - obj.solid = false; + obj.behavior.solid = false; obj.script_name = Some("s".to_string()); let mut board = open_board(board_w, 1, (board_w as i64 - 1, 0), vec![obj]); crate_at(&mut board, 2, 0); @@ -850,7 +850,7 @@ mod tests { game.tick(Duration::from_millis(16)); let b = game.board(); assert_eq!(b.get(2, 0).1, Archetype::Empty); - assert_eq!(b.get(3, 0).1, Archetype::Crate); + assert_eq!(b.get(3, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); } #[test] @@ -859,7 +859,7 @@ mod tests { // object sits at (0,0); the player at (3,0). passable should report only the // genuinely empty in-bounds cell. let mut obj = ObjectDef::new(0, 0); - obj.solid = false; + obj.behavior.solid = false; obj.script_name = Some("s".to_string()); let mut board = open_board(4, 1, (3, 0), vec![obj]); crate_at(&mut board, 2, 0); @@ -886,7 +886,7 @@ mod tests { // right after run_init (dt = 0) — under the old queued-Action behavior it // would have been stuck behind the delay and absent here. let mut obj = ObjectDef::new(0, 0); - obj.solid = false; + obj.behavior.solid = false; obj.script_name = Some("s".to_string()); let board = open_board(4, 1, (3, 0), vec![obj]); let src = "fn init(m) { delay(5.0); log(\"immediate\"); }"; @@ -916,7 +916,7 @@ mod tests { ccw: bool, ) -> GameState { let mut obj = ObjectDef::new(1, 1); - obj.solid = false; + obj.behavior.solid = false; obj.script_name = Some("spinner".to_string()); if ccw { obj.tags.insert("BUILTIN_spinner_ccw".to_string()); @@ -945,9 +945,9 @@ mod tests { let mut game = spinner_board(&[(1, 0), (2, 0)], &[(2, 1)]); game.tick(Duration::from_millis(16)); let b = game.board(); - assert_eq!(b.get(1, 0).1, Archetype::Crate); // N kept - assert_eq!(b.get(2, 0).1, Archetype::Crate); // NE kept (not destroyed) - assert_eq!(b.get(2, 1).1, Archetype::Wall); // wall kept + assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // N kept + assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // NE kept (not destroyed) + assert_eq!(b.get(2, 1).1, Archetype::Builtin(Builtin::Wall, "wall")); // wall kept } #[test] @@ -957,9 +957,9 @@ mod tests { let mut game = spinner_board(&[(0, 1), (0, 0), (1, 0)], &[]); game.tick(Duration::from_millis(16)); let b = game.board(); - assert_eq!(b.get(2, 0).1, Archetype::Crate); // NE: filled by N - assert_eq!(b.get(1, 0).1, Archetype::Crate); // N: filled by NW - assert_eq!(b.get(0, 0).1, Archetype::Crate); // NW: filled by W + assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // NE: filled by N + assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // N: filled by NW + assert_eq!(b.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // NW: filled by W assert_eq!(b.get(0, 1).1, Archetype::Empty); // W: vacated (hole moved here) } @@ -970,7 +970,7 @@ mod tests { let mut game = spinner_board_dir(&[(1, 0)], &[], true); game.tick(Duration::from_millis(16)); let b = game.board(); - assert_eq!(b.get(0, 0).1, Archetype::Crate); // NW: crate rotated counter-clockwise + assert_eq!(b.get(0, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); // NW: crate rotated counter-clockwise assert_eq!(b.get(2, 0).1, Archetype::Empty); // NE: untouched assert_eq!(b.get(1, 0).1, Archetype::Empty); // N: vacated } @@ -999,7 +999,7 @@ mod tests { #[test] fn set_key_gives_and_takes_keys() { let mut sobj = ObjectDef::new(0, 0); - sobj.solid = false; + sobj.behavior.solid = false; sobj.script_name = Some("s".to_string()); let board = open_board(2, 1, (1, 0), vec![sobj]); let scripts = HashMap::from([( @@ -1016,7 +1016,7 @@ mod tests { // A second script can take a key. let mut sobj2 = ObjectDef::new(0, 0); - sobj2.solid = false; + sobj2.behavior.solid = false; sobj2.script_name = Some("t".to_string()); let board2 = open_board(2, 1, (1, 0), vec![sobj2]); let scripts2 = HashMap::from([( @@ -1032,7 +1032,7 @@ mod tests { #[test] fn set_key_unknown_color_logs_error() { let mut sobj = ObjectDef::new(0, 0); - sobj.solid = false; + sobj.behavior.solid = false; sobj.script_name = Some("s".to_string()); let board = open_board(2, 1, (1, 0), vec![sobj]); let scripts = HashMap::from([( diff --git a/kiln-core/src/glyph.rs b/kiln-core/src/glyph.rs index 8e81f14..bbaa364 100644 --- a/kiln-core/src/glyph.rs +++ b/kiln-core/src/glyph.rs @@ -1,6 +1,7 @@ use color::Rgba8; use std::hash::{Hash, Hasher}; use rhai::Engine; +use serde::{Deserialize, Serialize}; use crate::script::Registerable; use crate::utils::LogSink; @@ -16,7 +17,7 @@ use crate::utils::LogSink; /// `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, Debug)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub struct Glyph { /// Tile index into the board's bitmap font (left-to-right, top-to-bottom). pub tile: u32, diff --git a/kiln-core/src/layer.rs b/kiln-core/src/layer.rs index abb3747..df28134 100644 --- a/kiln-core/src/layer.rs +++ b/kiln-core/src/layer.rs @@ -18,6 +18,7 @@ use crate::map_file::{TileIndex, parse_color}; use crate::object_def::ObjectDef; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use crate::utils::Pushable; /// Serde representation of the board `[grid]`: a char grid plus its palette. /// @@ -84,7 +85,7 @@ pub(crate) struct PaletteEntry { pub opaque: Option, /// Object pushability (defaults `false`). Only for `kind = "object"`. #[serde(default, skip_serializing_if = "Option::is_none")] - pub pushable: Option, + pub pushable: Option, /// Light radius in cells emitted on a dark board (defaults `0` = none). /// Only for `kind = "object"`. See [`ObjectDef::light`]. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -129,7 +130,7 @@ pub(crate) struct ObjectTemplate { pub glyph: Glyph, pub solid: bool, pub opaque: bool, - pub pushable: bool, + pub pushable: Pushable, /// Light radius in cells (0 = none); see [`ObjectDef::light`]. pub light: u32, pub script_name: Option, @@ -179,7 +180,7 @@ fn resolve_entry(ch: char, e: &PaletteEntry, errors: &mut Vec) -> Resol glyph: glyph_with_default(ObjectDef::default_glyph()), solid: e.solid.unwrap_or(true), opaque: e.opaque.unwrap_or(true), - pushable: e.pushable.unwrap_or(false), + pushable: e.pushable.unwrap_or(Pushable::No), light: e.light.unwrap_or(0), script_name: e.script_name.clone(), tags: e.tags.clone().unwrap_or_default(), diff --git a/kiln-core/src/lib.rs b/kiln-core/src/lib.rs index 4779470..c2d7b53 100644 --- a/kiln-core/src/lib.rs +++ b/kiln-core/src/lib.rs @@ -34,3 +34,4 @@ pub use utils::Direction; #[cfg(test)] mod tests; mod api; +pub mod tile; diff --git a/kiln-core/src/map_file.rs b/kiln-core/src/map_file.rs index 2119885..777a9ea 100644 --- a/kiln-core/src/map_file.rs +++ b/kiln-core/src/map_file.rs @@ -25,7 +25,7 @@ use crate::glyph::Glyph; use crate::layer::{GridData, PaletteEntry, Placement, build_grid}; use crate::log::LogLine; use crate::object_def::ObjectDef; -use crate::utils::{ObjectId, PlayerPos, PortalDef}; +use crate::utils::{Behavior, ObjectId, PlayerPos, PortalDef, Pushable}; /// The serde shell for one board in a `.toml` file: a header, the single grid, and /// the off-grid trigger/decoration lists. @@ -316,13 +316,16 @@ impl TryFrom for Board { x, y, glyph: t.glyph, - solid: t.solid, - opaque: t.opaque, - pushable: t.pushable, - light: t.light, - // Hand-placed objects are never grab targets; only expanded - // grab archetypes (e.g. gems) set this (see expand_builtin_archetypes). - grab: false, + behavior: Behavior { + solid: t.solid, + opaque: t.opaque, + pushable: t.pushable, + glow: t.light, + // Hand-placed objects are never grab targets; only expanded + // grab archetypes (e.g. gems) set this (see expand_builtin_archetypes). + // TODO this is wrong, obviously we want grabbable objects + grab: false, + }, script_name: t.script_name, // Script-backed archetypes are expanded after the board is built // (see `expand_builtin_archetypes` below), not via templates. @@ -355,11 +358,13 @@ impl TryFrom for Board { x: t.x, y: t.y, glyph: Glyph::transparent(), - solid: false, - opaque: false, - pushable: false, - light: 0, - grab: false, + behavior: Behavior { + solid: false, + opaque: false, + pushable: Pushable::No, + glow: 0, + grab: false, + }, script_name: Some(t.script_name), builtin_script: None, tags: t.tags.unwrap_or_default().into_iter().collect(), @@ -406,6 +411,7 @@ impl TryFrom for Board { grid, floor, decorations, + sensors: Vec::new(), player: PlayerPos { x: px as i64, y: py as i64, @@ -514,7 +520,7 @@ fn cell_entry(glyph: Glyph, arch: Archetype) -> PaletteEntry { /// Whether an object is a **trigger** — an invisible, non-solid, script-only object /// authored/serialized in `[[triggers]]` rather than the grid palette. fn is_trigger(o: &ObjectDef) -> bool { - !o.solid && o.glyph.tile == 0 && o.script_name.is_some() && o.builtin_script.is_none() + !o.behavior.solid && o.glyph.tile == 0 && o.script_name.is_some() && o.builtin_script.is_none() } /// Serializes `board`'s single grid (terrain + non-trigger objects + portals + @@ -563,10 +569,10 @@ fn grid_to_data(board: &Board) -> GridData { tile: Some(TileIndex::Num(o.glyph.tile)), fg: Some(color_to_hex(o.glyph.fg)), bg: Some(color_to_hex(o.glyph.bg)), - solid: Some(o.solid), - opaque: Some(o.opaque), - pushable: Some(o.pushable), - light: (o.light > 0).then_some(o.light), + solid: Some(o.behavior.solid), + opaque: Some(o.behavior.opaque), + pushable: Some(o.behavior.pushable), + light: (o.behavior.glow > 0).then_some(o.behavior.glow), script_name: o.script_name.clone(), tags: (!tags.is_empty()).then_some(tags), name: o.name.clone(), diff --git a/kiln-core/src/object_def.rs b/kiln-core/src/object_def.rs index 243a2b1..01a9d30 100644 --- a/kiln-core/src/object_def.rs +++ b/kiln-core/src/object_def.rs @@ -1,5 +1,5 @@ use crate::glyph::Glyph; -use crate::utils::ObjectId; +use crate::utils::{Behavior, ObjectId, Pushable}; use color::Rgba8; use std::collections::HashSet; use crate::api::queue::ObjQueue; @@ -37,25 +37,8 @@ pub struct ObjectDef { /// 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 / participates in movement. A solid object stops - /// a mover walking into it; at most one solid (object or grid archetype) may - /// occupy a cell. Inverse of the old `passable` flag. - pub solid: bool, - /// Whether the object blocks line of sight / FOV for the player - pub opaque: bool, - /// Whether the object can be shoved by a mover. Unused for now (no push - /// mechanic yet); defaults to `false`. - pub pushable: bool, - /// Whether walking into this object **grabs** it: the player moves onto the - /// cell and the object's `grab()` hook fires (the object is expected to - /// remove itself via `die()`). Set when a grab archetype (e.g. a gem) is - /// expanded into an object. Defaults to `false`. - pub grab: bool, - /// Light radius in cells that this object emits on a `dark` board (0 = no - /// light). The emitted *color* is this object's own glyph foreground color, - /// so a red glyph casts red light. See [`crate::fov`] for the lighting model. - /// Scripts can change it at runtime via `set_light(n)`. Defaults to `0`. - pub light: u32, + /// All the normal `Behavior` things about this object + pub behavior: Behavior, /// Compile-key of the Rhai script that drives this object: a name in /// [`World::scripts`](crate::world::World::scripts) for a hand-authored object, /// or a synthetic `BUILTIN_*` name set when a script-backed archetype is @@ -103,11 +86,13 @@ impl ObjectDef { x, y, glyph: Self::default_glyph(), - solid: true, - opaque: true, - pushable: false, - grab: false, - light: 0, + behavior: Behavior { + solid: true, + opaque: true, + pushable: Pushable::No, + grab: false, + glow: 0, + }, script_name: None, builtin_script: None, tags: HashSet::new(), diff --git a/kiln-core/src/tests/actions.rs b/kiln-core/src/tests/actions.rs index 3cf01cd..5beeddf 100644 --- a/kiln-core/src/tests/actions.rs +++ b/kiln-core/src/tests/actions.rs @@ -3,6 +3,7 @@ use crate::archetype::Archetype; use crate::board::tests::{crate_at, open_board, wall_at}; use crate::game::GameState; use std::time::Duration; +use crate::Builtin; #[test] fn move_command_relocates_the_source_object() { @@ -45,7 +46,7 @@ fn object_pushes_crate_on_init() { let b = game.board(); assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); assert_eq!(b.get(2, 0).1, Archetype::Empty); - assert_eq!(b.get(3, 0).1, Archetype::Crate); + assert_eq!(b.get(3, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); } #[test] diff --git a/kiln-core/src/tests/game_portals.rs b/kiln-core/src/tests/game_portals.rs index 0e52959..8332811 100644 --- a/kiln-core/src/tests/game_portals.rs +++ b/kiln-core/src/tests/game_portals.rs @@ -18,6 +18,7 @@ fn make_board(px: i64, py: i64, portals: Vec) -> Board { grid: vec![(Glyph::transparent(), Archetype::Empty); 9], floor: Floor::Blank, decorations: Vec::new(), + sensors: Vec::new(), player: PlayerPos { x: px, y: py }, objects: BTreeMap::new(), next_object_id: 1, diff --git a/kiln-core/src/tests/map_file/fill_sparse.rs b/kiln-core/src/tests/map_file/fill_sparse.rs index 9247507..f0baf35 100644 --- a/kiln-core/src/tests/map_file/fill_sparse.rs +++ b/kiln-core/src/tests/map_file/fill_sparse.rs @@ -19,12 +19,14 @@ palette = { "#" = { kind = "wall", tile = 35, fg = "#808080", bg = "#606060" } } let board = load_board(toml); for y in 0..2 { for x in 0..3 { - let expected = if (x, y) == (0, 0) { - Archetype::Empty // player won its fallback cell + if (x, y) == (0, 0) { // player won its fallback cell + assert!(board.solid_at(x, y).unwrap().player()); } else { - Archetype::Wall - }; - assert_eq!(board.get(x, y).1, expected, "cell ({x}, {y})"); + let obj = &board.objects[&board.object_ids_at(x, y)[0]]; + let tag = obj.tags.iter().next().unwrap(); + assert_eq!(tag, "BUILTIN_wall") + } + } } assert_eq!((board.player.x, board.player.y), (0, 0)); diff --git a/kiln-core/src/tests/map_file/object_placement.rs b/kiln-core/src/tests/map_file/object_placement.rs index 2ca4208..93fa337 100644 --- a/kiln-core/src/tests/map_file/object_placement.rs +++ b/kiln-core/src/tests/map_file/object_placement.rs @@ -77,6 +77,6 @@ fn non_solid_object_and_wall_coexist_in_separate_cells() { // never be authored onto a wall cell. A wall and a (separate-cell) non-solid // object both load fine. let board = load_board(&map_3x1_object("#G@", "G", "solid = false")); - assert_eq!(board.objects.len(), 1); + assert_eq!(board.objects.len(), 2); // The wall (for now) shows up as an object because it's a builtin, TODO assert!(board.is_valid()); } diff --git a/kiln-core/src/tests/map_file/pushers.rs b/kiln-core/src/tests/map_file/pushers.rs index 75a2a80..78e1ab7 100644 --- a/kiln-core/src/tests/map_file/pushers.rs +++ b/kiln-core/src/tests/map_file/pushers.rs @@ -7,6 +7,7 @@ use crate::game::GameState; use crate::map_file::MapFile; use crate::object_def::ObjectDef; use std::time::Duration; +use crate::{Board, Builtin}; /// Finds the pusher object on a board (by its built-in tag). fn pusher<'a>(board: &'a crate::board::Board, id: &mut u32) -> &'a ObjectDef { @@ -42,6 +43,10 @@ fn pusher_loads_as_a_tagged_scripted_solid_object() { assert!(!board.is_passable(0, 0), "pusher is solid"); } +fn is_tag(board: &Board, x: usize, y: usize, tag: &str) -> bool { + board.object_ids_at(x, y).iter().any(|id| board.objects[id].tags.contains(tag)) +} + #[test] fn pusher_advances_and_shoves_a_crate() { // Pusher at (0,0), crate at (1,0), the player parked at the east edge. @@ -74,7 +79,7 @@ fn pusher_advances_and_shoves_a_crate() { "crate left its start cell" ); assert!( - (2..b.width).any(|x| b.get(x, 0).1 == Archetype::Crate), + (2..b.width).any(|x| is_tag(&b, x, 0, "BUILTIN_crate")), "crate was shoved east" ); } diff --git a/kiln-core/src/tests/map_file/transporters.rs b/kiln-core/src/tests/map_file/transporters.rs index 14ad5c8..8a904b5 100644 --- a/kiln-core/src/tests/map_file/transporters.rs +++ b/kiln-core/src/tests/map_file/transporters.rs @@ -10,7 +10,7 @@ use crate::utils::Direction; use std::time::Duration; #[test] -fn transporter_loads_as_a_tagged_scripted_solid_object() { +fn transporter_loads_as_a_tagged_scripted_object() { let board = load_board(&map( 3, 1, @@ -26,9 +26,6 @@ fn transporter_loads_as_a_tagged_scripted_solid_object() { .expect("transporter object"); assert_eq!((obj.x, obj.y), (1, 0)); assert!(obj.builtin_script.is_some(), "carries the embedded script"); - assert!(obj.solid, "transporter is solid"); - assert!(!obj.opaque, "transporter is see-through"); - assert!(!obj.pushable, "transporter cannot be pushed"); } #[test] diff --git a/kiln-core/src/tests/mod.rs b/kiln-core/src/tests/mod.rs index 445e566..7110cc7 100644 --- a/kiln-core/src/tests/mod.rs +++ b/kiln-core/src/tests/mod.rs @@ -32,6 +32,7 @@ fn board_with_object( grid: vec![(Glyph::transparent(), Archetype::Empty)], floor: Floor::Blank, decorations: Vec::new(), + sensors: Vec::new(), player: PlayerPos { x: 0, y: 0 }, objects: BTreeMap::from([(1, object)]), next_object_id: 2, @@ -64,7 +65,7 @@ fn scripted_object(x: usize, y: usize, script: &str) -> ObjectDef { /// kind that receives an `enter` hook when a solid relocates onto its cell. fn nonsolid_object(x: usize, y: usize, script: &str) -> ObjectDef { let mut o = scripted_object(x, y, script); - o.solid = false; + o.behavior.solid = false; o } diff --git a/kiln-core/src/tests/movement.rs b/kiln-core/src/tests/movement.rs index a8aa99d..55a7507 100644 --- a/kiln-core/src/tests/movement.rs +++ b/kiln-core/src/tests/movement.rs @@ -3,8 +3,9 @@ use crate::board::tests::{add_floor, crate_at, open_board, stamp, wall_at}; use crate::game::GameState; use crate::glyph::Glyph; use crate::object_def::ObjectDef; -use crate::utils::Direction; +use crate::utils::{Direction, Pushable}; use color::Rgba8; +use crate::Builtin; #[test] fn pushing_a_crate_reveals_the_floor_underneath() { @@ -32,7 +33,7 @@ fn pushing_a_crate_reveals_the_floor_underneath() { let mut game = GameState::new(board); game.try_move(Direction::East); let b = game.board(); - assert_eq!(b.get(2, 0).1, Archetype::Crate); + assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); assert_eq!(b.get(1, 0).1, Archetype::Empty); assert_eq!(b.glyph_at(0, 0), floor_glyph); } @@ -46,7 +47,7 @@ fn player_pushes_single_crate() { let b = game.board(); assert_eq!((b.player.x, b.player.y), (1, 0)); assert_eq!(b.get(1, 0).1, Archetype::Empty); - assert_eq!(b.get(2, 0).1, Archetype::Crate); + assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); } #[test] @@ -58,8 +59,8 @@ fn push_blocked_by_wall_moves_nothing() { game.try_move(Direction::East); let b = game.board(); assert_eq!((b.player.x, b.player.y), (0, 0)); - assert_eq!(b.get(1, 0).1, Archetype::Crate); - assert_eq!(b.get(2, 0).1, Archetype::Wall); + assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); + assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Wall, "wall")); } #[test] @@ -71,7 +72,7 @@ fn push_blocked_by_edge_moves_nothing() { game.try_move(Direction::East); let b = game.board(); assert_eq!((b.player.x, b.player.y), (0, 0)); - assert_eq!(b.get(1, 0).1, Archetype::Crate); + assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); } #[test] @@ -84,8 +85,8 @@ fn cascade_pushes_two_crates() { let b = game.board(); assert_eq!((b.player.x, b.player.y), (1, 0)); assert_eq!(b.get(1, 0).1, Archetype::Empty); - assert_eq!(b.get(2, 0).1, Archetype::Crate); - assert_eq!(b.get(3, 0).1, Archetype::Crate); + assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); + assert_eq!(b.get(3, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); } #[test] @@ -98,15 +99,15 @@ fn cascade_blocked_by_wall_moves_nothing() { game.try_move(Direction::East); let b = game.board(); assert_eq!((b.player.x, b.player.y), (0, 0)); - assert_eq!(b.get(1, 0).1, Archetype::Crate); - assert_eq!(b.get(2, 0).1, Archetype::Crate); + assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); + assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::Crate, "crate")); } #[test] fn player_pushes_pushable_solid_object() { // A solid, pushable object is shoved exactly like a crate. let mut obj = ObjectDef::new(1, 0); - obj.pushable = true; + obj.behavior.pushable = Pushable::Any; let board = open_board(4, 1, (0, 0), vec![obj]); let mut game = GameState::new(board); game.try_move(Direction::East); @@ -119,44 +120,44 @@ fn player_pushes_pushable_solid_object() { fn hcrate_pushes_east_but_not_north() { // Pushing east: the HCrate slides. let mut board = open_board(4, 1, (0, 0), vec![]); - stamp(&mut board, 1, 0, Archetype::HCrate); + stamp(&mut board, 1, 0, Archetype::Builtin(Builtin::HCrate, "hcrate")); let mut game = GameState::new(board); game.try_move(Direction::East); let b = game.board(); assert_eq!((b.player.x, b.player.y), (1, 0)); assert_eq!(b.get(1, 0).1, Archetype::Empty); - assert_eq!(b.get(2, 0).1, Archetype::HCrate); + assert_eq!(b.get(2, 0).1, Archetype::Builtin(Builtin::HCrate, "hcrate")); drop(b); // Pushing north into an HCrate: blocked, nothing moves. let mut board = open_board(1, 4, (0, 3), vec![]); - stamp(&mut board, 0, 2, Archetype::HCrate); + stamp(&mut board, 0, 2, Archetype::Builtin(Builtin::HCrate, "hcrate")); let mut game = GameState::new(board); game.try_move(Direction::North); let b = game.board(); assert_eq!((b.player.x, b.player.y), (0, 3)); - assert_eq!(b.get(0, 2).1, Archetype::HCrate); + assert_eq!(b.get(0, 2).1, Archetype::Builtin(Builtin::HCrate, "hcrate")); } #[test] fn vcrate_pushes_north_but_not_east() { // Pushing north: the VCrate slides. let mut board = open_board(1, 4, (0, 3), vec![]); - stamp(&mut board, 0, 2, Archetype::VCrate); + stamp(&mut board, 0, 2, Archetype::Builtin(Builtin::VCrate, "vcrate")); let mut game = GameState::new(board); game.try_move(Direction::North); let b = game.board(); assert_eq!((b.player.x, b.player.y), (0, 2)); assert_eq!(b.get(0, 2).1, Archetype::Empty); - assert_eq!(b.get(0, 1).1, Archetype::VCrate); + assert_eq!(b.get(0, 1).1, Archetype::Builtin(Builtin::VCrate, "vcrate")); drop(b); // Pushing east into a VCrate: blocked, nothing moves. let mut board = open_board(4, 1, (0, 0), vec![]); - stamp(&mut board, 1, 0, Archetype::VCrate); + stamp(&mut board, 1, 0, Archetype::Builtin(Builtin::VCrate, "vcrate")); let mut game = GameState::new(board); game.try_move(Direction::East); let b = game.board(); assert_eq!((b.player.x, b.player.y), (0, 0)); - assert_eq!(b.get(1, 0).1, Archetype::VCrate); + assert_eq!(b.get(1, 0).1, Archetype::Builtin(Builtin::VCrate, "vcrate")); } diff --git a/kiln-core/src/tile.rs b/kiln-core/src/tile.rs new file mode 100644 index 0000000..0a9d138 --- /dev/null +++ b/kiln-core/src/tile.rs @@ -0,0 +1,128 @@ +use std::collections::HashSet; +use serde::{Deserialize, Serialize}; +use crate::api::queue::ObjQueue; +use crate::glyph::Glyph; +use crate::object_def::ObjectDef; +use crate::utils::{Behavior, ObjectId, Pushable}; + +/// The various ways that a tile might respond to another tile trying to move on top of it +#[derive(Serialize, Deserialize, PartialEq, Copy, Clone, Debug)] +pub enum EnterResponse { + /// Flat denial: don't let the move happen, block it. + Block, + /// Call the `grab` hook and then remove this object. + /// TODO will do something different once inventory exists as a concept + Grab, + /// Attempt to exit the cell ourselves, the opposite direction. This of course recurses; if our move + /// is denied then this is the same as a block. + Push(Pushable), + /// Call an `enter` hook and do something. The hook is responsible for resolving the conflict: + /// placing the moving-object somewhere else, moving ourselves somewhere else, or whatever. + /// It should return `true` or `false`: this return value is passed back up a `Push` chain to + /// the original moving object, determining whether the moves should happen. + /// Example: a teleporter moves anything that enters it to another spot, if it's unblocked. It + /// will check if the target cell is open and move the object there if it is, returning true. + /// If you push a crate into it, it returning true means you should move (into the space the crate + /// left behind); it returning false means the crate wasn't teleported so your move is blocked + /// also. + /// The hook return value is only used for _other_ things in the chain: regardless of what's returned, + /// the engine won't touch the crate or the teleporter; the hook is responsible for handling the + /// actual collision. A teleporter that teleports the crate and returns false will leave an empty + /// space behind (the player won't move into it because it returned false); a teleporter that + /// returns true without teleporting the crate, the crate will be destroyed by the player moving + /// on top of it. + Hook, + /// Swap places with whatever moved on top of you + Swap, + /// Get overwritten and destroyed by whatever moved on top of us. Right now, equivalent to `Grab` + /// if the hook does nothing (but in the future `Grab` will have other builtin behavior) + Squish +} + +impl From for EnterResponse { + fn from(behavior: Behavior) -> EnterResponse { + if behavior.grab { + EnterResponse::Grab + } else if behavior.pushable != Pushable::No { + EnterResponse::Push(behavior.pushable) + } else if behavior.solid { + EnterResponse::Block + } else { + EnterResponse::Squish + } + } +} + +/// Where `Sensor`s are drawn, in relation to the grid: +/// - `Above` is above everything, including the player. If the glyph has a nonzero tile, and it's +/// visible / lit, it will be drawn. +/// - `Below` is below the grid but above the floor. A nonzero-tile-glyph will be drawn only if +/// there's not a grid-thing in the same cell. +#[derive(Serialize, Deserialize, PartialEq, Copy, Clone, Debug)] +#[serde(rename = "lowercase")] +pub enum DrawLayer { Above, Below } + +/// How a thing interacts with the lighting and visibility models +#[derive(Serialize, Deserialize, PartialEq, Copy, Clone, Debug)] +#[serde(rename = "lowercase")] +pub struct Optics { + /// Opaque things block field of view + pub opaque: bool, + /// The brightness (in number of cells' radius) of light this + /// emits. It will emit light in the color of the foreground of its glyph. + /// A cell is visible if: + /// - the level isn't dark, in which case lighting isn't calculated + /// - the cell is within the field of view + /// - the cell is within the glow radius of at least one light source + /// + /// Its color will be tinted by the light sources illuminating it. + pub glow: u32, +} + +/// Everything an object-or-sensor needs to have a Rhai script attached. +/// TODO clean this up some, especially the script_name-vs-builtin_script dichotomy +#[derive(Clone, Default)] +pub struct Scripting { + /// Compile-key of the Rhai script that drives this object: a name in + /// [`World::scripts`](crate::world::World::scripts) for a hand-authored object, + /// or a synthetic `BUILTIN_*` name set when a script-backed archetype is + /// expanded (see [`builtin_script`](ObjectDef::builtin_script)). `None` means + /// this object has no script yet. + pub script_name: Option, + /// Embedded built-in script source, set when a script-backed archetype (e.g. a + /// `pusher_*` or `gem`) is expanded into an object at load time (see + /// [`crate::builtin_scripts`]). When set, this is the object's script *source* + /// (its compile-key is the synthetic `BUILTIN_*` [`script_name`](ObjectDef::script_name) + /// the same expansion assigns). Not part of the map file — it is regenerated + /// from the archetype on load. + pub builtin_script: Option<&'static str>, + /// Open-ended string labels for this object. Serialized as a TOML array; + /// not subject to any rate limit — mutations take effect immediately after + /// the frame's action queue is drained. + pub tags: HashSet, + /// Optional unique human-readable name for this object. `None` if unnamed. + /// Names are validated for uniqueness at map-load time; a duplicate name is + /// cleared to `None` (the object survives but becomes anonymous). + pub name: Option, + /// The output queue of actions for this object + pub queue: ObjQueue, +} + +/// A `Sensor` is some kind of object that exists alongside the map: it's drawn on the board but can't +/// affect board movement, as it doesn't live in the grid. +#[derive(Serialize, Deserialize, Clone)] +pub struct Sensor { + /// The unique ID of this sensor + pub id: ObjectId, + /// Where it is on the board: x coord + pub x: usize, + /// Where it is on the board: y coord + pub y: usize, + /// What it looks like + pub glyph: Glyph, + /// Sensors interact with FOV and lighting + pub optics: Optics, + /// Sensors have scripting ability + #[serde(skip)] + pub scripting: Scripting, +} \ No newline at end of file diff --git a/kiln-core/src/utils.rs b/kiln-core/src/utils.rs index f6e3a22..8b6f822 100644 --- a/kiln-core/src/utils.rs +++ b/kiln-core/src/utils.rs @@ -5,15 +5,18 @@ use crate::archetype::Archetype; use crate::glyph::Glyph; use color::Rgba8; use rhai::Dynamic; +use serde::{Deserialize, Serialize}; use crate::Board; use crate::log::LogLine; +use crate::tile::EnterResponse; /// Which directions a solid may be pushed in. /// /// Only meaningful for `solid` cells (a non-solid cell is passable, so nothing is /// ever pushed into it). `Crate` is [`Pushable::Any`]; the directional crates /// constrain pushes to one axis. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum Pushable { /// Cannot be pushed. No, @@ -61,6 +64,9 @@ pub struct Behavior { /// the thing is expected to remove itself via `die()`). Only meaningful when /// `solid`. The same grab fires if the thing is pushed into the player. pub grab: bool, + /// The radius of light this emits, if any. Light _color_ is determined by the + /// glyph foreground color of whatever it is. + pub glow: u32, } /// A stable, unique identifier for a board object. @@ -133,6 +139,7 @@ impl Solid { opaque: true, pushable: Pushable::Any, grab: false, + glow: 6 // TODO player should not inherently glow, but glowing inventory items need to wait for inventory }, } } @@ -185,18 +192,22 @@ impl Solid { /// Which directions the occupant may be pushed (the player → [`Pushable::Any`]). pub fn pushable(&self) -> Pushable { - self.behavior.pushable + if let EnterResponse::Push(p) = EnterResponse::from(self.behavior) { + p + } else { + Pushable::No + } } /// Whether this occupant is a **grab** thing (a gem-like solid the player /// collects on contact). The player is never grabbable. pub fn grab(&self) -> bool { - self.behavior.grab + EnterResponse::from(self.behavior) == EnterResponse::Grab } /// Whether this occupant blocks movement (always true for a `Solid`). pub fn solid(&self) -> bool { - self.behavior.solid + EnterResponse::from(self.behavior) == EnterResponse::Block } /// Whether this occupant blocks line of sight. diff --git a/kiln-core/src/world.rs b/kiln-core/src/world.rs index d9d0f57..3cd3511 100644 --- a/kiln-core/src/world.rs +++ b/kiln-core/src/world.rs @@ -134,6 +134,7 @@ mod tests { use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; + use crate::Builtin; /// A `deep_clone`d world owns independent boards: mutating the copy must leave the /// original board untouched (the property the editor's playtest relies on). @@ -155,14 +156,14 @@ mod tests { copy.boards["start"].borrow_mut().place_archetype( 1, 0, - Archetype::Wall, - Archetype::Wall.default_glyph(), + Archetype::Builtin(Builtin::Wall, "wall"), + Builtin::Wall.default_glyph_for("wall"), ); // The copy changed; the original is still empty at that cell. assert_eq!( copy.boards["start"].borrow().get(1, 0).1, - Archetype::Wall + Archetype::Builtin(Builtin::Wall, "wall") ); assert_eq!( world.boards["start"].borrow().get(1, 0).1, diff --git a/kiln-tui/src/editor.rs b/kiln-tui/src/editor.rs index f7fa53f..934768b 100644 --- a/kiln-tui/src/editor.rs +++ b/kiln-tui/src/editor.rs @@ -20,7 +20,7 @@ use kiln_core::game::GameState; use kiln_core::glyph::Glyph; use kiln_core::log::LogLine; use kiln_core::world::World; -use kiln_core::{Archetype, Board}; +use kiln_core::{Archetype, Board, Builtin}; use kiln_ui::code_editor::{CodeEditor, CodeEditorOutcome}; use kiln_ui::dialog::{Dialog, DialogResult, ListDialogResponse}; use ratatui::Frame; @@ -112,8 +112,8 @@ impl EditorState { code_editor: None, glyph_dialog: None, cursor, - current_archetype: Archetype::Wall, - current_glyph: Archetype::Wall.default_glyph(), + current_archetype: Archetype::Builtin(Builtin::Wall, "wall"), + current_glyph: Builtin::Wall.default_glyph_for("wall"), draw_mode: false, sidebar_width: DEFAULT_SIDEBAR_WIDTH, blink_on: true, diff --git a/kiln-tui/src/editor_menu.rs b/kiln-tui/src/editor_menu.rs index 37b23cd..a7e8fa9 100644 --- a/kiln-tui/src/editor_menu.rs +++ b/kiln-tui/src/editor_menu.rs @@ -77,10 +77,10 @@ impl MenuLevel { MenuEntry::item('c', "Custom glyph [TODO]", |_| {}), ], MenuLevel::Terrain => vec![ - MenuEntry::item('w', "Wall", |ed| ed.set_current(Archetype::Wall)), - MenuEntry::item('c', "■ Crate", |ed| ed.set_current(Archetype::Crate)), - MenuEntry::item('v', "↕ Crate", |ed| ed.set_current(Archetype::VCrate)), - MenuEntry::item('h', "↔ Crate", |ed| ed.set_current(Archetype::HCrate)), + MenuEntry::item('w', "Wall", |ed| ed.set_current(Archetype::Builtin(Builtin::Wall, "wall"))), + MenuEntry::item('c', "■ Crate", |ed| ed.set_current(Archetype::Builtin(Builtin::Crate, "crate"))), + MenuEntry::item('v', "↕ Crate", |ed| ed.set_current(Archetype::Builtin(Builtin::VCrate, "vcrate"))), + MenuEntry::item('h', "↔ Crate", |ed| ed.set_current(Archetype::Builtin(Builtin::HCrate, "hcrate"))), ], MenuLevel::Machines => vec![ MenuEntry::item('p', "Pushers...", |ed| ed.menu.push(MenuLevel::Pushers)),