Object passable/opaque: editor checkboxes + doc/simplify cleanup

- ObjectDef gains passable/opaque fields (defaults: false/true); Board::is_passable
  consults the object before the grid cell, so objects block movement independently
  of the floor tile beneath them
- Editor Objects tab exposes Passable and Opaque checkboxes; changes survive save/load
- Add ObjectDef::new(x, y) to centralise placement defaults; remove triple
  default_glyph() call and unused Glyph import from main.rs
- Fix stale Behavior doc comment; update CLAUDE.md and ARCHITECTURE.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 21:48:40 -05:00
parent 0241cd2a8d
commit 067bb9bee0
4 changed files with 43 additions and 27 deletions
+14 -5
View File
@@ -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<ObjectDef>` — scripted objects (parsed, not yet runtime-wired)
- `objects: Vec<ObjectDef>` — scripted objects (parsed; scripts not yet runtime-wired, but `passable`/`opaque` are live and affect collision)
- `portals: Vec<PortalDef>` — exits to other boards (parsed, not yet runtime-wired)
- `font: Option<FontSpec>` — 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<String>`. `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<Element>` 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<usize>` — 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.