basic drawing

This commit is contained in:
2026-06-20 17:53:47 -05:00
parent a1dc215712
commit 0dc69c7ebb
4 changed files with 251 additions and 40 deletions
+100
View File
@@ -329,6 +329,50 @@ impl Board {
self.objects.insert(id, object);
id
}
/// Editor primitive: stamps `arch` (with visual `glyph`) into the cell at
/// `(x, y)`, applying the editor's placement/removal rules.
///
/// Two cases, keyed only on the archetype (the floor is **never** touched — the
/// 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.
///
/// 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.
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).
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);
}
return;
}
// Placing solid terrain: a solid object can't share the cell, so drop it.
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);
}
/// 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)
}
}
#[cfg(test)]
@@ -559,6 +603,62 @@ pub(crate) mod tests {
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).
let mut board = open_board(3, 1, (2, 0), vec![ObjectDef::new(1, 0)]);
let floor = Glyph {
tile: '.' as u32,
..Glyph::transparent()
};
add_floor(&mut board, floor);
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 solid object that was there is gone.
assert!(board.object_ids_at(1, 0).is_empty());
assert_eq!(board.glyph_at(1, 0), wall);
}
#[test]
fn place_wall_overwrites_existing_terrain_in_place() {
// A crate already occupies the top layer 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));
}
#[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).
let mut obj = ObjectDef::new(1, 0);
obj.solid = false;
let mut board = open_board(3, 1, (2, 0), vec![obj]);
let floor = Glyph {
tile: '.' as u32,
..Glyph::transparent()
};
add_floor(&mut board, floor);
wall_at(&mut board, 1, 0);
board.place_archetype(1, 0, Archetype::Empty, Glyph::transparent());
// Terrain cleared to transparent Empty; object removed; floor still there.
assert_eq!(
board.get(1, 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);
}
#[test]
fn fresh_board_is_valid_and_reports_errors() {
let mut board = open_board(1, 1, (0, 0), vec![]);
+3 -2
View File
@@ -2,10 +2,10 @@ mod action;
mod archetype;
mod board;
mod builtin_scripts;
/// CP437 tile-index → character mapping ([`cp437::tile_to_char`]) for the default font.
pub mod cp437;
/// The 16 EGA/VGA named colors ([`colors::NAMED_COLORS`]), shared by scripts and the editor.
pub mod colors;
/// CP437 tile-index → character mapping ([`cp437::tile_to_char`]) for the default font.
pub mod cp437;
/// Procedural floor generators ([`floor::FloorGenerator`]).
pub mod floor;
/// Core game types: [`board::Board`], [`glyph::Glyph`], [`archetype::Archetype`], etc.
@@ -23,6 +23,7 @@ mod utils;
/// World type: a named collection of boards in a single `.toml` file ([`world::World`], [`world::load`]).
pub mod world;
pub use archetype::Archetype;
pub use board::Board;
pub use utils::Direction;