diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index fecaec6..36cacf4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -52,7 +52,7 @@ Optional per-board font override: `path: String`, `tile_w: u32`, `tile_h: u32`. A plain data struct of runtime behavioral properties — currently `passable: bool` and `opaque: bool`. Returned by `Archetype::behavior()`. Adding a new property (e.g. `shootable`) requires only a new field here; no match arms needed at call sites. This is a deliberate improvement over storing behavior as a bag of booleans on a per-cell or per-archetype basis. **`Archetype`** -An enum of named element types: `Empty`, `Wall`, `Object`, `ErrorBlock`. Each variant knows: +An enum of named element types: `Empty`, `Wall`, `ErrorBlock`. Each variant knows: - Its canonical map-file name (e.g. `"wall"`) via `name()` - Its default `Behavior` via `behavior()` - Its default `Glyph` via `default_glyph()` — used by the editor when stamping a cell @@ -69,10 +69,13 @@ In ZZT, each board tile had both a visual (character + color pair) and an elemen The complete unit of a game world — one "room" or "screen" in ZZT terminology. Holds: - `cells: Vec<(Glyph, Archetype)>` — row-major grid; each cell directly owns its visual and behavioral class - `player: Player` — current player position on this board -- `objects: Vec` — scripted objects (parsed, not yet runtime-wired) +- `objects: Vec` — scripted objects (parsed; scripts not yet runtime-wired, but `passable`/`opaque` are live and affect collision) - `portals: Vec` — exits to other boards (parsed, not yet runtime-wired) - `font: Option` — per-board font override; `None` means use the app default +**`ObjectDef`** +A scripted object placed on the board. Holds `x`, `y`, `glyph: Glyph` (owned by the object so scripts can change its appearance), `passable: bool`, `opaque: bool`, and `script_name: Option`. `passable` defaults `false` and `opaque` defaults `true` in map files (objects block by default). `Board::is_passable` checks objects before consulting the grid cell's `Archetype::behavior()`, so an impassable object blocks movement even when placed over a passable floor tile. The `passable` and `opaque` flags are editable via checkboxes in the editor's Objects tab. + **Why `cells` is `Vec<(Glyph, Archetype)>` with no palette:** An earlier design had `cells: Vec<(Glyph, usize)>` where the `usize` indexed a per-board `elements: Vec` palette. This was eliminated because the palette indirection added complexity without benefit: `Archetype` is a `Copy` enum (4 bytes), so storing it per-cell is as efficient as storing an index, and it removes the invariant that the index must stay in sync with a specific board's palette. @@ -167,6 +170,8 @@ All pixel-level rendering knowledge lives here. No dependency on `EditorState` o **`draw_board(painter, origin, board, font)`** — iterates all cells calling `draw_glyph`, then draws the player as an overlay on top. +**`draw_object_overlays(painter, origin, board, font, selected)`** — called from Edit mode when the Objects tab is active. Draws a dim border on every object cell and a bright border on the selected one, making objects discoverable without obscuring the underlying tile. + **`pos_to_cell(origin, pos, tile_w, tile_h) -> (i32, i32)`** — converts a pixel position to cell coordinates using floor division. Returns negative values for clicks above/left of the origin; callers must guard `>= 0`. Mirrors the rendering math so clicks land on the correct cell. **`board_origin(available, board_w, board_h, player, font) -> Pos2`** — computes the pixel origin for Play-mode rendering (centering or player-tracking with edge clamping). @@ -175,7 +180,7 @@ All pixel-level rendering knowledge lives here. No dependency on `EditorState` o ### `editor.rs` — editor state and side panel -**`EditorTab`** (`Palette` | `Board` | `World`) — which tab is active in the side panel. +**`EditorTab`** (`Palette` | `Board` | `Objects` | `World`) — which tab is active in the side panel. **`EditorState`** — transient editor state: - `selected: Archetype` — the archetype class to stamp @@ -184,8 +189,12 @@ All pixel-level rendering knowledge lives here. No dependency on `EditorState` o - `tab: EditorTab` — which side-panel tab is active - `font_dialog_open: bool` — whether the font picker dialog is visible - `font_dialog_state: FontDialogState` — in-progress edits for the font dialog +- `selected_object: Option` — index into `board.objects` of the currently selected object +- `object_glyph_picker_open: bool` — whether the object glyph picker is open +- `object_editing_glyph: Glyph` — local copy of the selected object's glyph; written back to `board.objects[i].glyph` every frame +- `placing_object: bool` — when `true`, the next board click creates a new `ObjectDef` at that cell -**`show_editor_panel(ctx, editor, font_spec, active_font)`** — renders the resizable right-side panel (default 200 px). Palette tab: scrollable archetype list and glyph preview button. Board tab: current font path label + "Font…" button that initializes `font_dialog_state` from `font_spec` and opens the font dialog. World tab: placeholder. +**`show_editor_panel(ctx, editor, board, active_font)`** — renders the resizable right-side panel (default 200 px). Palette tab: scrollable archetype list and glyph preview button. Board tab: current font path label + "Font…" button. Objects tab: "Add Object" button that enters placement mode; lists objects by coordinate with click-to-select; for the selected object shows glyph preview, **Passable** and **Opaque** checkboxes (writing directly to `board.objects[i]`), and a script combobox. World tab: placeholder. --- @@ -268,7 +277,7 @@ A direct mapping from palette character → `(Glyph, Archetype)` means the map f 1. Wire `ObjectDef` scripts to Rhai: when the player moves to an object's cell, fire its `on_touch` handler 2. Define the Rhai API surface (what functions scripts can call: `send_message`, movement, board queries) -**Object behavior from scripts** — `Archetype::Object.behavior()` currently returns a static default (`passable: false, opaque: false`). Eventually, `Board::is_passable` will need a special branch for Object cells that reads the behavior from the cell's Rhai script instead. +**Object behavior from scripts** — `ObjectDef.passable` and `ObjectDef.opaque` are live and editable in the editor, but they are static author-set values. Eventually they should be overridable by the object's Rhai script at runtime (e.g. a door script that flips `passable` when opened). `Board::is_passable` already has the right shape for this; the script runtime just needs to be wired in. **Portal navigation** — `PortalDef` stores a `target_map` and `target_entry` but there's no multi-board loading or board switching yet. diff --git a/CLAUDE.md b/CLAUDE.md index fa77689..90661ea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,11 +48,12 @@ The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI. - `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`, `Object`, `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. +- `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. - `ALL_ARCHETYPES: &[Archetype]` — ordered list of valid editor choices (excludes `ErrorBlock`). -- `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`, `portals: Vec`, `font: Option`. `cells` is `pub(crate)`. +- `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`, `portals: Vec`, `font: Option`. `cells` is `pub(crate)`. `is_passable(x, y)` checks objects before the grid cell: an impassable object blocks even over a passable floor tile. - `Player` — `x: i32, y: i32` -- `ObjectDef` / `PortalDef` — parsed from map files, stored on Board; not yet runtime-wired +- `ObjectDef` — scripted object placed on the board: `x`, `y`, `glyph: Glyph`, `passable: bool`, `opaque: bool`, `script_name: Option`. `passable` defaults `false` and `opaque` defaults `true` in map files. Scripts not yet runtime-wired. +- `PortalDef` — parsed from map files, stored on Board; not yet runtime-wired - `GameState` — holds `board: Board`; `try_move(dx, dy)` checks passability before moving the player **`src/font.rs`** — bitmap font loading and UV mapping: @@ -74,6 +75,7 @@ The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI. - `FontHeader` — deserializes the optional `[font]` section (`path`, `tile_w`, `tile_h`) - `impl From for Board` — converts a parsed file into a ready-to-use `Board`; unknown archetype names produce `ErrorBlock` - `pub fn load(path: &str) -> Result>` — reads and converts a `.toml` map file +- `pub fn save(board: &Board, path: &Path) -> Result<(), Box>` — serializes `Board` back to TOML and writes to disk **`src/main.rs`** — app entry point and frame loop: - `AppMode` enum (`Play` | `Edit`) — gates arrow-key input; toggles the side panel and viewport mode @@ -89,13 +91,14 @@ The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI. - `paint_glyph(painter, rect, glyph, font)` — `rect_filled` with `glyph.bg`, then `painter.image` tinted by `glyph.fg` - `draw_glyph(painter, origin, x, y, glyph, font)` — sizes cell from `font.tile_w/tile_h`, delegates to `paint_glyph` - `draw_board(painter, origin, board, font)` — draws all cells then player overlay +- `draw_object_overlays(painter, origin, board, font, selected)` — 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)` — centers board or clamps to player with no empty space - `pos_to_cell(origin, pos, tile_w, tile_h) -> (i32, i32)` — pixel → cell coordinates; negatives signal out-of-bounds **`src/editor.rs`** — editor state and side panel: -- `EditorTab` enum (`Palette` | `Board` | `World`) — which tab is active -- `EditorState` — holds `selected: Archetype`, `glyph: Glyph`, `glyph_picker_open: bool`, `tab: EditorTab`, `font_dialog_open: bool`, `font_dialog_state: FontDialogState`; selecting a new archetype resets `glyph` to that archetype's default -- `show_editor_panel(ctx, editor, font_spec, active_font)` — resizable right-side panel (default 200 px); Palette tab shows archetype list and glyph preview button; Board tab shows current font path and "Font…" button that opens `font_dialog::show`; World tab is a placeholder +- `EditorTab` enum (`Palette` | `Board` | `Objects` | `World`) — which tab is active +- `EditorState` — holds `selected: Archetype`, `glyph: Glyph`, `glyph_picker_open: bool`, `tab: EditorTab`, `font_dialog_open: bool`, `font_dialog_state: FontDialogState`, `selected_object: Option`, `object_glyph_picker_open: bool`, `object_editing_glyph: Glyph`, `placing_object: bool`; selecting a new archetype resets `glyph` to that archetype's default +- `show_editor_panel(ctx, editor, board, active_font)` — resizable right-side panel (default 200 px); Palette tab shows archetype list and glyph preview button; Board tab shows current font path and "Font…" button; Objects tab lists all board objects with click-to-select, glyph preview, **Passable/Opaque checkboxes**, and script combobox; World tab is a placeholder **`src/glyph_picker.rs`** — floating glyph picker dialog: - `show(ctx, open, glyph, board_cells, font)` — takes `open: &mut bool` and `glyph: &mut Glyph` directly; no dependency on `EditorState` diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index 841dcc9..4debe5e 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -70,9 +70,8 @@ pub struct FontSpec { /// passability and opacity. Future properties (pushable, shootable, etc.) can /// be added here without changing call sites. /// -/// For scripted objects (`Archetype::Object`), the behavior will eventually be -/// determined by the Rhai script rather than this struct. See the Object variant -/// for details. (TODO fix this comment) +/// For scripted objects, passability and opacity are stored directly on +/// [`ObjectDef`] and will eventually be overridable by Rhai scripts at runtime. #[derive(Copy, Clone, Debug)] pub struct Behavior { /// Whether an entity can walk through this cell. @@ -226,6 +225,7 @@ pub struct ObjectDef { } impl ObjectDef { + /// Returns the default glyph for a newly placed object: tile 63 (`?`) in yellow on black. pub fn default_glyph() -> Glyph { Glyph { tile: 63, @@ -233,6 +233,21 @@ impl ObjectDef { bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 }, } } + + /// Creates a new object at `(x, y)` with default glyph and blocking behavior. + /// + /// Defaults: `passable = false`, `opaque = true`, no script. These match + /// the serde defaults in the map file format so new objects round-trip correctly. + pub fn new(x: usize, y: usize) -> Self { + Self { + x, + y, + glyph: Self::default_glyph(), + passable: false, + opaque: true, + script_name: None, + } + } } /// A portal that teleports the player to a named entry point on another board. diff --git a/kiln-egui/src/main.rs b/kiln-egui/src/main.rs index f9d2306..cdf372f 100644 --- a/kiln-egui/src/main.rs +++ b/kiln-egui/src/main.rs @@ -10,7 +10,7 @@ mod render; use editor::{EditorState, EditorTab, show_editor_panel}; use font::BitmapFont; -use kiln_core::game::{FontSpec, GameState, Glyph, ObjectDef}; +use kiln_core::game::{FontSpec, GameState, ObjectDef}; use kiln_core::map_file; use render::{ DEFAULT_WINDOW_H, DEFAULT_WINDOW_W, MIN_WINDOW_H, MIN_WINDOW_W, board_origin, draw_board, @@ -237,18 +237,7 @@ impl eframe::App for App { if self.editor.placing_object { // Placement mode: create a new object here if none exists. if board.object_index_at(cx, cy).is_none() { - board.objects.push(ObjectDef { - x: cx, - y: cy, - glyph: Glyph { - tile: ObjectDef::default_glyph().tile, - fg: ObjectDef::default_glyph().fg, - bg: ObjectDef::default_glyph().bg, - }, - passable: false, - opaque: true, - script_name: None, - }); + board.objects.push(ObjectDef::new(cx, cy)); } self.editor.placing_object = false; } else {