Update CLAUDE.md and ARCHITECTURE.md for Behavior/Archetype redesign and editor mode
Documents the replacement of Element with Behavior + Archetype, the elimination of the per-board element palette, the new map file format (archetype = "name" instead of passable = bool), the ErrorBlock sentinel, and the editor mode (AppMode, EditorState, palette panel, click-to-paint). Also updates "What's not yet implemented" to reflect editor progress and call out the Object behavior / glyph chooser / file open gaps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+41
-19
@@ -38,22 +38,32 @@ The types here are the runtime representation of a game world. They are delibera
|
||||
**`Glyph`** (`Copy`)
|
||||
The visual representation of one cell: a character, a foreground color, and a background color. Stored *per cell* (not per element type) so individual cells can animate or vary their appearance independently — e.g. a "fire" element where each flame tile has a slightly different color — without changing their behavior.
|
||||
|
||||
**`Element`**
|
||||
The behavioral definition of a tile type. Currently just `passable: bool`; future fields will include `opaque: bool` (for line-of-sight), `shootable`, etc. Elements are stored once in a palette on `Board`; cells reference them by index. Many cells can share the same Element.
|
||||
**`Behavior`**
|
||||
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.
|
||||
|
||||
**Why Glyph and Element are separate:**
|
||||
In ZZT, each board tile had both a visual (character + color pair) and an element type. The visual could vary per-tile even for the same element. We replicate this: `Glyph` is the visual (per-cell), `Element` is the behavior (shared via palette). This lets you have a wall that's gray in one room and blue in another without creating two "wall" element types.
|
||||
**`Archetype`**
|
||||
An enum of named element types: `Empty`, `Wall`, `Object`, `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
|
||||
|
||||
`ErrorBlock` is a sentinel for map files that reference an unknown archetype name. It renders as a yellow `?` on red so malformed maps are immediately visible in-game. It is excluded from `ALL_ARCHETYPES` (the editor's list) because it is not a valid authoring choice.
|
||||
|
||||
**Why `Behavior` and `Archetype` are separate types:**
|
||||
`Archetype` is the *named class* of a thing — it lets you say "this is a wall" in a map file and in the editor. `Behavior` is the *runtime properties* that drive simulation. Keeping them separate means adding a new property (e.g. `pushable`) only requires a new field on `Behavior`; no match arms need to be updated across the codebase.
|
||||
|
||||
**Why Glyph and Archetype are separate:**
|
||||
In ZZT, each board tile had both a visual (character + color pair) and an element type. The visual could vary per-tile even for the same element type. We replicate this: `Glyph` is the visual (per-cell), `Archetype` is the class (shared across cells of the same type). This lets you have a wall that's gray in one room and blue in another without creating two archetype variants.
|
||||
|
||||
**`Board`**
|
||||
The complete unit of a game world — one "room" or "screen" in ZZT terminology. Holds:
|
||||
- `elements: Vec<Element>` — the behavior palette for this board
|
||||
- `cells: Vec<(Glyph, usize)>` — row-major grid; each cell is a visual + an index into `elements`
|
||||
- `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)
|
||||
- `portals: Vec<PortalDef>` — exits to other boards (parsed, not yet runtime-wired)
|
||||
|
||||
**Why `cells` is `Vec<(Glyph, usize)>` not `Vec<Cell>`:**
|
||||
An anonymous tuple prevents the element index from being detached from its board and misused. The `usize` has no meaning on its own — it only makes sense relative to a specific `Board`'s `elements` palette. Wrapping it in a named `Cell` struct would give it false independence.
|
||||
**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.
|
||||
|
||||
**Why `Board` is the complete unit (no wrapper struct):**
|
||||
An earlier design had `GameMap { board: Board, player: Player, ... }`. This was eliminated because the split was artificial: there's no meaningful use of a `Board` without a player position, and no meaningful use of a player without a `Board`. ZZT itself treats a board as containing everything — the grid, the objects, and the player entry point. Collapsing to a single struct matches the domain model.
|
||||
@@ -67,7 +77,7 @@ Currently a thin wrapper around `Board` that provides game-logic methods (`try_m
|
||||
|
||||
**`MapFile`** and supporting structs are serde deserialization types only — they exist solely to parse TOML and are never used at runtime.
|
||||
|
||||
**`impl From<MapFile> for Board`** — the single conversion point. Reads the palette, builds the `elements` vec, then walks the grid string character-by-character to build `cells`. This is the only place that knows about both the file format and the runtime representation.
|
||||
**`impl From<MapFile> for Board`** — the single conversion point. Reads the palette (each entry maps a character to a `(Glyph, Archetype)` pair), then walks the grid string character-by-character to build `cells`. Unknown archetype names produce an `ErrorBlock` cell and log a warning. This is the only place that knows about both the file format and the runtime representation.
|
||||
|
||||
**`pub fn load(path: &str) -> Result<Board, ...>`** — reads a file, deserializes, converts. Called from `main()` before the window is created.
|
||||
|
||||
@@ -78,13 +88,18 @@ The window minimum size is derived from `board.width` and `board.height`. eframe
|
||||
|
||||
### `main.rs` — app + rendering
|
||||
|
||||
`App` holds a `GameState`. The `update` method:
|
||||
1. Reads arrow key input and calls `GameState::try_move`
|
||||
2. Draws the menu bar
|
||||
3. Draws the board: for each cell, a filled background rect then a centered monospace character
|
||||
4. Draws the player on top using `Glyph::player()` (hardcoded `@` in cyan — the player is not a board cell)
|
||||
`App` holds a `GameState`, an `AppMode` (`Play` | `Edit`), and an `EditorState` (`selected: Archetype`). The `update` method:
|
||||
|
||||
**Cell rendering constants:** `CELL_W = 14.0`, `CELL_H = 20.0` pixels. The board is centered in the CentralPanel when the window is larger than the minimum size.
|
||||
1. Reads arrow key input and calls `GameState::try_move` — **Play mode only**
|
||||
2. Draws the menu bar with a File menu and a Play/Edit mode toggle
|
||||
3. In Edit mode: draws a right-side palette panel listing `ALL_ARCHETYPES` as selectable rows — declared before `CentralPanel` so egui allocates its space first
|
||||
4. Draws the board: for each cell, a filled background rect then a centered monospace character
|
||||
5. Draws the player on top using `Glyph::player()` (hardcoded `@` in cyan — the player is not a board cell)
|
||||
6. In Edit mode: allocates the full viewport as a click target; on click, converts pixel position to cell coordinates and stamps the cell with `(selected.default_glyph(), selected)`
|
||||
|
||||
**Cell rendering constants:** `CELL_W = 14.0`, `CELL_H = 20.0` pixels. The board is centered in the CentralPanel when the window is larger than the minimum size. Window min-size is computed for Play mode; the Edit mode palette panel (`PALETTE_PANEL_W = 120.0` px) may clip the board at minimum size.
|
||||
|
||||
**Click-to-cell math:** `rel = click_pos - origin; cell_x = (rel.x / CELL_W).floor() as i32`. Negative `rel` values stay negative and are caught by a `>= 0` guard. This mirrors the rendering math exactly so clicks land on the correct cell.
|
||||
|
||||
---
|
||||
|
||||
@@ -100,8 +115,8 @@ height = 25
|
||||
player_start = [30, 12]
|
||||
|
||||
[palette]
|
||||
" " = { passable = true, ch = " ", fg = "#000000", bg = "#000000" }
|
||||
"#" = { passable = false, ch = "#", fg = "#808080", bg = "#606060" }
|
||||
" " = { archetype = "empty", ch = " ", fg = "#000000", bg = "#000000" }
|
||||
"#" = { archetype = "wall", ch = "#", fg = "#808080", bg = "#606060" }
|
||||
|
||||
[grid]
|
||||
content = """
|
||||
@@ -127,8 +142,11 @@ target_entry = "west_door"
|
||||
**Why TOML over a custom format:**
|
||||
The `toml` crate gives us deserialization with minimal code. Multi-line strings for the grid give a visual representation of the map. Embedded Rhai scripts fit naturally in TOML multi-line strings without escaping issues.
|
||||
|
||||
**Why archetypes by name, not by property:**
|
||||
Palette entries used to store `passable = true/false` directly. Switching to `archetype = "wall"` means adding a new behavioral property (e.g. `opaque`) only requires a code change — existing map files don't need to be updated. It also makes files more readable: `archetype = "wall"` is self-documenting. Unknown names produce a visible `ErrorBlock` cell rather than silently defaulting.
|
||||
|
||||
**Why the palette approach:**
|
||||
A direct mapping from palette character → `(Glyph, Element)` means the map file is both human-readable (you can see the shape of the room from the grid string) and efficient (shared element definitions, per-cell visual variation possible by using different palette chars with the same `passable` value but different colors).
|
||||
A direct mapping from palette character → `(Glyph, Archetype)` means the map file is both human-readable (you can see the shape of the room from the grid string) and flexible (per-cell visual variation is possible by using different palette chars with the same archetype but different colors).
|
||||
|
||||
**Colors** are `"#RRGGBB"` hex strings — universally understood, hand-editable.
|
||||
|
||||
@@ -142,11 +160,15 @@ A direct mapping from palette character → `(Glyph, Element)` means the map fil
|
||||
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.
|
||||
|
||||
**Portal navigation** — `PortalDef` stores a `target_map` and `target_entry` but there's no multi-board loading or board switching yet.
|
||||
|
||||
**Multi-board world** — right now the engine loads a single `maps/start.toml`. Future: a world file or directory of boards, lazy-loaded as the player moves through portals.
|
||||
|
||||
**Game creation tools** — the long-term goal is an in-app editor. Not started.
|
||||
**Editor glyph chooser** — the editor can paint cells with archetypes (default glyphs), but there's no way yet to choose a custom character or color for a cell. The next editor feature is buttons that open char/color picker dialogs.
|
||||
|
||||
**File open dialog** — the editor has no way to load a different map file from the UI. This requires a native file dialog (e.g. `rfd`). WASM support for file dialogs is an open question.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user