Floor layer

This commit is contained in:
2026-06-06 18:49:45 -05:00
parent a5f60e22e1
commit 374a69f0ca
8 changed files with 699 additions and 33 deletions
+113 -7
View File
@@ -20,7 +20,7 @@ use std::time::Duration;
/// `Glyph` values come from the map file palette and are set at load time.
/// The player is the only entity whose glyph is hardcoded at runtime
/// (see [`Glyph::player`]).
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Glyph {
/// Tile index into the board's bitmap font (left-to-right, top-to-bottom).
pub tile: u32,
@@ -428,6 +428,16 @@ pub struct Board {
/// Row-major grid of `(Glyph, Archetype)` pairs. Use [`Board::get`] to
/// access by `(x, y)` coordinates.
pub(crate) cells: Vec<(Glyph, Archetype)>,
/// Row-major cache of the visual floor layer: the glyph drawn for a cell when
/// it would otherwise render as [`Archetype::Empty`]. Computed once at load
/// from [`floor_spec`](Board::floor_spec) (see [`crate::floor::build_floor`]).
/// When a map declares no `[floor]`, every entry is black-on-black space (the
/// historical look of an empty cell). Read it via [`Board::display_glyph`].
pub(crate) floor: Vec<Glyph>,
/// The raw `[floor]` declaration, kept so [`crate::map_file::save`] can
/// round-trip it. `None` when the map declared no floor. (Generators
/// re-randomize on reload; literal glyph grids are preserved exactly.)
pub(crate) floor_spec: Option<crate::floor::FloorSpec>,
/// Current player position. See [`Player`] for caveats about its future.
pub player: Player,
/// Scripted objects on this board. Parsed from the map file; not yet active.
@@ -472,6 +482,23 @@ impl Board {
&mut self.cells[y * self.width + x]
}
/// Returns the glyph to draw for the **static layer** of cell `(x, y)` — the
/// grid archetype, or the floor underneath an [`Archetype::Empty`] cell.
///
/// This is the single source of truth front-ends use for the non-object,
/// non-player layer: a wall/crate draws its own per-cell glyph, while an empty
/// cell draws its [`floor`](Board::floor) glyph. An `Empty` cell's *own* glyph
/// is intentionally ignored — the floor layer supersedes it (so a cell vacated
/// by a pushed crate reveals the floor, not black). Panics if out of bounds.
pub fn display_glyph(&self, x: usize, y: usize) -> Glyph {
let (glyph, arch) = self.get(x, y);
if *arch == Archetype::Empty {
self.floor[y * self.width + x]
} else {
*glyph
}
}
/// Returns `true` if `(x, y)` is a valid cell coordinate on this board.
///
/// Takes signed coords so callers can pass a raw `pos + delta` without first
@@ -836,6 +863,8 @@ mod tests {
width: 1,
height: 1,
cells: vec![(Archetype::Empty.default_glyph(), Archetype::Empty)],
floor: vec![Archetype::Empty.default_glyph()],
floor_spec: None,
player: Player { x: 0, y: 0 },
objects: vec![object],
portals: Vec::new(),
@@ -872,6 +901,8 @@ mod tests {
width: w,
height: h,
cells: vec![(Archetype::Empty.default_glyph(), Archetype::Empty); w * h],
floor: vec![Archetype::Empty.default_glyph(); w * h],
floor_spec: None,
player: Player {
x: player.0,
y: player.1,
@@ -1352,16 +1383,25 @@ mod tests {
let mut game = GameState::new(board);
game.run_init();
// First (eastward) move is blocked by the wall: object hasn't moved.
assert_eq!((game.board().objects[0].x, game.board().objects[0].y), (1, 1));
assert_eq!(
(game.board().objects[0].x, game.board().objects[0].y),
(1, 1)
);
// The blocked move charged the cooldown, so South is still pending here.
game.tick(Duration::from_millis(100));
game.tick(Duration::from_millis(100));
assert_eq!((game.board().objects[0].x, game.board().objects[0].y), (1, 1));
assert_eq!(
(game.board().objects[0].x, game.board().objects[0].y),
(1, 1)
);
// Past 250 ms the queued South move resolves.
game.tick(Duration::from_millis(100));
assert_eq!((game.board().objects[0].x, game.board().objects[0].y), (1, 2));
assert_eq!(
(game.board().objects[0].x, game.board().objects[0].y),
(1, 2)
);
}
#[test]
@@ -1373,7 +1413,10 @@ mod tests {
1,
(0, 0),
vec![scripted_object(1, 0, "q")],
&[("q", "fn init() { set_tile(5); set_tile(6); log(`len=${Queue.length()}`); }")],
&[(
"q",
"fn init() { set_tile(5); set_tile(6); log(`len=${Queue.length()}`); }",
)],
);
let mut game = GameState::new(board);
game.run_init();
@@ -1405,7 +1448,10 @@ mod tests {
1,
(0, 0),
vec![scripted_object(1, 0, "b")],
&[("b", "fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }")],
&[(
"b",
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
)],
);
wall_at(&mut board, 2, 0);
let mut game = GameState::new(board);
@@ -1418,7 +1464,10 @@ mod tests {
1,
(0, 0),
vec![scripted_object(1, 0, "b")],
&[("b", "fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }")],
&[(
"b",
"fn init() { if blocked(East) { set_tile(9); } else { set_tile(7); } }",
)],
);
let mut game = GameState::new(board);
game.run_init();
@@ -1507,6 +1556,63 @@ mod tests {
assert!(log_texts(&game).iter().any(|t| t == "bumped by -1"));
}
#[test]
fn display_glyph_uses_floor_for_empty_and_grid_for_solid() {
// A board with a distinctive floor glyph; empty cells show the floor, while
// a wall keeps drawing its own grid glyph (floor ignored under solids).
let mut board = open_board(2, 1, (1, 0), vec![], &[]);
let floor_glyph = Glyph {
tile: '.' as u32,
fg: Rgba8 {
r: 10,
g: 20,
b: 30,
a: 255,
},
bg: Rgba8 {
r: 1,
g: 2,
b: 3,
a: 255,
},
};
board.floor = vec![floor_glyph; 2];
wall_at(&mut board, 0, 0);
assert_eq!(board.display_glyph(0, 0), Archetype::Wall.default_glyph()); // solid: grid glyph
assert_eq!(board.display_glyph(1, 0), floor_glyph); // empty: floor glyph
}
#[test]
fn pushing_a_crate_reveals_the_floor_underneath() {
// The cell a crate is pushed off of becomes Empty, so its `display_glyph`
// should show the floor glyph rather than black-on-black.
let mut board = open_board(4, 1, (0, 0), vec![], &[]);
let floor_glyph = Glyph {
tile: ',' as u32,
fg: Rgba8 {
r: 40,
g: 60,
b: 40,
a: 255,
},
bg: Rgba8 {
r: 5,
g: 10,
b: 5,
a: 255,
},
};
board.floor = vec![floor_glyph; 4];
crate_at(&mut board, 1, 0);
let mut game = GameState::new(board);
game.try_move(Direction::East); // player at (0,0) pushes the crate east
let b = game.board();
assert_eq!(b.get(2, 0).1, Archetype::Crate); // crate moved one east
// The vacated cell (1,0) is now Empty and reveals the floor.
assert_eq!(b.get(1, 0).1, Archetype::Empty);
assert_eq!(b.display_glyph(1, 0), floor_glyph);
}
#[test]
fn solid_at_reports_player() {
// The player's own cell is solid (and thus impassable to movers).