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
+68 -4
View File
@@ -1,3 +1,4 @@
use crate::floor::{FloorSpec, build_floor};
use crate::game::{Archetype, Board, FontSpec, Glyph, ObjectDef, Player, PortalDef};
use crate::log::LogLine;
use color::Rgba8;
@@ -25,6 +26,10 @@ pub struct MapFile {
/// Optional `[font]` section specifying a bitmap font for this board.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub font: Option<FontHeader>,
/// Optional `[floor]` declaration: the visual floor layer (a generator name,
/// a single glyph, or a grid with its own palette). See [`FloorSpec`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub floor: Option<FloorSpec>,
/// Any `[[objects]]` entries. Optional; defaults to empty.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) objects: Vec<MapFileObjectEntry>,
@@ -76,9 +81,9 @@ pub struct FontHeader {
/// `tile = " "` for convenience (the char is converted to its Unicode scalar).
/// This means existing maps that used `ch = "#"` can be migrated by renaming
/// the key to `tile`; the char form keeps working.
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, Clone, Copy)]
#[serde(untagged)]
pub(crate) enum TileIndex {
pub enum TileIndex {
/// A direct tile index (e.g. `tile = 35`).
Num(u32),
/// A single-character shorthand (e.g. `tile = "#"`); converted to its Unicode scalar.
@@ -195,7 +200,7 @@ fn is_zoom_default(z: &u32) -> bool {
/// Parses an `"#RRGGBB"` hex color string into an [`Rgba8`].
/// Returns opaque black on any parse failure.
fn parse_color(hex: &str) -> Rgba8 {
pub(crate) fn parse_color(hex: &str) -> Rgba8 {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Rgba8 {
@@ -212,7 +217,7 @@ fn parse_color(hex: &str) -> Rgba8 {
}
/// Converts an [`Rgba8`] to an `"#RRGGBB"` hex string (alpha is ignored).
fn color_to_hex(color: Rgba8) -> String {
pub(crate) fn color_to_hex(color: Rgba8) -> String {
format!("#{:02X}{:02X}{:02X}", color.r, color.g, color.b)
}
@@ -409,6 +414,12 @@ impl TryFrom<MapFile> for Board {
tile_h: f.tile_h,
});
// Build the cached floor layer from the (optional) `[floor]` declaration.
// Best-effort: any problem is recorded on `load_errors`, not fatal. The
// raw spec is kept on the board so a save can round-trip it.
let floor_spec = mf.floor;
let floor = build_floor(&floor_spec, w, h, &mut load_errors);
// Convert MapFileObjectEntry → ObjectDef, enforcing one solid per cell.
// `solid_occupied` is seeded from the grid (solid archetypes); each solid
// object then claims its cell, and a second solid on a cell is dropped.
@@ -513,6 +524,8 @@ impl TryFrom<MapFile> for Board {
width: w,
height: h,
cells,
floor,
floor_spec,
player: Player {
x: px as i32,
y: py as i32,
@@ -648,6 +661,9 @@ impl From<&Board> for MapFile {
palette,
grid: GridData { content },
font,
// Round-trip the original floor declaration verbatim (generators
// re-randomize on the next load; literal glyph grids are exact).
floor: board.floor_spec.clone(),
objects,
portals,
scripts: board.scripts.clone(),
@@ -1026,4 +1042,52 @@ bg = "#000000"
assert_eq!(obj2.glyph.fg, obj.glyph.fg);
assert_eq!(obj2.glyph.bg, obj.glyph.bg);
}
#[test]
fn floor_spec_round_trips_through_toml() {
// A 2×2 board with a grid floor (a generator and a fixed glyph) must survive
// save→load with its `[floor]` declaration intact, and the reloaded board's
// computed floor must place the fixed glyph back at its declared cell.
let toml = r##"
[map]
name = "Test"
width = 2
height = 2
player_start = [0, 0]
[palette]
"." = { archetype = "empty", tile = 46, fg = "#000000", bg = "#000000" }
[grid]
content = """
..
..
"""
[floor]
content = """
g.
.g
"""
[floor.palette]
"g" = "grass"
"." = { tile = "#", fg = "#010203", bg = "#040506" }
"##;
let board = load_board(toml);
assert!(board.floor_spec.is_some());
let fixed = Glyph {
tile: '#' as u32,
fg: parse_color("#010203"),
bg: parse_color("#040506"),
};
// (1,0) is the fixed `.` floor glyph (the cell archetype is Empty).
assert_eq!(board.display_glyph(1, 0), fixed);
// Save back to TOML and reload; the floor declaration must be preserved.
let toml_out = toml::to_string_pretty(&MapFile::from(&board)).unwrap();
let board2 = load_board(&toml_out);
assert!(board2.floor_spec.is_some());
assert_eq!(board2.display_glyph(1, 0), fixed);
}
}