f3bbfbb169
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
131 lines
8.0 KiB
Markdown
131 lines
8.0 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Project
|
||
|
||
`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).
|
||
|
||
## Code style
|
||
|
||
- 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.
|
||
|
||
## Finishing an epic
|
||
|
||
When the user says **"finish the epic"**, do all of the following in order:
|
||
1. Update `CLAUDE.md` to reflect any new modules, types, or behaviors added during the session.
|
||
2. Update `ARCHITECTURE.md` to reflect the same.
|
||
3. Run the `/simplify` skill on changed code.
|
||
4. Commit everything with a summary message.
|
||
|
||
## Commands
|
||
|
||
```bash
|
||
cargo build # compile
|
||
cargo run # build and run (loads maps/start.toml)
|
||
cargo test # run all tests
|
||
cargo test <name> # run a single test by name (substring match)
|
||
cargo clippy # lint
|
||
cargo fmt # format
|
||
```
|
||
|
||
## Architecture
|
||
|
||
The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI. eframe drives a retained-mode UI: the `App::update` method is called every frame and is responsible for both drawing and responding to input.
|
||
|
||
`update` is structured in phases:
|
||
- Input handling — arrow keys move the player (Play mode only)
|
||
- `egui::TopBottomPanel::top` — menu bar (File → Exit, Play/Edit mode toggle)
|
||
- `egui::SidePanel::right` — archetype palette panel (Edit mode only; declared before CentralPanel)
|
||
- `egui::CentralPanel::default` — game viewport; rendered with `ui.painter()`; click-to-paint in Edit mode
|
||
|
||
### Modules
|
||
|
||
**`src/game.rs`** — all core game types:
|
||
- `Glyph` (`Copy`) — per-cell visual: `ch: char`, `fg/bg: Color32`. `Glyph::player()` is the only constructor used at runtime (colors for board tiles come from the map file).
|
||
- `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.
|
||
- `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<ObjectDef>`, `portals: Vec<PortalDef>`. `cells` is `pub(crate)`.
|
||
- `Player` — `x: i32, y: i32`
|
||
- `ObjectDef` / `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/map_file.rs`** — map file loading:
|
||
- `MapFile` and friends — serde `Deserialize` types for TOML map files
|
||
- `impl From<MapFile> for Board` — converts a parsed file into a ready-to-use `Board`
|
||
- `pub fn load(path: &str) -> Result<Board, Box<dyn std::error::Error>>` — reads and converts a `.toml` map file
|
||
|
||
**`src/main.rs`** — app entry point and frame loop:
|
||
- `AppMode` enum (`Play` | `Edit`) — gates arrow-key input; toggles the side panel and viewport mode
|
||
- `App` holds `GameState`, `AppMode`, and `EditorState`; `update` phases: input → menu bar → editor panel → board → glyph picker dialog
|
||
- **Play mode**: arrow keys move player; `render::board_origin` centers or player-tracks the viewport
|
||
- **Edit mode**: `ScrollArea::both()` wraps the board; click-to-paint stamps `(editor.glyph, editor.selected)`; calls `editor::show_editor_panel` and `glyph_picker::show`
|
||
|
||
**`src/render.rs`** — cell rendering constants and drawing primitives:
|
||
- `CELL_W = 14.0`, `CELL_H = 20.0` and window sizing constants (`DEFAULT_WINDOW_W = 840`, `DEFAULT_WINDOW_H = 524`)
|
||
- `draw_glyph(painter, origin, x, y, glyph)` — filled rect (bg) + centered monospace char (fg)
|
||
- `draw_board(painter, origin, board)` — draws all cells then player overlay
|
||
- `board_origin(available, board_w, board_h, player)` — centers board or clamps to player with no empty space
|
||
- `pos_to_cell(origin, pos) -> (i32, i32)` — pixel → cell coordinates via floor division; 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`; selecting a new archetype resets `glyph` to that archetype's default
|
||
- `show_editor_panel(ctx, editor, board)` — resizable right-side panel (default 200 px); Palette tab shows archetype list and glyph preview button; Board/World tabs are placeholders
|
||
|
||
**`src/glyph_picker.rs`** — floating glyph picker dialog:
|
||
- `show(ctx, open, glyph, board_cells)` — takes `open: &mut bool` and `glyph: &mut Glyph` directly; no dependency on `EditorState`
|
||
- Three sections: board palette (unique glyphs, click to select), FG/BG color pickers, 16×6 printable ASCII character grid
|
||
|
||
### Map file format (`maps/*.toml`)
|
||
|
||
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.
|
||
|
||
```toml
|
||
[map]
|
||
name = "Room Name"
|
||
width = 60
|
||
height = 25
|
||
player_start = [30, 12]
|
||
|
||
[palette]
|
||
" " = { archetype = "empty", ch = " ", fg = "#000000", bg = "#000000" }
|
||
"#" = { archetype = "wall", ch = "#", fg = "#808080", bg = "#606060" }
|
||
|
||
[grid]
|
||
content = """
|
||
############################################################
|
||
# #
|
||
############################################################
|
||
"""
|
||
|
||
[[objects]] # optional; parsed but not yet runtime-wired
|
||
x = 10
|
||
y = 5
|
||
script = """
|
||
on_touch(|| { send_message("open"); });
|
||
"""
|
||
|
||
[[portals]] # optional; parsed but not yet runtime-wired
|
||
x = 59
|
||
y = 12
|
||
target_map = "cave"
|
||
target_entry = "west_door"
|
||
```
|
||
|
||
Colors are `"#RRGGBB"` hex strings. `player_start` is a header field — the player is not a board cell. 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.
|
||
|
||
### Key design decisions
|
||
|
||
- **`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.
|
||
- **File loading happens in `main()`** before the window is created, so board dimensions are available for window sizing.
|
||
|
||
eframe runs on its own event loop thread; do not assume single-threaded execution.
|