Solidity
This commit is contained in:
+100
-22
@@ -73,17 +73,22 @@ pub struct FontSpec {
|
||||
///
|
||||
/// `Behavior` is a plain data struct returned by [`Archetype::behavior`]. It
|
||||
/// contains the properties the engine needs to simulate a cell — currently
|
||||
/// passability and opacity. Future properties (pushable, shootable, etc.) can
|
||||
/// solidity, opacity, and pushability. Future properties (shootable, etc.) can
|
||||
/// be added here without changing call sites.
|
||||
///
|
||||
/// For scripted objects, passability and opacity are stored directly on
|
||||
/// For scripted objects, solidity and opacity are stored directly on
|
||||
/// [`ObjectDef`] and will eventually be overridable by Rhai scripts at runtime.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Behavior {
|
||||
/// Whether an entity can walk through this cell.
|
||||
pub passable: bool,
|
||||
/// Whether this cell blocks / participates in movement. A solid cell stops a
|
||||
/// mover (and is the only kind of cell that can later be pushed or receive a
|
||||
/// collision event). This is the inverse of the old `passable` flag.
|
||||
pub solid: bool,
|
||||
/// Whether this cell blocks line of sight (reserved for future rendering).
|
||||
pub opaque: bool,
|
||||
/// Whether a mover can shove this cell. Unused for now — every archetype is
|
||||
/// `false`; the push mechanic is future work.
|
||||
pub pushable: bool,
|
||||
}
|
||||
|
||||
/// A class of board cell, encoding its default behavior and appearance.
|
||||
@@ -121,16 +126,19 @@ impl Archetype {
|
||||
pub fn behavior(&self) -> Behavior {
|
||||
match self {
|
||||
Archetype::Empty => Behavior {
|
||||
passable: true,
|
||||
solid: false,
|
||||
opaque: false,
|
||||
pushable: false,
|
||||
},
|
||||
Archetype::Wall => Behavior {
|
||||
passable: false,
|
||||
solid: true,
|
||||
opaque: true,
|
||||
pushable: false,
|
||||
},
|
||||
Archetype::ErrorBlock => Behavior {
|
||||
passable: false,
|
||||
solid: true,
|
||||
opaque: true,
|
||||
pushable: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -222,10 +230,15 @@ 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 movement, of players or other objects
|
||||
pub passable: bool,
|
||||
/// 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,
|
||||
/// Name of the Rhai script in [`Board::scripts`] that drives this object.
|
||||
/// `None` means this object has no script yet.
|
||||
pub script_name: Option<String>,
|
||||
@@ -244,20 +257,34 @@ impl ObjectDef {
|
||||
|
||||
/// Creates a new object at `(x, y)` with default glyph and blocking behavior.
|
||||
///
|
||||
/// Defaults: `passable = false`, `opaque = true`, no script. These match
|
||||
/// the serde defaults in the map file format so new objects round-trip correctly.
|
||||
/// Defaults: `solid = true`, `opaque = true`, `pushable = false`, no script.
|
||||
/// These match the serde defaults in the map file format so new objects
|
||||
/// round-trip correctly.
|
||||
pub fn new(x: usize, y: usize) -> Self {
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
glyph: Self::default_glyph(),
|
||||
passable: false,
|
||||
solid: true,
|
||||
opaque: true,
|
||||
pushable: false,
|
||||
script_name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The single solid occupant of a board cell, returned by [`Board::solid_at`].
|
||||
///
|
||||
/// At most one solid — a grid [`Archetype`] *or* an [`ObjectDef`] — may occupy a
|
||||
/// cell (the invariant enforced at load time), so this represents the one thing a
|
||||
/// mover would collide with there.
|
||||
pub enum Solid<'a> {
|
||||
/// The cell's grid archetype is itself solid (e.g. [`Archetype::Wall`]).
|
||||
Cell(Archetype),
|
||||
/// A solid [`ObjectDef`] occupies the cell.
|
||||
Object(&'a ObjectDef),
|
||||
}
|
||||
|
||||
/// A portal that teleports the player to a named entry point on another board.
|
||||
///
|
||||
/// Portals are loaded from `[[portals]]` entries in `.toml` map files and
|
||||
@@ -367,19 +394,33 @@ impl Board {
|
||||
&self.cells
|
||||
}
|
||||
|
||||
/// Returns `true` if the cell at `(x, y)` allows entities to pass through.
|
||||
/// Returns the single solid entity occupying `(x, y)`, if any.
|
||||
///
|
||||
/// Objects block movement before the grid cell archetype is consulted.
|
||||
/// Checks objects first, then the grid archetype. Because at most one solid
|
||||
/// may occupy a cell (an invariant enforced when the board is loaded — see
|
||||
/// [`crate::map_file`]), this returns that one occupant or `None`.
|
||||
/// Panics if `x` or `y` are out of bounds.
|
||||
pub fn solid_at(&self, x: usize, y: usize) -> Option<Solid<'_>> {
|
||||
// A solid object shadows the grid cell it sits on.
|
||||
if let Some(obj) = self.object_at(x, y)
|
||||
&& obj.solid
|
||||
{
|
||||
return Some(Solid::Object(obj));
|
||||
}
|
||||
// Otherwise the grid archetype itself may be solid (e.g. a wall).
|
||||
let arch = self.get(x, y).1;
|
||||
if arch.behavior().solid {
|
||||
return Some(Solid::Cell(arch));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns `true` if a mover can enter `(x, y)` — i.e. no solid occupies it.
|
||||
///
|
||||
/// Convenience inverse of [`solid_at`](Board::solid_at).
|
||||
/// Panics if `x` or `y` are out of bounds.
|
||||
pub fn is_passable(&self, x: usize, y: usize) -> bool {
|
||||
// An object on this cell blocks if it's unpassable
|
||||
if let Some(obj) = self.object_at(x, y)
|
||||
&& !obj.passable
|
||||
{
|
||||
false
|
||||
} else {
|
||||
self.get(x, y).1.behavior().passable
|
||||
}
|
||||
self.solid_at(x, y).is_none()
|
||||
}
|
||||
|
||||
/// Returns the index into [`Board::objects`] of the object at `(x, y)`, if any.
|
||||
@@ -595,6 +636,43 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solid_at_reports_wall_object_and_empty() {
|
||||
// A 3×1 board: empty floor, a wall, and an empty cell holding one object.
|
||||
let mut board = open_board(3, 1, (0, 0), vec![], &[]);
|
||||
board.cells[1] = (Archetype::Wall.default_glyph(), Archetype::Wall);
|
||||
// A solid object on the otherwise-empty cell (2, 0).
|
||||
board.objects.push(ObjectDef::new(2, 0)); // solid by default
|
||||
|
||||
// Empty floor: nothing solid.
|
||||
assert!(board.solid_at(0, 0).is_none());
|
||||
assert!(board.is_passable(0, 0));
|
||||
|
||||
// Wall cell: the grid archetype is the solid.
|
||||
match board.solid_at(1, 0) {
|
||||
Some(Solid::Cell(Archetype::Wall)) => {}
|
||||
other => panic!("expected Solid::Cell(Wall), got {:?}", other.is_some()),
|
||||
}
|
||||
assert!(!board.is_passable(1, 0));
|
||||
|
||||
// Object cell: the solid object shadows the empty floor under it.
|
||||
match board.solid_at(2, 0) {
|
||||
Some(Solid::Object(o)) => assert_eq!((o.x, o.y), (2, 0)),
|
||||
_ => panic!("expected Solid::Object"),
|
||||
}
|
||||
assert!(!board.is_passable(2, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_solid_object_does_not_block() {
|
||||
// A non-solid object sits on empty floor: the cell stays passable.
|
||||
let mut obj = ObjectDef::new(1, 0);
|
||||
obj.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));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_runs_only_on_run_init_not_at_construction() {
|
||||
let board = board_with_object(
|
||||
|
||||
Reference in New Issue
Block a user