Floor layer
This commit is contained in:
+113
-7
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user