use std::collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use crate::api::queue::ObjQueue; use crate::{Builtin, Direction}; use crate::builtin::BUILTIN_SOURCES; use crate::floor::{Floor, FloorBiome}; use crate::glyph::Glyph; use crate::object_def::ObjectDef; use crate::utils::{ObjectId, Pushable}; use crate::utils::Pushable::No; /// 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)] #[serde(rename_all = "lowercase")] pub enum EnterResponse { /// Flat denial: don't let the move happen, block it. Block, /// If the player moves, call the `grab` hook and then remove this object. Anything else, act as /// `Pushable::Any`. /// 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, as long as it was the player. Anything else, /// act as `Pushable::Any`. 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 EnterResponse { /// Returns whether this object will transmit a push of the given direction through it: /// - `Push(p)`, if p allows that direction /// - `Grab`, because grabbable things act like push if something pushes them /// - `Swap`, same reason pub fn transmits_push(self, dir: Direction) -> bool { match self { EnterResponse::Grab | EnterResponse::Swap => true, EnterResponse::Push(p) if p.allows(dir) => true, _ => false } } /// Returns whether this object will sense a bump from the given direction: /// - `Push(p)`, if p disallows that direction /// - `Block`, because nothing can move through it /// - `Hook`, because it's obligated to handle the entry and not pass it on /// - but nothing else pub fn bumpable(self, dir: Direction) -> bool { match self { EnterResponse::Block | EnterResponse::Hook => true, EnterResponse::Push(p) if !p.allows(dir) => true, _ => false } } } /// 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 #[serde(default = "default_as_true")] 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. #[serde(default)] pub glow: u32, } impl Default for Optics { fn default() -> Self { Self { opaque: false, glow: 0, } } } const fn default_as_true() -> bool { true } const fn default_as_below() -> DrawLayer { DrawLayer::Below } #[derive(Hash, PartialEq, Eq, Clone, Debug, Default)] pub enum ScriptKey { #[default] None, World(String), Builtin(&'static str), } impl ScriptKey { pub fn name(&self) -> &str { match self { ScriptKey::None => "", ScriptKey::World(name) => name.as_str(), ScriptKey::Builtin(name) => *name } } /// Attempt to find and return the source for the given script key: /// - For `None`, return Ok(None) since there's not a script to find /// - For `World`, return either Ok(Some(&str)) or Err if it's not in the given hashmap /// - For `Builtin`, return Ok(Some(&str)) assuming the builtin is a valid name (Err in the unlikely case...) pub fn source<'a>(&self, sources: &'a HashMap) -> Result, String> { match self { ScriptKey::None => Ok(None), ScriptKey::World(name) => sources.get(name).map(String::as_str) .map_or(Err(format!("unknown script '{name}'")), |s| Ok(Some(s))), key @ ScriptKey::Builtin(name) => BUILTIN_SOURCES.get(key) .map_or(Err(format!("No builtin script '{name}'")), |s| Ok(Some(*s))), } } } impl From> for ScriptKey { fn from(s: Option) -> Self { s.map_or(Self::None, |s| Self::World(s)) } } /// 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 ScriptAttributes { /// Stable identity assigned by [`crate::board::Board::add_object`]. /// `0` is the sentinel meaning "not yet inserted into a board". /// Real ids start at 1 and never change after assignment. /// Not serialized — the id is stamped on insert, not stored in map files. pub id: ObjectId, /// 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, /// How this object affects FOV and lighting pub optics: Optics, /// 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: ScriptKey, /// 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. /// 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(Clone)] pub struct Sensor { /// Where it is on the board: x coord pub x: usize, /// Where it is on the board: y coord pub y: usize, /// Whether this is drawn above or below the grid pub draw_layer: DrawLayer, /// Sensors have scripting ability pub scripting: ScriptAttributes, } /// The serialized representation of a Sensor. Can be turned into a Sensor, or vice versa #[derive(Serialize, Deserialize, Clone)] pub struct SensorSpec { pub x: usize, pub y: usize, #[serde(default, skip_serializing_if = "Option::is_none")] pub script: Option, #[serde(default, skip_serializing_if = "Glyph::is_transparent")] pub glyph: Glyph, #[serde(flatten, default)] pub optics: Optics, #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tags: Vec, #[serde(default = "default_as_below")] pub draw_layer: DrawLayer, } impl SensorSpec { pub fn into_sensor(self, next_object_id: &mut ObjectId) -> Sensor { let id = *next_object_id; *next_object_id += 1; Sensor { x: self.x, y: self.y, draw_layer: self.draw_layer, scripting: ScriptAttributes { id, glyph: self.glyph, optics: self.optics, name: self.name, tags: self.tags.into_iter().collect(), script_name: self.script.into(), ..Default::default() } } } } pub trait Hookable { fn scriptable(&self) -> &ScriptAttributes; fn location(&self) -> (usize, usize); fn id(&self) -> ObjectId { self.scriptable().id } fn glyph(&self) -> Glyph { self.scriptable().glyph } fn optics(&self) -> Optics { self.scriptable().optics } fn name(&self) -> &Option { &self.scriptable().name } fn tags(&self) -> &HashSet { &self.scriptable().tags } fn solid(&self) -> bool; /// The compile-key for an object's script: the object's `script_name` — a /// world-pool name for a named script, or a synthetic `BUILTIN_*` name set by /// [`Board::expand_builtin_archetypes`](crate::board::Board::expand_builtin_archetypes) /// for an expanded built-in (so identical built-ins share one compiled AST, /// while the source still comes from `builtin_script`). `None` if the object has /// no script. fn script_key(&self) -> &ScriptKey { &self.scriptable().script_name } } impl Hookable for &Sensor { fn scriptable(&self) -> &ScriptAttributes { &self.scripting } fn location(&self) -> (usize, usize) { (self.x, self.y) } fn solid(&self) -> bool { false } } pub struct LocatedObject<'a>(pub &'a ObjectDef, pub (usize, usize)); impl Hookable for LocatedObject<'_> { fn scriptable(&self) -> &ScriptAttributes { &self.0.scripting } fn location(&self) -> (usize, usize) { self.1 } fn solid(&self) -> bool { true } } #[derive(Clone, Debug)] pub enum Tile { Player, Object(Box), } impl Tile { pub fn glyph(&self) -> Glyph { match self { Tile::Player => Glyph::player(), Tile::Object(obj) => obj.scripting.glyph, } } pub fn player(&self) -> bool { matches!(self, Self::Player) } /// Can this tile be moved by a `shift`? pub fn shiftable(&self) -> bool { match self { Tile::Player => true, // Player will shift anywhere Tile::Object(obj) => { match obj.enter_response { EnterResponse::Push(p) => p != No, EnterResponse::Hook | EnterResponse::Block => false, // Blockers never shift EnterResponse::Grab | EnterResponse::Swap | EnterResponse::Squish => true, } } } } } /// The serialized representation of a Tile. Can be turned into a Tile, or vice versa #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "lowercase", tag = "type")] pub enum TileSpec { Player, Object { #[serde(default, skip_serializing_if = "Option::is_none")] script: Option, enter: EnterResponse, #[serde(flatten)] glyph: Glyph, #[serde(flatten, default)] optics: Optics, #[serde(default, skip_serializing_if = "Option::is_none")] name: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] tags: Vec, }, Builtin { kind: String, #[serde(flatten, default, skip_serializing_if = "Option::is_none")] glyph: Option, } } pub trait IntoTile { fn into_tile(self, next_object_id: &mut ObjectId) -> Result; } impl IntoTile for TileSpec { fn into_tile(self, next_object_id: &mut ObjectId) -> Result { match self { TileSpec::Player => Ok(Tile::Player), // TileSpec::Portal { name, target_board, target_name } => { // let def = PortalDef { // name, // target_map: target_board, // target_entry: target_name, // }; // Ok(Tile::Portal(Box::new(def))) // } TileSpec::Object { script, enter, glyph, optics, name, tags } => { let def = ObjectDef { enter_response: enter, scripting: ScriptAttributes { id: *next_object_id, glyph, optics, script_name: script.into(), tags: tags.into_iter().collect(), name, queue: ObjQueue::new(), } }; *next_object_id = *next_object_id + 1; Ok(Tile::Object(Box::new(def))) }, TileSpec::Builtin { kind, glyph } => { if let Some((builtin, variant)) = Builtin::from_name(&kind) { let def = ObjectDef { enter_response: builtin.enter_response(), scripting: ScriptAttributes { id: *next_object_id, glyph: glyph.unwrap_or(builtin.default_glyph_for(variant)), optics: builtin.optics(), script_name: builtin.script(), tags: HashSet::from([format!("BUILTIN_{}", variant)]), name: None, queue: ObjQueue::new(), } }; *next_object_id = *next_object_id + 1; Ok(Tile::Object(Box::new(def))) } else { Err(format!("Unknown builtin kind {}", kind)) } } } } } /// Convenience functions for tests #[cfg(test)] impl TileSpec { pub fn wall() -> Self { TileSpec::Builtin { kind: "wall".to_string(), glyph: None } } pub fn player() -> Self { TileSpec::Player } pub fn krate() -> Self { TileSpec::Builtin { kind: "crate".to_string(), glyph: None } } pub fn gem() -> Self { TileSpec::Builtin { kind: "gem".to_string(), glyph: None } } } #[derive(Serialize, Deserialize, Copy, Clone)] #[serde(rename_all = "lowercase")] pub enum FloorSpec { Biome(FloorBiome), Glyph(Glyph), } pub trait IntoFloor { fn into_floor(self, width: usize, height: usize) -> Floor; } impl IntoFloor for Option { fn into_floor(self, width: usize, height: usize) -> Floor { match self { Some(FloorSpec::Biome(biome)) => Floor::biome(biome, width, height), Some(FloorSpec::Glyph(glyph)) => Floor::Fixed(glyph), None => Floor::Blank } } }