Files
kiln/CLAUDE.md
T
2026-05-18 21:48:20 -05:00

131 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.