Different content grids

This commit is contained in:
2026-06-16 00:01:02 -05:00
parent 4929734212
commit 50d56aa428
5 changed files with 251 additions and 82 deletions
+117 -20
View File
@@ -34,16 +34,42 @@ pub(crate) struct Layer {
pub(crate) cells: Vec<(Glyph, Archetype)>,
}
/// Serde representation of one `[[layers]]` entry: a grid string and its palette.
/// Serde representation of one `[[layers]]` entry: a grid plus its palette.
///
/// The grid is given in exactly one of three ways (precedence: `content`, then
/// `fill`, then `sparse`; none of them ⇒ an all-spaces grid):
/// - `content` — a multi-line grid string, one char per cell (`width × height`).
/// - `fill` — a single character; the whole grid is filled with it (handy for a
/// layer of identical floor).
/// - `sparse` — a list of `{ x, y, ch }` cells over an otherwise all-spaces grid
/// (handy for a layer holding just a few objects).
#[derive(Deserialize, Serialize)]
pub(crate) struct LayerData {
/// Multi-line grid string; one char per cell, looked up in `palette`.
pub content: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// Single character to fill the whole `width × height` grid with.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fill: Option<String>,
/// Individual cells over an otherwise all-spaces grid.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sparse: Option<Vec<SparseCell>>,
/// Char (as a one-character string key) → palette entry.
#[serde(default)]
pub palette: HashMap<String, PaletteEntry>,
}
/// One cell in a [`LayerData::sparse`] list: a single character at `(x, y)`.
#[derive(Deserialize, Serialize, Clone)]
pub(crate) struct SparseCell {
/// Column (0-indexed).
pub x: usize,
/// Row (0-indexed).
pub y: usize,
/// The grid character at this cell (a one-character string).
pub ch: String,
}
/// One palette entry, discriminated by [`kind`](PaletteEntry::kind).
///
/// A single flat struct (rather than an enum) because `kind` is open-ended: it is
@@ -215,6 +241,91 @@ fn resolve_entry(ch: char, e: &PaletteEntry, errors: &mut Vec<LogLine>) -> Resol
}
}
/// Resolves a layer's grid to a `height × width` matrix of chars from whichever of
/// `content` / `fill` / `sparse` is supplied (in that precedence; none ⇒ all spaces).
///
/// Only an explicit `content` can mismatch the board dimensions — the single hard
/// error. `fill`/`sparse` always produce an exactly-sized grid; a non-single-char
/// `fill`/`ch` or an out-of-bounds `sparse` cell is recorded on `errors` and the
/// offending cell falls back to (or stays) a space.
fn grid_chars(
data: &LayerData,
width: usize,
height: usize,
errors: &mut Vec<LogLine>,
) -> Result<Vec<Vec<char>>, String> {
// Reads a one-character string field, recording `context` and returning `None`
// when it is empty or longer than one char.
let single_char = |s: &str, context: String, errors: &mut Vec<LogLine>| {
let mut chs = s.chars();
match (chs.next(), chs.next()) {
(Some(c), None) => Some(c),
_ => {
errors.push(LogLine::error(context));
None
}
}
};
if let Some(content) = &data.content {
// Explicit grid: validate it matches the declared dimensions.
let rows: Vec<&str> = content.lines().collect();
if rows.len() != height {
return Err(format!(
"layer grid has {} rows but the board is {height} tall",
rows.len()
));
}
let mut grid = Vec::with_capacity(height);
for (i, line) in rows.iter().enumerate() {
let row: Vec<char> = line.chars().collect();
if row.len() != width {
return Err(format!(
"layer grid row {i} has {} characters but the board is {width} wide",
row.len()
));
}
grid.push(row);
}
return Ok(grid);
}
if let Some(fill) = &data.fill {
// A whole grid of one character.
let ch = single_char(
fill,
format!("layer fill must be a single character (got {fill:?}); using a space"),
errors,
)
.unwrap_or(' ');
return Ok(vec![vec![ch; width]; height]);
}
// `sparse` (or nothing): an all-spaces grid with the listed cells stamped in.
let mut grid = vec![vec![' '; width]; height];
for cell in data.sparse.iter().flatten() {
let Some(ch) = single_char(
&cell.ch,
format!(
"sparse cell ch must be a single character (got {:?}); skipping",
cell.ch
),
errors,
) else {
continue;
};
if cell.x >= width || cell.y >= height {
errors.push(LogLine::error(format!(
"sparse cell ({}, {}) is out of bounds; skipping",
cell.x, cell.y
)));
continue;
}
grid[cell.y][cell.x] = ch;
}
Ok(grid)
}
/// Builds one [`Layer`] from its [`LayerData`], plus the non-terrain placements it
/// contains (with their `(x, y)`).
///
@@ -239,28 +350,14 @@ pub(crate) fn build_layer(
})
.collect();
// Validate dimensions before walking — the only fatal error.
let rows: Vec<&str> = data.content.lines().collect();
if rows.len() != height {
return Err(format!(
"layer grid has {} rows but the board is {height} tall",
rows.len()
));
}
for (i, line) in rows.iter().enumerate() {
let cols = line.chars().count();
if cols != width {
return Err(format!(
"layer grid row {i} has {cols} characters but the board is {width} wide"
));
}
}
// Resolve the grid (content / fill / sparse) before walking it.
let grid = grid_chars(data, width, height, errors)?;
// Walk the grid, filling cells and collecting placements.
let mut cells: Vec<(Glyph, Archetype)> = Vec::with_capacity(width * height);
let mut placements: Vec<Placement> = Vec::new();
for (y, line) in rows.iter().enumerate() {
for (x, ch) in line.chars().enumerate() {
for (y, row) in grid.iter().enumerate() {
for (x, &ch) in row.iter().enumerate() {
match resolved.get(&ch) {
Some(Resolved::Cell(g, a)) => cells.push((*g, *a)),
Some(Resolved::FloorGen(g)) => cells.push((g.generate(rng), Archetype::Empty)),