the huge refactor

This commit is contained in:
2026-07-24 21:52:05 -05:00
parent 5146cc9bcc
commit 917f4b1bf0
43 changed files with 1807 additions and 3267 deletions
+319 -26
View File
@@ -1,16 +1,21 @@
use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use crate::api::queue::ObjQueue;
use crate::{Builtin, Direction};
use crate::floor::{Floor, FloorBiome};
use crate::glyph::Glyph;
use crate::object_def::ObjectDef;
use crate::utils::{Behavior, ObjectId, Pushable};
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,
/// Call the `grab` hook and then remove this object.
/// 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
@@ -32,32 +37,46 @@ pub enum EnterResponse {
/// 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 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 From<Behavior> 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
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.
/// 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.
/// 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 }
@@ -67,6 +86,7 @@ pub enum DrawLayer { Above, Below }
#[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.
@@ -76,13 +96,41 @@ pub struct Optics {
/// - 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
}
/// 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 {
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
@@ -110,19 +158,264 @@ pub struct Scripting {
/// 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)]
#[derive(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,
/// Whether this is drawn above or below the grid
pub draw_layer: DrawLayer,
/// Sensors have scripting ability
#[serde(skip)]
pub scripting: Scripting,
}
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>,
pub glyph: Glyph,
#[serde(flatten, default)]
pub optics: Optics,
#[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(),
script_name: self.script,
..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.
fn script_key(&self) -> Option<&String> {
self.scriptable().script_name.as_ref()
}
}
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,
script_name: script,
builtin_script: None,
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: None,
builtin_script: Some(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::Builtin { kind: "wall".to_string(), glyph: None }
}
pub fn krate() -> Self {
TileSpec::Builtin { kind: "wall".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<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
}
}
}