2026-07-24 23:46:43 -05:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2026-07-14 23:48:50 -05:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use crate::api::queue::ObjQueue;
|
2026-07-24 21:52:05 -05:00
|
|
|
use crate::{Builtin, Direction};
|
2026-07-24 23:46:43 -05:00
|
|
|
use crate::builtin::BUILTIN_SOURCES;
|
2026-07-24 21:52:05 -05:00
|
|
|
use crate::floor::{Floor, FloorBiome};
|
2026-07-14 23:48:50 -05:00
|
|
|
use crate::glyph::Glyph;
|
|
|
|
|
use crate::object_def::ObjectDef;
|
2026-07-24 21:52:05 -05:00
|
|
|
use crate::utils::{ObjectId, Pushable};
|
|
|
|
|
use crate::utils::Pushable::No;
|
2026-07-14 23:48:50 -05:00
|
|
|
|
|
|
|
|
/// 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)]
|
2026-07-24 21:52:05 -05:00
|
|
|
#[serde(rename_all = "lowercase")]
|
2026-07-14 23:48:50 -05:00
|
|
|
pub enum EnterResponse {
|
|
|
|
|
/// Flat denial: don't let the move happen, block it.
|
|
|
|
|
Block,
|
2026-07-24 21:52:05 -05:00
|
|
|
/// If the player moves, call the `grab` hook and then remove this object. Anything else, act as
|
|
|
|
|
/// `Pushable::Any`.
|
2026-07-14 23:48:50 -05:00
|
|
|
/// 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,
|
2026-07-24 21:52:05 -05:00
|
|
|
/// Swap places with whatever moved on top of you, as long as it was the player. Anything else,
|
|
|
|
|
/// act as `Pushable::Any`.
|
2026-07-14 23:48:50 -05:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 21:52:05 -05:00
|
|
|
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
|
2026-07-14 23:48:50 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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
|
2026-07-24 21:52:05 -05:00
|
|
|
/// visible / lit, it will be drawn.
|
2026-07-14 23:48:50 -05:00
|
|
|
/// - `Below` is below the grid but above the floor. A nonzero-tile-glyph will be drawn only if
|
2026-07-24 21:52:05 -05:00
|
|
|
/// there's not a grid-thing in the same cell.
|
2026-07-14 23:48:50 -05:00
|
|
|
#[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
|
2026-07-24 21:52:05 -05:00
|
|
|
#[serde(default = "default_as_true")]
|
2026-07-14 23:48:50 -05:00
|
|
|
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.
|
2026-07-24 21:52:05 -05:00
|
|
|
#[serde(default)]
|
2026-07-14 23:48:50 -05:00
|
|
|
pub glow: u32,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 21:52:05 -05:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-24 23:46:43 -05:00
|
|
|
#[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 => "<none>",
|
|
|
|
|
ScriptKey::World(name) => name.as_str(),
|
|
|
|
|
ScriptKey::Builtin(name) => *name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 23:20:52 -05:00
|
|
|
/// 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<String, String>) -> Result<Option<&'a str>, String> {
|
2026-07-24 23:46:43 -05:00
|
|
|
match self {
|
2026-07-25 23:20:52 -05:00
|
|
|
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))),
|
2026-07-24 23:46:43 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<Option<String>> for ScriptKey {
|
|
|
|
|
fn from(s: Option<String>) -> Self {
|
|
|
|
|
s.map_or(Self::None, |s| Self::World(s))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 23:48:50 -05:00
|
|
|
/// 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)]
|
2026-07-24 21:52:05 -05:00
|
|
|
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,
|
2026-07-14 23:48:50 -05:00
|
|
|
/// 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.
|
2026-07-24 23:46:43 -05:00
|
|
|
pub script_name: ScriptKey,
|
2026-07-14 23:48:50 -05:00
|
|
|
/// 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<String>,
|
|
|
|
|
/// 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<String>,
|
|
|
|
|
/// 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.
|
2026-07-24 21:52:05 -05:00
|
|
|
#[derive(Clone)]
|
2026-07-14 23:48:50 -05:00
|
|
|
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,
|
2026-07-24 21:52:05 -05:00
|
|
|
/// 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<String>,
|
2026-08-04 22:56:05 -05:00
|
|
|
#[serde(default, skip_serializing_if = "Glyph::is_transparent")]
|
2026-07-14 23:48:50 -05:00
|
|
|
pub glyph: Glyph,
|
2026-07-24 21:52:05 -05:00
|
|
|
#[serde(flatten, default)]
|
2026-07-14 23:48:50 -05:00
|
|
|
pub optics: Optics,
|
2026-07-24 21:52:05 -05:00
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub name: Option<String>,
|
|
|
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
|
|
|
pub tags: Vec<String>,
|
|
|
|
|
#[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(),
|
2026-07-24 23:46:43 -05:00
|
|
|
script_name: self.script.into(),
|
2026-07-24 21:52:05 -05:00
|
|
|
..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<String> {
|
|
|
|
|
&self.scriptable().name
|
|
|
|
|
}
|
|
|
|
|
fn tags(&self) -> &HashSet<String> {
|
|
|
|
|
&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.
|
2026-07-24 23:46:43 -05:00
|
|
|
fn script_key(&self) -> &ScriptKey {
|
|
|
|
|
&self.scriptable().script_name
|
2026-07-24 21:52:05 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<ObjectDef>),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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<String>,
|
|
|
|
|
enter: EnterResponse,
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
|
glyph: Glyph,
|
|
|
|
|
#[serde(flatten, default)]
|
|
|
|
|
optics: Optics,
|
|
|
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
name: Option<String>,
|
|
|
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
|
|
|
tags: Vec<String>,
|
|
|
|
|
},
|
|
|
|
|
Builtin {
|
|
|
|
|
kind: String,
|
|
|
|
|
#[serde(flatten, default, skip_serializing_if = "Option::is_none")]
|
|
|
|
|
glyph: Option<Glyph>,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub trait IntoTile {
|
|
|
|
|
fn into_tile(self, next_object_id: &mut ObjectId) -> Result<Tile, String>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IntoTile for TileSpec {
|
|
|
|
|
fn into_tile(self, next_object_id: &mut ObjectId) -> Result<Tile, String> {
|
|
|
|
|
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,
|
2026-07-24 23:46:43 -05:00
|
|
|
script_name: script.into(),
|
2026-07-24 21:52:05 -05:00
|
|
|
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(),
|
2026-07-24 23:46:43 -05:00
|
|
|
script_name: builtin.script(),
|
2026-07-24 21:52:05 -05:00
|
|
|
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 {
|
2026-07-24 23:46:43 -05:00
|
|
|
TileSpec::Player
|
2026-07-24 21:52:05 -05:00
|
|
|
}
|
|
|
|
|
pub fn krate() -> Self {
|
2026-07-24 23:46:43 -05:00
|
|
|
TileSpec::Builtin { kind: "crate".to_string(), glyph: None }
|
2026-07-24 21:52:05 -05:00
|
|
|
}
|
|
|
|
|
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<FloorSpec> {
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|