`kiln` is a ZZT-inspired game-making system written in Rust (edition 2024). The goal is a system where players can create games using a scripting language, similar to the classic DOS game ZZT. It uses **Rhai** for scripting (WASM-compatible, sandboxed, pure Rust).
- Add `///` rustdoc comments to every `pub` type, field, and function. The user reads rustdoc in their IDE to understand the code while making changes.
- Add inline `//` comments inside non-trivial function bodies to explain the *why* of each logical step — especially in `update` loops, rendering math, and conversion logic.
- Keep comments accurate: update them when the code they describe changes.
- **`kiln-core`** — the engine: all core game types (`Board`, `Glyph`, `Archetype`, `GameState`, …) plus `.toml` map-file load/save. No rendering or UI; every front-end depends on it.
- **`kiln-tui`** — a terminal **player** (no editor yet) built on **ratatui 0.30 / crossterm**. Takes a map-file path on the command line and lets you walk the player around the board with the arrow keys.
- **`kiln-egui`** — the original eframe/egui desktop app (player + editor). **Still in the tree but no longer a workspace member**: it was removed from the root `Cargo.toml``members` and its files were left untouched, so it is not built at the root and is not guaranteed to compile against the current engine. Its notes below are retained for when it is revived.
-`Glyph` (`Copy`, `Eq`, `Hash`) — per-cell visual: `tile: u32` (tilesheet index), `fg/bg: Color32`. `Glyph::player()` is a `const fn`; all other glyphs come from the map file. Derives `Hash` so boards can deduplicate glyphs into a palette.
-`FontSpec` — optional per-board bitmap font override: `path: String`, `tile_w: u32`, `tile_h: u32`. When `None`, the app default font is used.
-`Behavior` — plain data struct of runtime behavioral properties: `passable: bool`, `opaque: bool`. Returned by `Archetype::behavior()`; new properties added here require no match arms elsewhere.
-`Archetype` (`Copy`, `PartialEq`) — enum of named element types: `Empty`, `Wall`, `ErrorBlock`. Each variant provides `behavior()`, `name()` (used in map files), and `default_glyph()` (used by the editor when stamping a cell). `ErrorBlock` is a sentinel for unknown archetype names — renders as yellow `?` on red.
-`Board` — the complete game unit (ZZT-style "board"): `width`, `height`, `cells: Vec<(Glyph, Archetype)>` (row-major; each cell owns its visual and behavioral class directly), `player: Player`, `objects: Vec<ObjectDef>`, `portals: Vec<PortalDef>`, `font: Option<FontSpec>`. `cells` is `pub(crate)`. `is_passable(x, y)` checks objects before the grid cell: an impassable object blocks even over a passable floor tile.
-`MapFile` and friends — serde `Deserialize` types for TOML map files
-`TileIndex` — `#[serde(untagged)]` enum accepting either `Num(u32)` or `Chr(char)`; lets map files use `tile = " "` (char) or `tile = 32` (integer) interchangeably
-`FontHeader` — deserializes the optional `[font]` section (`path`, `tile_w`, `tile_h`)
-`impl From<MapFile> for Board` — converts a parsed file into a ready-to-use `Board`; unknown archetype names produce `ErrorBlock`
-`pub fn load(path: &str) -> Result<Board, Box<dyn std::error::Error>>` — reads and converts a `.toml` map file
-`pub fn save(board: &Board, path: &Path) -> Result<(), Box<dyn std::error::Error>>` — serializes `Board` back to TOML and writes to disk
### kiln-tui modules
The terminal player. Renders the board as text: each `Glyph.tile` index is reinterpreted as a character (the default kiln font is CP437, where the tile index equals the code point) and drawn with the glyph's RGB colors. No bitmap font or UV mapping is involved.
**`kiln-tui/src/main.rs`** — entry point and play loop:
- Parses a single positional arg (the map path); prints usage and exits non-zero if missing. Loads the board via `kiln_core::map_file::load`*before* touching the terminal so load errors print to a normal screen.
-`ratatui::init()` → detect `TerminalCaps` → push Kitty flags when supported → `run` loop → pop Kitty flags → `ratatui::restore()`.
-`run` blocks on `event::read()`; it acts on `Press`**and**`Repeat` key events (so holding a key moves continuously) and ignores `Release` (which the Kitty protocol also emits, and which would otherwise double-fire each move). Arrow keys or `hjkl` move the player via `GameState::try_move`; `q`/`Esc` quit.
-`draw` wraps the board in a bordered `Block` titled with the board name and controls; `caps.summary_line()` is shown right-aligned in the bottom border.
-`BoardWidget<'a>` — a `ratatui::widgets::Widget` that draws the board. Per axis, `axis(board_len, view, player) -> (offset, pad, count)`: a board smaller than the viewport is **centered** with screen padding, a larger one **scrolls** to keep the player on screen with no empty margins. Each cell resolves glyph priority player > object > grid floor, then writes char + fg/bg into the buffer.
**`kiln-tui/src/term.rs`** — terminal capability detection and Kitty setup:
-`TerminalCaps { keyboard_enhancement: bool, truecolor: bool }` — detected once at startup. `keyboard_enhancement` is a real query/response (`supports_keyboard_enhancement()`); `truecolor` reads `$COLORTERM`. Intended to be handed to scripts so they can pick bindings the terminal actually supports.
-`summary_line() -> Line` — styled one-line badge for the bottom border; when `truecolor`, the word "truecolor" is drawn with each letter a different rainbow color (`hue_to_rgb`, an HSV→RGB helper), else a plain "256-color".
-`push_kitty_flags()` / `pop_kitty_flags()` — enable/disable the Kitty keyboard protocol (`DISAMBIGUATE_ESCAPE_CODES | REPORT_EVENT_TYPES | REPORT_ALTERNATE_KEYS | REPORT_ALL_KEYS_AS_ESCAPE_CODES`); the last flag makes modifier-only keypresses observable. Pushed only when supported, popped before restore. Enabling `REPORT_EVENT_TYPES` is why the input loop must filter out `Release` events.
### kiln-egui modules (no longer a workspace member)
**`kiln-egui/src/font.rs`** — bitmap font loading and UV mapping:
-`BitmapFont` — wraps an egui `TextureHandle` (preprocessed to opaque-white / transparent) plus tile dimensions. The top-left pixel of the source PNG defines the "background" color; those pixels become `TRANSPARENT`, all others become `WHITE`. At render time, `painter.image(…, tile_uv, fg_color)` tints white pixels to `fg` and transparent pixels reveal the `bg` fill behind.
-`BitmapFont::load(ctx, path, tile_w, tile_h)` — loads from disk
-`BitmapFont::from_bytes(ctx, bytes, tile_w, tile_h, label)` — loads from embedded bytes
-`BitmapFont::create_placeholder(ctx)` — 16×16 grid of 8×8 procedural tiles (bit-pattern bars), used as fallback when no font file is present
-`BitmapFont::tile_uv(tile)` — returns a UV `Rect` for the given tile index (left-to-right, top-to-bottom). Unit-tested via `compute_tile_uv`.
-`BitmapFont::cell_size(zoom)` — pixel size (`Vec2`) of one rendered cell at the given integer zoom; centralizes the `tile_w * zoom` math used across rendering and hit-testing
-`FontDialogState::from_spec(spec)` — initializes from an existing `FontSpec` or defaults (8×16, empty path)
-`show(ctx, open, state, font_spec)` — floating dialog with file browser (`rfd::FileDialog`), tile dimension `DragValue` controls, scrollable tilesheet preview with grid overlay, and Apply / Use default buttons. Uses a `should_close` flag to avoid the egui `.open()` borrow conflict.
-`App` holds `GameState`, `AppMode`, `EditorState`, `default_font: BitmapFont`, and `board_font: Option<BitmapFont>`
-`App::new(board, egui_ctx)` — loads `assets/vga-font-8x16.png` as the default font (falls back to `create_placeholder`); loads per-board font from `board.font` if present
-`App::update` is a thin sequence of phase methods: `handle_input` (table-driven arrow keys, Play only) → `menu_bar` (File Save/Save As via `save_to`/`save_as`, Exit, mode toggle) → `try_show_script_editor` (returns `true` to take over the frame) → editor side panel + font reload → `show_board` → `handle_board_click` → `show_glyph_pickers` → object-glyph writeback
-`App::active_font()` — resolves the per-board font or the default; used wherever an exclusive borrow of `self` is not also needed
-`App::show_board(&self) -> Option<(usize, usize)>` — draws the viewport and returns the clicked cell (Edit mode); the click is dispatched to `handle_board_click(&mut self, cx, cy)`*after*`show_board` returns, so the `&mut` handler never conflicts with the font borrow
- Borrow split: at the editor-panel and glyph-picker call sites, `self.board_font.as_ref().unwrap_or(&self.default_font)` is used inline (not `active_font()`) so the font borrow stays disjoint from the `&mut self.editor`/`&mut board` borrows
- Window sizing constants (`DEFAULT_WINDOW_W = 840`, `DEFAULT_WINDOW_H = 524`, `MIN_WINDOW_W/H`); no fixed `CELL_W/H` — cell size comes from the active `BitmapFont`
-`glyph_preview_button(ui, glyph, font) -> Response` — allocates a one-cell swatch (unzoomed), paints `glyph`, returns the click response; used by the Palette and Objects tabs
-`draw_glyph(painter, origin, x, y, glyph, font, zoom)` — sizes cell via `font.cell_size(zoom)`, delegates to `paint_glyph`
-`draw_board(painter, origin, board, font, zoom)` — draws all cells then player overlay
-`draw_object_overlays(painter, origin, board, font, selected, zoom)` — highlights all object cells in the Objects editor tab; the selected object gets a brighter border
-`board_origin(available, board_w, board_h, player, font, zoom)` — centers board or clamps to player with no empty space
-`show_editor_panel(ctx, editor, board, active_font)` — resizable right-side panel (default 200 px); renders the tab bar then dispatches to a per-tab function (`palette_tab`, `objects_tab`, `board_tab`, `scripts_tab`; World is empty), passing the relevant sub-struct(s). Palette shows the archetype list and glyph preview button; Board shows the font path, "Font…" button, and zoom slider; Objects lists the selected object's glyph preview, **Passable/Opaque checkboxes**, and script combobox; Scripts creates/edits named scripts
-`show(ctx, open, glyph, board_cells, font)` — takes `open: &mut bool` and `glyph: &mut Glyph` directly; no dependency on `EditorState`
- Three sections: board palette (unique glyphs deduped via `HashSet<Glyph>`, click to select), FG/BG color pickers, scrollable tile grid (all tiles in the active font at `tile_w × tile_h` per cell)
XPM-inspired: a `[palette]` maps single characters to `(Glyph, Archetype)` definitions; `[grid] content` is a TOML multi-line string where each character indexes the palette.
Colors are `"#RRGGBB"` hex strings. `player_start` is a header field — the player is not a board cell. The `tile` field accepts either a single-character string (`tile = " "`) or an integer (`tile = 35`); both are valid and existing char-style map files continue to work. The grid multi-line string's leading newline is trimmed by TOML; trailing newline is handled correctly by `str::lines()`. Unknown archetype names produce an `ErrorBlock` cell and a logged warning.
- **`Behavior` and `Archetype` are separate types** — `Archetype` is the named class of a thing (`Wall`, `Empty`, `Object`); `Behavior` is its runtime properties (`passable`, `opaque`). Adding a new property means adding a field to `Behavior`, not a match arm at every call site.
- **`cells: Vec<(Glyph, Archetype)>`** — each cell owns its visual and behavioral class directly; there is no per-board element palette or integer indirection. `Archetype` is `Copy` so this is efficient.
- **Archetypes are referenced by name in map files** — so `ALL_ARCHETYPES` can be reordered or extended without breaking saved games.
- **`Board` is the complete unit** — grid, player, objects, and portals all live on `Board`, matching how ZZT treats a "board". No separate wrapper struct.
- **Glyph (visual) and Archetype (behavior) are decoupled** — each cell has its own `Glyph` (so colors can vary per-cell, e.g. fire flickering) while sharing an `Archetype` with other cells of the same type.