new layer model
This commit is contained in:
+170
-213
@@ -1,12 +1,33 @@
|
||||
use crate::archetype::Archetype;
|
||||
use crate::floor::Floor;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::layer::Layer;
|
||||
use crate::log::LogLine;
|
||||
use crate::object_def::ObjectDef;
|
||||
use crate::utils::Direction;
|
||||
use crate::utils::{Behavior, ObjectId, PlayerPos, PortalDef, Pushable, RegistryValue, Solid};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
/// A non-solid `(glyph, archetype)` placed at a board coordinate, **outside** the
|
||||
/// main grid, drawn only when the grid cell at `(x, y)` is empty.
|
||||
///
|
||||
/// Decorations exist so a single file format can serve both world files and save
|
||||
/// files: at authoring time every cell holds at most one thing, but during play a
|
||||
/// runtime solid can end up sharing a cell with a non-solid that was already there.
|
||||
/// The non-solid is recorded here (the grid keeps the solid). The editor does not
|
||||
/// place decorations; they are a save/runtime concern. A decoration's archetype is
|
||||
/// always non-solid (a solid one is rejected at load).
|
||||
#[derive(Clone)]
|
||||
pub struct Decoration {
|
||||
/// Column (0-indexed).
|
||||
pub x: usize,
|
||||
/// Row (0-indexed).
|
||||
pub y: usize,
|
||||
/// The decoration's visual.
|
||||
pub glyph: Glyph,
|
||||
/// The decoration's (non-solid) archetype.
|
||||
pub archetype: Archetype,
|
||||
}
|
||||
|
||||
/// The complete state of one game board (a single room or screen).
|
||||
///
|
||||
/// `Board` is the central data structure of the engine, equivalent to a
|
||||
@@ -37,12 +58,18 @@ pub struct Board {
|
||||
pub width: usize,
|
||||
/// Height of the board in cells.
|
||||
pub height: usize,
|
||||
/// Ordered draw stack of [`Layer`]s, bottom (index 0) to top. Each layer holds
|
||||
/// a row-major grid of `(Glyph, Archetype)` cells; a transparent cell lets the
|
||||
/// layer beneath show through. Drawing ([`Board::glyph_at`]) walks the stack
|
||||
/// top-down; solidity ([`Board::solid_at`]) scans every layer. Access a single
|
||||
/// cell with [`Board::get`]/[`Board::get_mut`] by `(z, x, y)`.
|
||||
pub(crate) layers: Vec<Layer>,
|
||||
/// The single row-major grid of `(Glyph, Archetype)` cells (`width * height`),
|
||||
/// holding every solid and most non-solids. A transparent cell (glyph tile 0)
|
||||
/// draws nothing, revealing a [`decoration`](Board::decorations) or the
|
||||
/// [`floor`](Board::floor) beneath. Access a cell with [`Board::get`]/
|
||||
/// [`Board::get_mut`] by `(x, y)`.
|
||||
pub(crate) grid: Vec<(Glyph, Archetype)>,
|
||||
/// The board's cosmetic floor (blank / one fixed glyph / a biome), drawn beneath
|
||||
/// everything. Replaces the old dedicated floor layer.
|
||||
pub(crate) floor: Floor,
|
||||
/// 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<Decoration>,
|
||||
/// 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,
|
||||
@@ -75,98 +102,91 @@ pub struct Board {
|
||||
}
|
||||
|
||||
impl Board {
|
||||
/// Number of draw layers on this board (≥ 1 for a loaded board).
|
||||
pub fn layer_count(&self) -> usize {
|
||||
self.layers.len()
|
||||
}
|
||||
|
||||
/// Return a list of all `ObjectId`s currently on the board.
|
||||
pub fn all_ids(&self) -> Vec<ObjectId> {
|
||||
self.objects.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Returns a reference to the cell at `(x, y)` on layer `z`.
|
||||
/// Returns a reference to the `(Glyph, Archetype)` cell at `(x, y)`.
|
||||
///
|
||||
/// The cell is a `(Glyph, Archetype)` tuple. Panics if `z`, `x`, or `y` are
|
||||
/// out of bounds.
|
||||
pub fn get(&self, z: usize, x: usize, y: usize) -> &(Glyph, Archetype) {
|
||||
&self.layers[z].cells[y * self.width + x]
|
||||
/// Panics if `x` or `y` are out of bounds.
|
||||
pub fn get(&self, x: usize, y: usize) -> &(Glyph, Archetype) {
|
||||
&self.grid[y * self.width + x]
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the cell at `(x, y)` on layer `z`.
|
||||
/// Returns a mutable reference to the cell at `(x, y)`.
|
||||
///
|
||||
/// Panics if `z`, `x`, or `y` are out of bounds.
|
||||
pub fn get_mut(&mut self, z: usize, x: usize, y: usize) -> &mut (Glyph, Archetype) {
|
||||
/// Panics if `x` or `y` are out of bounds.
|
||||
pub fn get_mut(&mut self, x: usize, y: usize) -> &mut (Glyph, Archetype) {
|
||||
let w = self.width;
|
||||
&mut self.layers[z].cells[y * w + x]
|
||||
&mut self.grid[y * w + x]
|
||||
}
|
||||
|
||||
/// Replace the solid (if any) at `(x, y)` with `Empty`
|
||||
/// Replace the solid terrain (if any) at `(x, y)` with a transparent `Empty`
|
||||
/// cell, revealing the floor beneath.
|
||||
pub fn clear_solid(&mut self, x: usize, y: usize) {
|
||||
if self.in_bounds((x as i64, y as i64))
|
||||
&& let Some(z) = self.solid_cell_layer(x, y) {
|
||||
*self.get_mut(z, x, y) = (Archetype::Empty.default_glyph(), Archetype::Empty)
|
||||
}
|
||||
if self.in_bounds((x as i64, y as i64)) && self.get(x, y).1.behavior().solid {
|
||||
*self.get_mut(x, y) = (Glyph::transparent(), Archetype::Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the glyph to display at `(x, y)`, honoring layer draw order.
|
||||
/// Returns the glyph to display at `(x, y)`.
|
||||
///
|
||||
/// The player is always drawn on top (it is not part of the layer stack yet).
|
||||
/// Otherwise the layers are walked **top-down**; the first thing that draws on
|
||||
/// a layer wins:
|
||||
/// With a single grid the draw order is a fixed precedence (no layer walk):
|
||||
///
|
||||
/// 1. an object on that layer (a solid object always; otherwise a non-solid
|
||||
/// object whose glyph is not transparent, i.e. `tile != 0`),
|
||||
/// 2. a portal on that layer,
|
||||
/// 3. the layer's terrain cell — a solid always draws, and a non-solid draws
|
||||
/// only when not transparent (`tile != 0`).
|
||||
/// 1. the player (drawn on top; not part of the grid yet);
|
||||
/// 2. an object at the cell — a solid object always, otherwise the first
|
||||
/// non-transparent non-solid object (`tile != 0`, so invisible objects exist);
|
||||
/// 3. the grid cell `(glyph, arch)` — a solid always draws, a non-solid only when
|
||||
/// visible (`tile != 0`);
|
||||
/// 4. a portal at the cell (portals sit on a transparent grid cell);
|
||||
/// 5. a [`decoration`](Board::decorations) at the cell (reached only because the
|
||||
/// grid cell was empty);
|
||||
/// 6. the [`floor`](Board::floor) glyph, if any;
|
||||
/// 7. the canonical black `Empty` glyph.
|
||||
///
|
||||
/// If no layer contributes anything, the canonical black `Empty` glyph is used.
|
||||
/// Panics if out of bounds.
|
||||
pub fn glyph_at(&self, x: usize, y: usize) -> Glyph {
|
||||
// The player is rendered above the whole stack (see the `Player` notes).
|
||||
// The player is rendered above everything (see the `Player` notes).
|
||||
if self.player.x == x as i64 && self.player.y == y as i64 {
|
||||
return Glyph::player();
|
||||
}
|
||||
|
||||
for z in (0..self.layers.len()).rev() {
|
||||
// Objects on this layer: a solid object always draws; otherwise the
|
||||
// first non-transparent non-solid object (lets invisible objects exist).
|
||||
let mut nonsolid: Option<Glyph> = None;
|
||||
for o in self
|
||||
.objects
|
||||
.values()
|
||||
.filter(|o| o.x == x && o.y == y && o.z == z)
|
||||
{
|
||||
if o.solid {
|
||||
return o.glyph;
|
||||
}
|
||||
if nonsolid.is_none() && o.glyph.tile != 0 {
|
||||
nonsolid = Some(o.glyph);
|
||||
}
|
||||
// Objects: a solid object always draws; otherwise the first non-transparent
|
||||
// non-solid object (lets invisible trigger objects exist).
|
||||
let mut nonsolid: Option<Glyph> = None;
|
||||
for o in self.objects.values().filter(|o| o.x == x && o.y == y) {
|
||||
if o.solid {
|
||||
return o.glyph;
|
||||
}
|
||||
if let Some(g) = nonsolid {
|
||||
return g;
|
||||
}
|
||||
|
||||
// A portal on this layer draws above its (transparent) terrain cell.
|
||||
if self
|
||||
.portals
|
||||
.iter()
|
||||
.any(|p| p.x == x && p.y == y && p.z == z)
|
||||
{
|
||||
return PortalDef::default_glyph();
|
||||
}
|
||||
|
||||
// The terrain cell: a solid always draws; a non-solid only if visible.
|
||||
let (glyph, arch) = self.get(z, x, y);
|
||||
if arch.behavior().solid || glyph.tile != 0 {
|
||||
return *glyph;
|
||||
if nonsolid.is_none() && o.glyph.tile != 0 {
|
||||
nonsolid = Some(o.glyph);
|
||||
}
|
||||
}
|
||||
if let Some(g) = nonsolid {
|
||||
return g;
|
||||
}
|
||||
|
||||
// Nothing on any layer: the canonical black empty cell.
|
||||
Archetype::Empty.default_glyph()
|
||||
// The grid cell: a solid always draws; a non-solid only if visible.
|
||||
let (glyph, arch) = self.get(x, y);
|
||||
if arch.behavior().solid || glyph.tile != 0 {
|
||||
return *glyph;
|
||||
}
|
||||
|
||||
// A portal sits on its (transparent) grid cell.
|
||||
if self.portals.iter().any(|p| p.x == x && p.y == y) {
|
||||
return PortalDef::default_glyph();
|
||||
}
|
||||
|
||||
// The grid cell was empty: a decoration may show here.
|
||||
if let Some(d) = self.decorations.iter().find(|d| d.x == x && d.y == y) {
|
||||
return d.glyph;
|
||||
}
|
||||
|
||||
// Then the floor, else the canonical black empty cell.
|
||||
self.floor
|
||||
.glyph_at(x, y, self.width)
|
||||
.unwrap_or_else(|| Archetype::Empty.default_glyph())
|
||||
}
|
||||
|
||||
/// Returns `true` if `(x, y)` is a valid cell coordinate on this board.
|
||||
@@ -218,20 +238,14 @@ impl Board {
|
||||
};
|
||||
return Some(Solid::object_at(x, y, id, behavior));
|
||||
}
|
||||
// Otherwise some layer's terrain archetype may be solid (e.g. a wall).
|
||||
if let Some(z) = self.solid_cell_layer(x, y) {
|
||||
let (glyph, arch) = *self.get(z, x, y);
|
||||
return Some(Solid::terrain_at(x, y, z, glyph, arch));
|
||||
// Otherwise the grid cell's terrain archetype may be solid (e.g. a wall).
|
||||
let (glyph, arch) = *self.get(x, y);
|
||||
if arch.behavior().solid {
|
||||
return Some(Solid::terrain_at(x, y, glyph, arch));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the index of the layer whose terrain cell at `(x, y)` is solid, if
|
||||
/// any. By the one-solid-per-cell invariant there is at most one such layer.
|
||||
fn solid_cell_layer(&self, x: usize, y: usize) -> Option<usize> {
|
||||
(0..self.layers.len()).find(|&z| self.get(z, x, y).1.behavior().solid)
|
||||
}
|
||||
|
||||
/// Returns `true` if a mover can enter `(x, y)` — i.e. no solid occupies it.
|
||||
///
|
||||
/// Convenience inverse of [`solid_at`](Board::solid_at).
|
||||
@@ -381,10 +395,9 @@ impl Board {
|
||||
|
||||
/// Moves the single solid occupant of `(x, y)` one step by `(dx, dy)`.
|
||||
///
|
||||
/// A solid object is relocated (keeping its layer); otherwise the solid
|
||||
/// terrain archetype (a crate) is moved within its own layer, leaving a
|
||||
/// transparent cell behind so the layer beneath (e.g. floor) shows through.
|
||||
/// The caller guarantees the destination is already clear.
|
||||
/// A solid object is relocated; otherwise the solid terrain archetype (a crate)
|
||||
/// is moved, leaving a transparent cell behind so the floor shows through. The
|
||||
/// caller guarantees the destination is already clear.
|
||||
fn shift_solid(&mut self, x: usize, y: usize, dx: i64, dy: i64) {
|
||||
let (tx, ty) = ((x as i64 + dx) as usize, (y as i64 + dy) as usize);
|
||||
let Some(solid) = self.solid_at(x, y) else {
|
||||
@@ -393,8 +406,8 @@ impl Board {
|
||||
// A terrain cell leaves a transparent cell behind (revealing any floor); the
|
||||
// player and objects carry no grid cell, so there is nothing to vacate. `place`
|
||||
// captured the glyph/arch, so clearing the source first is safe.
|
||||
if let Some(z) = self.solid_cell_layer(x, y) {
|
||||
*self.get_mut(z, x, y) = (Glyph::transparent(), Archetype::Empty);
|
||||
if self.get(x, y).1.behavior().solid {
|
||||
*self.get_mut(x, y) = (Glyph::transparent(), Archetype::Empty);
|
||||
}
|
||||
solid.place(self, tx, ty);
|
||||
}
|
||||
@@ -465,23 +478,19 @@ impl Board {
|
||||
/// drawing tools cannot place, remove, or alter a floor):
|
||||
///
|
||||
/// - **Terrain** (`arch != Empty`, always solid today): removes any solid object
|
||||
/// already in the cell, then writes `(glyph, arch)` into the cell's terrain.
|
||||
/// - **Erase** (`arch == Empty`): removes the cell's terrain *and* every object in
|
||||
/// it, leaving the floor (a visible `Empty` cell on a lower layer) in place.
|
||||
/// already in the cell, then writes `(glyph, arch)` into the grid cell.
|
||||
/// - **Erase** (`arch == Empty`): removes the grid cell's terrain *and* every
|
||||
/// object in it, leaving the floor beneath in place.
|
||||
///
|
||||
/// Terrain is written to the cell's existing terrain layer (the single non-`Empty`
|
||||
/// archetype across layers, if any) or else the top layer; a vacated terrain cell
|
||||
/// becomes a transparent `Empty` so a lower floor shows through. Panics if `(x, y)`
|
||||
/// is out of bounds.
|
||||
/// A vacated grid cell becomes a transparent `Empty` so the floor shows through.
|
||||
/// Panics if `(x, y)` is out of bounds.
|
||||
pub fn place_archetype(&mut self, x: usize, y: usize, arch: Archetype, glyph: Glyph) {
|
||||
if arch == Archetype::Empty {
|
||||
// Erase: drop every object in the cell and clear its terrain (keep floor).
|
||||
// Erase: drop every object in the cell and clear its grid cell (keep floor).
|
||||
for id in self.object_ids_at(x, y) {
|
||||
self.objects.remove(&id);
|
||||
}
|
||||
if let Some(z) = self.terrain_layer_at(x, y) {
|
||||
*self.get_mut(z, x, y) = (Glyph::transparent(), Archetype::Empty);
|
||||
}
|
||||
*self.get_mut(x, y) = (Glyph::transparent(), Archetype::Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -489,10 +498,7 @@ impl Board {
|
||||
if let Some(id) = self.solid_object_id_at(x, y) {
|
||||
self.objects.remove(&id);
|
||||
}
|
||||
// Reuse the existing terrain layer if the cell already has terrain, else the
|
||||
// top layer (so the new wall draws above any floor on a lower layer).
|
||||
let z = self.terrain_layer_at(x, y).unwrap_or(self.layers.len() - 1);
|
||||
*self.get_mut(z, x, y) = (glyph, arch);
|
||||
*self.get_mut(x, y) = (glyph, arch);
|
||||
}
|
||||
|
||||
/// Replaces every terrain cell whose archetype is script-backed (e.g. a
|
||||
@@ -509,26 +515,23 @@ impl Board {
|
||||
/// already loaded as objects are untouched, so it is safe to call more than once.
|
||||
pub fn expand_builtin_archetypes(&mut self) {
|
||||
use crate::builtin_scripts::builtin_tag;
|
||||
// Collect first: the loop below mutates both layers and the object map.
|
||||
let mut found: Vec<(usize, usize, usize, Glyph, Archetype)> = Vec::new();
|
||||
for z in 0..self.layers.len() {
|
||||
for y in 0..self.height {
|
||||
for x in 0..self.width {
|
||||
let (glyph, arch) = *self.get(z, x, y);
|
||||
if matches!(arch, Archetype::Builtin(_, _)) {
|
||||
found.push((z, x, y, glyph, arch));
|
||||
}
|
||||
// Collect first: the loop below mutates both the grid and the object map.
|
||||
let mut found: Vec<(usize, usize, Glyph, Archetype)> = Vec::new();
|
||||
for y in 0..self.height {
|
||||
for x in 0..self.width {
|
||||
let (glyph, arch) = *self.get(x, y);
|
||||
if matches!(arch, Archetype::Builtin(_, _)) {
|
||||
found.push((x, y, glyph, arch));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (z, x, y, glyph, arch) in found {
|
||||
// Vacate the terrain cell (revealing any floor beneath), then spawn the
|
||||
for (x, y, glyph, arch) in found {
|
||||
// Vacate the grid cell (revealing any floor beneath), then spawn the
|
||||
// object — mirroring `resolve_entry`'s object template.
|
||||
*self.get_mut(z, x, y) = (Glyph::transparent(), Archetype::Empty);
|
||||
*self.get_mut(x, y) = (Glyph::transparent(), Archetype::Empty);
|
||||
let Archetype::Builtin(b, _alias) = arch else { continue };
|
||||
let beh = b.behavior();
|
||||
let mut obj = ObjectDef::new(x, y);
|
||||
obj.z = z;
|
||||
obj.glyph = glyph;
|
||||
obj.solid = beh.solid;
|
||||
obj.opaque = beh.opaque;
|
||||
@@ -617,13 +620,6 @@ impl Board {
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Returns the index of the layer whose terrain cell at `(x, y)` is non-`Empty`
|
||||
/// (the cell's single terrain archetype, if any). By the one-solid-per-cell
|
||||
/// invariant there is at most one such layer.
|
||||
fn terrain_layer_at(&self, x: usize, y: usize) -> Option<usize> {
|
||||
(0..self.layers.len()).find(|&z| self.get(z, x, y).1 != Archetype::Empty)
|
||||
}
|
||||
|
||||
/// Clear the queues of all objects on this board: called when entering a board, objects
|
||||
/// don't retain their state across board visits (they get initialized again, but can
|
||||
/// store things in the board registry)
|
||||
@@ -638,20 +634,20 @@ impl Board {
|
||||
pub(crate) mod tests {
|
||||
use super::Board;
|
||||
use crate::archetype::{Archetype, Builtin};
|
||||
use crate::floor::Floor;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::layer::Layer;
|
||||
use crate::object_def::ObjectDef;
|
||||
use crate::utils::Direction;
|
||||
use crate::utils::{ObjectId, PlayerPos};
|
||||
use color::Rgba8;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
/// Builds an all-empty `w×h` single-layer board with the given player position
|
||||
/// and objects (all on layer 0). Assigns sequential ids (1..=n) to objects.
|
||||
/// Builds an all-empty `w×h` board with the given player position and objects.
|
||||
/// Assigns sequential ids (1..=n) to objects.
|
||||
///
|
||||
/// The single layer is fully transparent, so terrain stamped via [`crate_at`]
|
||||
/// etc. always lands on the board's top layer. Use [`add_floor`] to slip a
|
||||
/// visible floor layer underneath.
|
||||
/// The grid is fully transparent (a blank floor), so terrain stamped via
|
||||
/// [`crate_at`] etc. lands on the single grid. Use [`add_floor`] to give the
|
||||
/// board a visible fixed floor underneath.
|
||||
pub(crate) fn open_board(
|
||||
w: usize,
|
||||
h: usize,
|
||||
@@ -669,9 +665,9 @@ pub(crate) mod tests {
|
||||
name: "test".into(),
|
||||
width: w,
|
||||
height: h,
|
||||
layers: vec![Layer {
|
||||
cells: vec![(Glyph::transparent(), Archetype::Empty); w * h],
|
||||
}],
|
||||
grid: vec![(Glyph::transparent(), Archetype::Empty); w * h],
|
||||
floor: Floor::Blank,
|
||||
decorations: Vec::new(),
|
||||
player: PlayerPos {
|
||||
x: player.0,
|
||||
y: player.1,
|
||||
@@ -685,42 +681,25 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a visible floor layer (filled with `glyph`) below everything,
|
||||
/// bumping existing terrain and objects up one layer.
|
||||
/// Gives the board a uniform fixed floor glyph (the single-grid replacement for
|
||||
/// the old separate floor layer).
|
||||
pub(crate) fn add_floor(board: &mut Board, glyph: Glyph) {
|
||||
let count = board.width * board.height;
|
||||
board.layers.insert(
|
||||
0,
|
||||
Layer {
|
||||
cells: vec![(glyph, Archetype::Empty); count],
|
||||
},
|
||||
);
|
||||
for o in board.objects.values_mut() {
|
||||
o.z += 1;
|
||||
}
|
||||
board.floor = Floor::Fixed(glyph);
|
||||
}
|
||||
|
||||
/// The index of the board's top (terrain) layer, where stamps are written.
|
||||
fn top(board: &Board) -> usize {
|
||||
board.layers.len() - 1
|
||||
}
|
||||
|
||||
/// Stamps a crate cell onto the board's top layer.
|
||||
/// Stamps a crate cell onto the grid.
|
||||
pub(crate) fn crate_at(board: &mut Board, x: usize, y: usize) {
|
||||
let z = top(board);
|
||||
*board.get_mut(z, x, y) = (Archetype::Crate.default_glyph(), Archetype::Crate);
|
||||
*board.get_mut(x, y) = (Archetype::Crate.default_glyph(), Archetype::Crate);
|
||||
}
|
||||
|
||||
/// Stamps a wall cell onto the board's top layer.
|
||||
/// Stamps a wall cell onto the grid.
|
||||
pub(crate) fn wall_at(board: &mut Board, x: usize, y: usize) {
|
||||
let z = top(board);
|
||||
*board.get_mut(z, x, y) = (Archetype::Wall.default_glyph(), Archetype::Wall);
|
||||
*board.get_mut(x, y) = (Archetype::Wall.default_glyph(), Archetype::Wall);
|
||||
}
|
||||
|
||||
/// Stamps an arbitrary archetype cell onto the board's top layer.
|
||||
/// Stamps an arbitrary archetype cell onto the grid.
|
||||
pub(crate) fn stamp(board: &mut Board, x: usize, y: usize, arch: Archetype) {
|
||||
let z = top(board);
|
||||
*board.get_mut(z, x, y) = (arch.default_glyph(), arch);
|
||||
*board.get_mut(x, y) = (arch.default_glyph(), arch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -790,8 +769,8 @@ 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(0, 1, 0).1, Archetype::Crate); // no mutation
|
||||
assert_eq!(board.get(0, 2, 0).1, Archetype::Empty);
|
||||
assert_eq!(board.get(1, 0).1, Archetype::Crate); // no mutation
|
||||
assert_eq!(board.get(2, 0).1, Archetype::Empty);
|
||||
|
||||
let mut board = open_board(3, 1, (0, 0), vec![]);
|
||||
crate_at(&mut board, 1, 0);
|
||||
@@ -808,7 +787,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, 0).1, Archetype::Crate); // read-only
|
||||
assert_eq!(board.get(0, 0).1, Archetype::Crate); // read-only
|
||||
|
||||
// Crate with another pushable crate ahead: still shiftable (unlike can_push,
|
||||
// which would follow the chain to the wall and fail).
|
||||
@@ -854,8 +833,8 @@ pub(crate) mod tests {
|
||||
crate_at(&mut board, 1, 0);
|
||||
assert!(board.can_push(1, 0, Direction::East));
|
||||
board.push(1, 0, Direction::East);
|
||||
assert_eq!(board.get(0, 1, 0).1, Archetype::Empty);
|
||||
assert_eq!(board.get(0, 2, 0).1, Archetype::Crate);
|
||||
assert_eq!(board.get(1, 0).1, Archetype::Empty);
|
||||
assert_eq!(board.get(2, 0).1, Archetype::Crate);
|
||||
assert_eq!((board.player.x, board.player.y), (3, 0));
|
||||
}
|
||||
|
||||
@@ -867,7 +846,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(0, 1, 0).1, Archetype::Crate);
|
||||
assert_eq!(board.get(1, 0).1, Archetype::Crate);
|
||||
assert_eq!((board.player.x, board.player.y), (2, 0));
|
||||
}
|
||||
|
||||
@@ -890,7 +869,7 @@ pub(crate) mod tests {
|
||||
a: 255,
|
||||
},
|
||||
};
|
||||
// Floor on a lower layer, a wall on the top (terrain) layer at (0,0).
|
||||
// A fixed floor attribute, a wall on the grid at (0,0).
|
||||
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.
|
||||
@@ -898,29 +877,9 @@ pub(crate) mod tests {
|
||||
assert_eq!(board.glyph_at(1, 0), floor_glyph);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glyph_at_draws_higher_layer_over_lower() {
|
||||
// A non-solid object on an upper layer renders above a wall on a lower one.
|
||||
let mut board = open_board(2, 1, (1, 0), vec![]);
|
||||
wall_at(&mut board, 0, 0); // wall on layer 0
|
||||
// Add an upper layer holding a visible, non-solid object at (0,0).
|
||||
board.layers.push(Layer {
|
||||
cells: vec![(Glyph::transparent(), Archetype::Empty); 2],
|
||||
});
|
||||
let mut obj = ObjectDef::new(0, 0);
|
||||
obj.z = 1;
|
||||
obj.solid = false;
|
||||
obj.glyph = Glyph {
|
||||
tile: '*' as u32,
|
||||
..Glyph::transparent()
|
||||
};
|
||||
board.add_object(obj);
|
||||
assert_eq!(board.glyph_at(0, 0).tile, '*' as u32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn place_wall_keeps_floor_and_removes_solid_object() {
|
||||
// Floor on layer 0, terrain layer on top; a solid object sits at (1,0).
|
||||
// A fixed floor attribute; a solid object sits on the grid at (1,0).
|
||||
let mut board = open_board(3, 1, (2, 0), vec![ObjectDef::new(1, 0)]);
|
||||
let floor = Glyph {
|
||||
tile: '.' as u32,
|
||||
@@ -931,9 +890,8 @@ pub(crate) mod tests {
|
||||
let wall = Archetype::Wall.default_glyph();
|
||||
board.place_archetype(1, 0, Archetype::Wall, wall);
|
||||
|
||||
// The wall landed on the terrain (top) layer; the floor below is untouched.
|
||||
assert_eq!(board.get(1, 1, 0), &(wall, Archetype::Wall));
|
||||
assert_eq!(board.get(0, 1, 0).0, floor);
|
||||
// The wall landed on the grid; the floor attribute is untouched.
|
||||
assert_eq!(board.get(1, 0), &(wall, Archetype::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);
|
||||
@@ -941,17 +899,17 @@ pub(crate) mod tests {
|
||||
|
||||
#[test]
|
||||
fn place_wall_overwrites_existing_terrain_in_place() {
|
||||
// A crate already occupies the top layer at (1,0).
|
||||
// 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(0, 1, 0), &(wall, Archetype::Wall));
|
||||
assert_eq!(board.get(1, 0), &(wall, Archetype::Wall));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erase_removes_terrain_and_objects_but_keeps_floor() {
|
||||
// Floor, a wall on the terrain layer, and a (non-solid) object all at (1,0).
|
||||
// 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;
|
||||
let mut board = open_board(3, 1, (2, 0), vec![obj]);
|
||||
@@ -964,13 +922,12 @@ pub(crate) mod tests {
|
||||
|
||||
board.place_archetype(1, 0, Archetype::Empty, Glyph::transparent());
|
||||
|
||||
// Terrain cleared to transparent Empty; object removed; floor still there.
|
||||
// Grid cell cleared to transparent Empty; object removed; floor still there.
|
||||
assert_eq!(
|
||||
board.get(1, 1, 0),
|
||||
board.get(1, 0),
|
||||
&(Glyph::transparent(), Archetype::Empty)
|
||||
);
|
||||
assert!(board.object_ids_at(1, 0).is_empty());
|
||||
assert_eq!(board.get(0, 1, 0).0, floor);
|
||||
assert_eq!(board.glyph_at(1, 0), floor);
|
||||
}
|
||||
|
||||
@@ -990,7 +947,7 @@ pub(crate) mod tests {
|
||||
board.expand_builtin_archetypes();
|
||||
|
||||
// The terrain cell is vacated and a scripted object takes its place.
|
||||
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty);
|
||||
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);
|
||||
@@ -1019,7 +976,7 @@ pub(crate) mod tests {
|
||||
crate_at(&mut board, 0, 0);
|
||||
let errs = board.apply_shift(&[(0, 0), (9, 0)]);
|
||||
assert_eq!(errs.len(), 1);
|
||||
assert_eq!(board.get(0, 0, 0).1, Archetype::Crate); // unchanged
|
||||
assert_eq!(board.get(0, 0).1, Archetype::Crate); // unchanged
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1043,11 +1000,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, 0).1, Archetype::Crate); // wrapped from (4,0)
|
||||
assert_eq!(board.get(0, 1, 0).1, Archetype::Crate); // moved from (0,0)
|
||||
assert_eq!(board.get(0, 2, 0).1, Archetype::Wall); // blocked, immobile
|
||||
assert_eq!(board.get(0, 3, 0).1, Archetype::Empty); // cleared, crate moved
|
||||
assert_eq!(board.get(0, 4, 0).1, Archetype::Crate); // moved from (3,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(3, 0).1, Archetype::Empty); // cleared, crate moved
|
||||
assert_eq!(board.get(4, 0).1, Archetype::Crate); // moved from (3,0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1066,9 +1023,9 @@ pub(crate) mod tests {
|
||||
|
||||
let _errs = board.apply_shift(&[(0, 0), (1, 0), (2, 0)]);
|
||||
|
||||
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty); // HCrate moved out
|
||||
assert_eq!(board.get(0, 1, 0).1, Archetype::HCrate); // moved from (0,0)
|
||||
assert_eq!(board.get(0, 2, 0).1, Archetype::Crate); // moved from (1,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)
|
||||
}
|
||||
|
||||
// Subcase B: VCrate stays; Crate behind it still moves
|
||||
@@ -1084,9 +1041,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, 0).1, Archetype::VCrate); // immobile
|
||||
assert_eq!(board.get(0, 1, 0).1, Archetype::Empty); // crate moved out
|
||||
assert_eq!(board.get(0, 2, 0).1, Archetype::Crate); // moved from (1,0)
|
||||
assert_eq!(board.get(0, 0).1, Archetype::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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,9 +1062,9 @@ pub(crate) mod tests {
|
||||
|
||||
let _errs = board.apply_shift(&[(0, 0), (0, 1), (0, 2)]);
|
||||
|
||||
assert_eq!(board.get(0, 0, 0).1, Archetype::Empty); // VCrate moved out
|
||||
assert_eq!(board.get(0, 0, 1).1, Archetype::VCrate); // moved from (0,0)
|
||||
assert_eq!(board.get(0, 0, 2).1, Archetype::Crate); // moved from (0,1)
|
||||
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)
|
||||
}
|
||||
|
||||
// Subcase B: HCrate stays; Crate behind it still moves
|
||||
@@ -1123,9 +1080,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, 0).1, Archetype::HCrate); // immobile
|
||||
assert_eq!(board.get(0, 0, 1).1, Archetype::Empty); // crate moved out
|
||||
assert_eq!(board.get(0, 0, 2).1, Archetype::Crate); // moved from (0,1)
|
||||
assert_eq!(board.get(0, 0).1, Archetype::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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user