Add bitmap font rendering via PNG tilesheet

Replace egui text rendering with a BitmapFont system:
- Glyph.tile: u32 replaces ch: char; top-left pixel of PNG is background sentinel
- BitmapFont loads/preprocesses PNG to opaque-white + transparent, tinted at render time
- Default font: assets/vga-font-8x16.png (CP437, 8×16 tiles); placeholder generated if missing
- Boards can specify a per-board font via [font] in their .toml file
- Editor Board tab: "Font…" button opens font dialog (rfd file picker, tile dims, tilesheet preview)
- Glyph picker: tile grid from active font replaces hardcoded ASCII char grid
- map_file: TileIndex untagged enum accepts char or integer in palette entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 00:07:04 -05:00
parent 12ebe77932
commit 212297ecf9
14 changed files with 1174 additions and 260 deletions
+77 -28
View File
@@ -3,36 +3,55 @@ use serde::Deserialize;
/// The visual representation of a single board cell.
///
/// `Glyph` holds everything needed to draw one cell on screen: which character
/// to display and what colors to use. It is stored per-cell (not per archetype),
/// so individual cells can vary their appearance independently — for example,
/// a field of "fire" tiles where each flame has a slightly different color
/// while all sharing the same [`Archetype`] behavior.
/// `Glyph` holds everything needed to draw one cell on screen: which tile
/// index to display and what colors to use. It is stored per-cell (not per
/// archetype), so individual cells can vary their appearance independently.
///
/// `tile` is a left-to-right, top-to-bottom index into the board's bitmap
/// font. For the default CP437 font this matches the ASCII/CP437 code point.
///
/// `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)]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Glyph {
/// The character to display in this cell.
pub ch: char,
/// Foreground (text) color.
/// Tile index into the board's bitmap font (left-to-right, top-to-bottom).
pub tile: u32,
/// Foreground color, applied to non-background pixels of the tile.
pub fg: Color32,
/// Background color, drawn as a filled rectangle behind the character.
/// Background color, drawn as a filled rectangle behind the tile.
pub bg: Color32,
}
impl Glyph {
/// Returns the glyph used to render the player: `@` in light blue on black.
/// Returns the glyph used to render the player: tile 64 (`@`) in light blue on black.
///
/// This is the only hardcoded glyph; all other glyphs come from the map
/// file palette. It will be removed once the player becomes a scripted
/// object with its own palette entry.
pub fn player() -> Self {
Self { ch: '@', fg: Color32::LIGHT_BLUE, bg: Color32::BLACK }
pub const fn player() -> Self {
Self {
tile: 64,
fg: Color32::LIGHT_BLUE,
bg: Color32::BLACK,
}
}
}
/// Specifies a bitmap font for a board.
///
/// Each board can optionally specify a font image and tile dimensions.
/// When absent, the app's default embedded CP437 font is used.
#[derive(Clone, PartialEq, Eq)]
pub struct FontSpec {
/// Path to the PNG font image, relative to the working directory.
pub path: String,
/// Width of each tile in the font image, in pixels.
pub tile_w: u32,
/// Height of each tile in the font image, in pixels.
pub tile_h: u32,
}
/// The behavioral properties of a board cell at runtime.
///
/// `Behavior` is a plain data struct returned by [`Archetype::behavior`]. It
@@ -87,19 +106,31 @@ impl Archetype {
/// For `Object`, this is a placeholder until Rhai scripts drive the behavior.
pub fn behavior(&self) -> Behavior {
match self {
Archetype::Empty => Behavior { passable: true, opaque: false },
Archetype::Wall => Behavior { passable: false, opaque: true },
Archetype::Object => Behavior { passable: false, opaque: false },
Archetype::ErrorBlock => Behavior { passable: false, opaque: true },
Archetype::Empty => Behavior {
passable: true,
opaque: false,
},
Archetype::Wall => Behavior {
passable: false,
opaque: true,
},
Archetype::Object => Behavior {
passable: false,
opaque: false,
},
Archetype::ErrorBlock => Behavior {
passable: false,
opaque: true,
},
}
}
/// Returns the canonical name used to reference this archetype in map files.
pub fn name(&self) -> &'static str {
match self {
Archetype::Empty => "empty",
Archetype::Wall => "wall",
Archetype::Object => "object",
Archetype::Empty => "empty",
Archetype::Wall => "wall",
Archetype::Object => "object",
Archetype::ErrorBlock => "error_block",
}
}
@@ -110,11 +141,27 @@ impl Archetype {
/// cells retain their own per-cell glyph.
pub fn default_glyph(&self) -> Glyph {
match self {
Archetype::Empty => Glyph { ch: ' ', fg: Color32::BLACK, bg: Color32::BLACK },
Archetype::Wall => Glyph { ch: '#', fg: Color32::from_rgb(0x80, 0x80, 0x80), bg: Color32::from_rgb(0x60, 0x60, 0x60) },
Archetype::Object => Glyph { ch: '?', fg: Color32::YELLOW, bg: Color32::BLACK },
Archetype::Empty => Glyph {
tile: 32,
fg: Color32::BLACK,
bg: Color32::BLACK,
},
Archetype::Wall => Glyph {
tile: 35,
fg: Color32::from_rgb(0x80, 0x80, 0x80),
bg: Color32::from_rgb(0x60, 0x60, 0x60),
},
Archetype::Object => Glyph {
tile: 63,
fg: Color32::YELLOW,
bg: Color32::BLACK,
},
// Visually distinct so malformed map files are immediately obvious.
Archetype::ErrorBlock => Glyph { ch: '?', fg: Color32::YELLOW, bg: Color32::RED },
Archetype::ErrorBlock => Glyph {
tile: 63,
fg: Color32::YELLOW,
bg: Color32::RED,
},
}
}
}
@@ -128,10 +175,10 @@ impl TryFrom<&str> for Archetype {
/// [`Archetype::ErrorBlock`] and log the error so the problem is visible.
fn try_from(name: &str) -> Result<Self, Self::Error> {
match name {
"empty" => Ok(Archetype::Empty),
"wall" => Ok(Archetype::Wall),
"empty" => Ok(Archetype::Empty),
"wall" => Ok(Archetype::Wall),
"object" => Ok(Archetype::Object),
_ => Err(format!("unknown archetype: {name}")),
_ => Err(format!("unknown archetype: {name}")),
}
}
}
@@ -230,6 +277,8 @@ pub struct Board {
pub objects: Vec<ObjectDef>,
/// Portals on this board. Parsed from the map file; not yet active.
pub portals: Vec<PortalDef>,
/// Optional font override for this board. When `None`, the app default is used.
pub font: Option<FontSpec>,
}
impl Board {
@@ -295,4 +344,4 @@ impl GameState {
}
}
}
}
}