builtin macro
This commit is contained in:
@@ -140,7 +140,7 @@ Colors are `"#RRGGBB"` hex strings. The player is placed by a `kind = "player"`
|
|||||||
- **Draw order is layer order** — `Board::glyph_at` walks `Board::layers` top-down and returns the first thing that draws (a solid object always; a visible non-solid object; a portal; a solid terrain cell; a non-transparent terrain cell). There is no separate floor field: a "floor" is an `Empty` cell with a visible glyph on a lower layer, so a non-solid object on a higher layer can render *above* a solid one. A cell vacated by a pushed crate is left transparent (`Glyph::transparent()`, tile 0), revealing the layer beneath with no movement-code changes. See `board.rs` / `layer.rs`.
|
- **Draw order is layer order** — `Board::glyph_at` walks `Board::layers` top-down and returns the first thing that draws (a solid object always; a visible non-solid object; a portal; a solid terrain cell; a non-transparent terrain cell). There is no separate floor field: a "floor" is an `Empty` cell with a visible glyph on a lower layer, so a non-solid object on a higher layer can render *above* a solid one. A cell vacated by a pushed crate is left transparent (`Glyph::transparent()`, tile 0), revealing the layer beneath with no movement-code changes. See `board.rs` / `layer.rs`.
|
||||||
- **One solid per cell (across all layers)** — at most one solid entity (the player, a solid terrain archetype on any layer, *or* a solid object) may occupy a cell. The player is a first-class solid: `Board::solid_at` reports `Solid::Player` for the player's cell (checked first), it blocks movers, and it is **pushable in any direction** — a push chain that reaches the player slides the player along (and is rejected if the player has nowhere to go, e.g. against a wall). The map loader enforces the invariant: a second solid stacked on a cell is dropped (recorded as a load error); the player *wins* its cell — its resolved position (including the `(0, 0)` fallback when no `player` char resolves) silently clears any solid terrain under it (any layer) and drops a conflicting object. `Board::solid_at` relies on this invariant.
|
- **One solid per cell (across all layers)** — at most one solid entity (the player, a solid terrain archetype on any layer, *or* a solid object) may occupy a cell. The player is a first-class solid: `Board::solid_at` reports `Solid::Player` for the player's cell (checked first), it blocks movers, and it is **pushable in any direction** — a push chain that reaches the player slides the player along (and is rejected if the player has nowhere to go, e.g. against a wall). The map loader enforces the invariant: a second solid stacked on a cell is dropped (recorded as a load error); the player *wins* its cell — its resolved position (including the `(0, 0)` fallback when no `player` char resolves) silently clears any solid terrain under it (any layer) and drops a conflicting object. `Board::solid_at` relies on this invariant.
|
||||||
- **Each layer is `Vec<(Glyph, Archetype)>`** — every 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. A transparent cell (glyph tile 0) draws nothing; solidity is read from the archetype regardless of transparency.
|
- **Each layer is `Vec<(Glyph, Archetype)>`** — every 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. A transparent cell (glyph tile 0) draws nothing; solidity is read from the archetype regardless of transparency.
|
||||||
- **Archetypes are referenced by name in map files** — so `ALL_ARCHETYPES` can be reordered or extended without breaking saved games. Adding a variant: the `match`es in `behavior()`/`name()`/`default_glyph()` are exhaustive (the compiler flags them), but **`TryFrom<&str>` and `ALL_ARCHETYPES` are not** — forget the `TryFrom` arm and the archetype silently loads as `ErrorBlock`. Also update the map-format example.
|
- **Archetypes are referenced by name in map files** — so variants can be reordered or extended without breaking saved games. **Terrain archetypes** (`Wall`, `Crate`, etc.): the `match`es in `behavior()`/`name()`/`default_glyph()` are exhaustive (the compiler flags them), but `TryFrom<&str>` is not — forget its arm and the new name silently loads as `ErrorBlock`. **Script-backed archetypes** (`Builtin`): adding one requires only (1) an entry in the `builtins!` macro invocation in `archetype.rs` and (2) the `.rhai` script file — `TryFrom<&str>`, `behavior()`, `name()`, `default_glyph()`, and the expansion pass all derive from the macro output automatically. In both cases, also update the map-format example.
|
||||||
- **`Board` is the complete unit** — grid, player, objects, and portals all live on `Board`, matching how ZZT treats a "board". No separate wrapper struct.
|
- **`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.
|
- **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.
|
||||||
- **World loading happens in `main()`** before the terminal is initialized, so load errors print to a normal screen and board dimensions are known before the game loop starts.
|
- **World loading happens in `main()`** before the terminal is initialized, so load errors print to a normal screen and board dimensions are known before the game loop starts.
|
||||||
|
|||||||
+6
-6
@@ -18,12 +18,12 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
|
|||||||
- `NAMED_COLORS: [(&str, Rgba8); 16]` — the 16 EGA/VGA palette colors as `(name, color)` pairs in palette order (`Black`, `Blue`, …, `White`). The **single source of truth**: `script::register_global_constants` builds the Rhai `Black`/`Blue`/… `"#RRGGBB"` constants from it, and kiln-ui's glyph picker builds its named-swatch strips from it, so the two never drift.
|
- `NAMED_COLORS: [(&str, Rgba8); 16]` — the 16 EGA/VGA palette colors as `(name, color)` pairs in palette order (`Black`, `Blue`, …, `White`). The **single source of truth**: `script::register_global_constants` builds the Rhai `Black`/`Blue`/… `"#RRGGBB"` constants from it, and kiln-ui's glyph picker builds its named-swatch strips from it, so the two never drift.
|
||||||
|
|
||||||
**`kiln-core/src/archetype.rs`** — element taxonomy:
|
**`kiln-core/src/archetype.rs`** — element taxonomy:
|
||||||
- `Archetype` (`Copy`, `PartialEq`, `Hash`) — enum of named element types: `Empty`, `Wall`, `Crate`, `HCrate`, `VCrate`, `Pusher(Direction)`, `Spinner(SpinDirection)`, `Gem`, `ErrorBlock`. Each variant provides `behavior()`, `name()` (used in map files), and `default_glyph()`. `Crate` pushable any direction (CP437 ■, char 254); `HCrate`/`VCrate` pushable east/west (↔, char 29) / north/south (↕, char 18) only. `Pusher(dir)` (`pusher_north|south|east|west`) is a **map-file keyword only**: at load it expands into a scripted object (see [`builtin_scripts`]), never a terrain cell. `Spinner(SpinDirection)` (`SpinDirection::{Clockwise,CounterClockwise}`; keywords `spinner_cw|spinner_ccw`) is the same kind of script-backed keyword: it expands into a `spinner.rhai` object that rotates its 8 neighbours each 0.5 s and animates its glyph (default glyph is the first frame, `/` CW / `\` CCW, gray on black). `Gem` (keyword `gem`) is also a script-backed keyword: solid + non-opaque + pushable + **grab** (the only archetype with `grab: true`), it expands into a `gem.rhai` object (default glyph blue ♦, CP437 tile 4) whose `grab()` hook adds a gem to the player and `die()`s. `ErrorBlock` is a sentinel for unknown archetype names (yellow `?` on red). `TryFrom<&str>` parses by name; unrecognized names return `Err` (the map loader substitutes `ErrorBlock`).
|
- `Archetype` (`Copy`, `PartialEq`, `Hash`) — enum of named element types: `Empty`, `Wall`, `Crate`, `HCrate`, `VCrate`, `Builtin(Builtin, &'static str)`, `ErrorBlock`. Each variant provides `behavior()`, `name()` (used in map files), and `default_glyph()`. `Crate` pushable any direction (CP437 ■, char 254); `HCrate`/`VCrate` pushable east/west (↔, char 29) / north/south (↕, char 18) only. `ErrorBlock` is a sentinel for unknown archetype names (yellow `?` on red). `TryFrom<&str>` checks the `Builtin` registry first (via `Builtin::from_name`), then the hard-coded terrain names; unrecognized names return `Err` (the map loader substitutes `ErrorBlock`).
|
||||||
|
- `Builtin` (`Copy`, `PartialEq`, `Hash`) — the family enum for all script-backed archetypes: `Gem`, `Pusher`, `Spinner`. Generated by the `builtins!` macro (see below) along with all its methods. `Archetype::Builtin(family, alias)` carries both the family (selects the behavior and script source) and the specific alias matched during parsing (e.g. `"pusher_north"` or `"spinner_cw"`); the alias drives the per-alias glyph, the `BUILTIN_<alias>` tag on the expanded object, and the compile-cache key.
|
||||||
|
- `macro_rules! builtins!` — the declarative registry for script-backed archetypes. Each entry: `Variant => ["alias" => tile, …] { behavior, fg, bg, script: include_str!(…) }`. The macro generates `enum Builtin { … }` and `impl Builtin { from_name, behavior, default_glyph_for, script }`. **To add a new builtin**: add one entry here + write `src/scripts/<name>.rhai`. No other code changes are needed — `TryFrom<&str>`, `behavior()`, `name()`, `default_glyph()`, the expansion pass, and the save round-trip all derive from the macro output. Current entries: `Gem` (`"gem"` → tile 4, blue ♦, solid + pushable + grab), `Pusher` (`"pusher_north"` → 30, `"pusher_south"` → 31, `"pusher_east"` → 16, `"pusher_west"` → 17; gray arrow tiles, solid + unpushable), `Spinner` (`"spinner_cw"` → 47 `/`, `"spinner_ccw"` → 92 `\`; gray, solid + unpushable). All aliases in a family share one Rhai script; the script reads `Me.has_tag("BUILTIN_<alias>")` to determine per-instance direction/chirality.
|
||||||
|
|
||||||
**`kiln-core/src/builtin_scripts.rs`** — archetypes that are really scripted objects (`pub(crate)`):
|
**`kiln-core/src/builtin_scripts.rs`** — tag helpers for script-backed archetypes (`pub(crate)`):
|
||||||
- `archetype_script(arch) -> Option<&'static str>` — the dispatch (one arm per script-backed archetype) returning the embedded Rhai source (`include_str!`); currently `Pusher(_) -> scripts/pusher.rhai`, `Spinner(_) -> scripts/spinner.rhai`, and `Gem -> scripts/gem.rhai` (just `fn grab() { add_gems(1); die(); }`). Such an archetype loads as a plain terrain cell (`layer::resolve_entry`); `Board::expand_builtin_archetypes` (called from `TryFrom<MapFile>` at load and again before a playtest) is the **single** place that turns it into an `ObjectDef` carrying that source in `builtin_script` plus a `BUILTIN_<archetype>` tag. The spinner reads its `BUILTIN_spinner_cw`/`_ccw` tag for spin direction (defaulting clockwise) and stores per-instance animation frame state in the board `Registry`.
|
- `BUILTIN_TAG_PREFIX` (`"BUILTIN_"`), `builtin_tag(arch) -> String`, `archetype_from_builtin_tag(tag) -> Option<Archetype>` — the tag convention. `expand_builtin_archetypes` tags each expanded object with `BUILTIN_<alias>` (e.g. `"BUILTIN_pusher_east"`) and also uses this string as the object's `script_name` compile-cache key (so each alias gets its own cached AST, though the Rhai source is the same for all aliases in a family). The script reads its tag to determine per-instance direction. `map_file::save` uses `archetype_from_builtin_tag` to collapse the object back into its archetype keyword so maps round-trip. The script sources and behavior definitions now live in `archetype.rs` via the `builtins!` macro (via `include_str!` of `scripts/*.rhai`); this file has only the three tag helpers.
|
||||||
- `archetype_script_key(arch) -> Option<&'static str>` — the synthetic `BUILTIN_*` compile-key for a script-backed archetype (`"BUILTIN_pusher"`/`"BUILTIN_spinner"`/`"BUILTIN_gem"`). `expand_builtin_archetypes` stores it as the expanded object's `script_name` so all instances share one compiled AST (the *source* comes from `archetype_script`, keyed by this name). Distinct from `builtin_tag`, which is per-direction (`BUILTIN_pusher_east`) and drives save round-tripping.
|
|
||||||
- `BUILTIN_TAG_PREFIX` (`"BUILTIN_"`), `builtin_tag(arch) -> String`, `archetype_from_builtin_tag(tag) -> Option<Archetype>` — the tag convention. The script reads its tag for per-instance params (the pusher reads it for its direction, since `Me` isn't visible inside Rhai helper functions); `map_file::save` uses it to collapse the object back into its archetype keyword so maps round-trip. `scripts/pusher.rhai` self-propels via `move(dir)` (which already shoves chains via `step_object`/`push`), pacing itself with `delay` to the old ~0.5 s cadence.
|
|
||||||
|
|
||||||
**`kiln-core/src/utils.rs`** — shared primitive types (`pub(crate)`):
|
**`kiln-core/src/utils.rs`** — shared primitive types (`pub(crate)`):
|
||||||
- `Pushable` (`No`/`Any`/`Horizontal`/`Vertical`) — which directions a solid may be pushed. `allows(dir) -> bool`.
|
- `Pushable` (`No`/`Any`/`Horizontal`/`Vertical`) — which directions a solid may be pushed. `allows(dir) -> bool`.
|
||||||
@@ -55,7 +55,7 @@ The core types that were formerly monolithic in `game.rs` are now split into foc
|
|||||||
- `grab_object_at(x, y) -> Option<ObjectId>` — a solid `grab` object on the cell (the player-walks-onto-it case). `GameState::try_move` uses it to fire `grab()` instead of bumping when the player steps onto a grab thing. Grab is **only** a player-movement event: a grab thing pushed/swapped into the player is treated as an ordinary solid (no special-case). (`remove_object` only edits the `objects` map; a live `ScriptHost` keeps a stale `ObjectRuntime` whose later host-fn calls resolve to a missing id and no-op — benign.)
|
- `grab_object_at(x, y) -> Option<ObjectId>` — a solid `grab` object on the cell (the player-walks-onto-it case). `GameState::try_move` uses it to fire `grab()` instead of bumping when the player steps onto a grab thing. Grab is **only** a player-movement event: a grab thing pushed/swapped into the player is treated as an ordinary solid (no special-case). (`remove_object` only edits the `objects` map; a live `ScriptHost` keeps a stale `ObjectRuntime` whose later host-fn calls resolve to a missing id and no-op — benign.)
|
||||||
- `apply_swap(pairs: &[(i32,i32,i32,i32)]) -> Vec<LogLine>` — applies a batch of one-way solid moves **simultaneously** (reads every source's solid occupant — player/object/terrain — into a private `SolidSnapshot` before writing any destination, so cycles and two-cell swaps resolve). A source with no solid moves an "empty", removing the destination's solid (terrain cleared; a displaced object despawned via `remove_object`). The **player is never destroyed** — a write that would overwrite it without relocating it is skipped + logged (a grab object is no exception: it is refused like any other solid). Because a refused solid stays at its source cell, which another entry may also target, a final **overlap sweep** over the swapped cells keeps one solid per cell and deletes the rest (never the player), logging an error per deletion. Out-of-bounds entries are skipped + logged. Backs the script `swap()` fn.
|
- `apply_swap(pairs: &[(i32,i32,i32,i32)]) -> Vec<LogLine>` — applies a batch of one-way solid moves **simultaneously** (reads every source's solid occupant — player/object/terrain — into a private `SolidSnapshot` before writing any destination, so cycles and two-cell swaps resolve). A source with no solid moves an "empty", removing the destination's solid (terrain cleared; a displaced object despawned via `remove_object`). The **player is never destroyed** — a write that would overwrite it without relocating it is skipped + logged (a grab object is no exception: it is refused like any other solid). Because a refused solid stays at its source cell, which another entry may also target, a final **overlap sweep** over the swapped cells keeps one solid per cell and deletes the rest (never the player), logging an error per deletion. Out-of-bounds entries are skipped + logged. Backs the script `swap()` fn.
|
||||||
- `place_archetype(x, y, arch, glyph)` — editor stamp primitive (used by kiln-tui's drawing tools). Keyed only on the archetype, leaving any floor untouched: a non-`Empty` (solid) arch removes a solid object in the cell then writes `(glyph, arch)` into the existing terrain layer (`terrain_layer_at`, the single non-`Empty` cell) or else the top layer; `Empty` erases — removes every object plus the terrain cell (→ transparent `Empty`). Note it writes **terrain** even for script-backed archetypes (pushers/spinners), so the editor stamps an inert cell — see `expand_builtin_archetypes`.
|
- `place_archetype(x, y, arch, glyph)` — editor stamp primitive (used by kiln-tui's drawing tools). Keyed only on the archetype, leaving any floor untouched: a non-`Empty` (solid) arch removes a solid object in the cell then writes `(glyph, arch)` into the existing terrain layer (`terrain_layer_at`, the single non-`Empty` cell) or else the top layer; `Empty` erases — removes every object plus the terrain cell (→ transparent `Empty`). Note it writes **terrain** even for script-backed archetypes (pushers/spinners), so the editor stamps an inert cell — see `expand_builtin_archetypes`.
|
||||||
- `expand_builtin_archetypes()` — the **single** expansion point: replaces every script-backed terrain cell (e.g. `Spinner`/`Pusher`/`Gem`, those with an `archetype_script`) with the scripted object it expands to (embedded script + `BUILTIN_*` tag + behavior, copying the cell's glyph so palette overrides survive). The object's `pushable`/`grab` flags are taken from the archetype's `Behavior` (so a gem becomes pushable + grabbable; pushers/spinners stay unpushable). Idempotent (cells already loaded as objects are skipped). Called by `TryFrom<MapFile> for Board` after cross-layer validation (so disk loads get working machines) and again by the editor's `playtest()` on the `World::deep_clone` (so editor-stamped machines, which `place_archetype` writes as inert terrain, also run — the playtest skips the reload path). Because it runs after explicit-object placement, builtin objects get ids after hand-placed ones (still layer-then-reading order among themselves).
|
- `expand_builtin_archetypes()` — the **single** expansion point: replaces every `Archetype::Builtin(b, alias)` terrain cell with the scripted object it expands to. For each such cell: vacates the terrain slot (→ transparent `Empty`), calls `b.script()` for the embedded source, uses `builtin_tag(arch)` as both the `BUILTIN_<alias>` tag and the `script_name` compile-cache key, and copies the cell's glyph (so palette overrides survive). The object's `pushable`/`grab` flags come from `b.behavior()`. Idempotent (vacated cells become `Archetype::Empty`, so a second pass finds nothing). Called by `TryFrom<MapFile> for Board` after cross-layer validation (disk loads) and again by the editor's `playtest()` on the `World::deep_clone` (so editor-stamped machines, which `place_archetype` writes as inert terrain, also run). Builtin objects get ids after hand-placed ones (layer-then-reading order among themselves).
|
||||||
- `in_bounds((i32, i32))` — bounds-checks a possibly-negative coord.
|
- `in_bounds((i32, i32))` — bounds-checks a possibly-negative coord.
|
||||||
- `is_valid()` / `load_errors()` / `report_error(msg)` — nonfatal load-error surface.
|
- `is_valid()` / `load_errors()` / `report_error(msg)` — nonfatal load-error surface.
|
||||||
|
|
||||||
|
|||||||
+164
-132
@@ -1,7 +1,117 @@
|
|||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::utils::{Behavior, Direction, Pushable};
|
use crate::utils::{Behavior, Pushable};
|
||||||
use color::Rgba8;
|
use color::Rgba8;
|
||||||
|
|
||||||
|
/// Declares the set of script-backed archetype families.
|
||||||
|
///
|
||||||
|
/// Each entry specifies:
|
||||||
|
/// - A `Variant` name (becomes a [`Builtin`] enum variant).
|
||||||
|
/// - A `["name" => tile, …]` list: one map-file keyword per alias with the tile
|
||||||
|
/// index for the editor glyph. All aliases in a family share `behavior`, `fg`,
|
||||||
|
/// `bg`, and embedded Rhai `script`.
|
||||||
|
/// - `behavior`, `fg`, `bg`: per-family defaults.
|
||||||
|
/// - `script`: the embedded Rhai source; `include_str!` paths are relative to this
|
||||||
|
/// file, so `include_str!("scripts/pusher.rhai")` resolves to
|
||||||
|
/// `kiln-core/src/scripts/pusher.rhai`.
|
||||||
|
///
|
||||||
|
/// **To add a new builtin archetype:** add one entry here + write the `.rhai` file.
|
||||||
|
/// No other code needs to change — `TryFrom<&str>`, `behavior()`, `name()`,
|
||||||
|
/// `default_glyph()`, the expansion pass, and the save round-trip all derive from
|
||||||
|
/// the macro output automatically.
|
||||||
|
macro_rules! builtins {
|
||||||
|
(
|
||||||
|
$(
|
||||||
|
$variant:ident => [ $( $name:literal => $tile:literal ),+ $(,)? ] {
|
||||||
|
behavior: $behavior:expr,
|
||||||
|
fg: $fg:expr,
|
||||||
|
bg: $bg:expr,
|
||||||
|
script: $script:expr $(,)?
|
||||||
|
}
|
||||||
|
),+ $(,)?
|
||||||
|
) => {
|
||||||
|
/// A family of script-backed archetypes, generated by the [`builtins!`] macro.
|
||||||
|
///
|
||||||
|
/// Each variant groups one or more map-file keywords (aliases) that share one
|
||||||
|
/// embedded Rhai script and a uniform [`Behavior`]. The specific alias used in
|
||||||
|
/// the map file is preserved as the `&'static str` in [`Archetype::Builtin`]
|
||||||
|
/// so scripts can read it via the `BUILTIN_<alias>` tag (e.g. a pusher reads
|
||||||
|
/// `Me.has_tag("BUILTIN_pusher_north")` to know its direction).
|
||||||
|
///
|
||||||
|
/// ## Adding a new builtin
|
||||||
|
///
|
||||||
|
/// Add one entry to the `builtins!` invocation in `archetype.rs` and write
|
||||||
|
/// `kiln-core/src/scripts/<name>.rhai`. No other files need to change.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum Builtin {
|
||||||
|
$( $variant ),+
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Builtin {
|
||||||
|
/// Returns `(family_variant, matched_alias)` for `name`, or `None` if `name`
|
||||||
|
/// is not a known builtin keyword. The returned `&'static str` is the exact
|
||||||
|
/// literal from the macro (always valid for a `Archetype::Builtin` field).
|
||||||
|
pub(crate) fn from_name(name: &str) -> Option<(Self, &'static str)> {
|
||||||
|
match name {
|
||||||
|
$(
|
||||||
|
$( $name => Some((Builtin::$variant, $name)), )+
|
||||||
|
)+
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the uniform behavior shared by all aliases in this family.
|
||||||
|
pub(crate) fn behavior(self) -> Behavior {
|
||||||
|
match self {
|
||||||
|
$( Builtin::$variant => $behavior, )+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the glyph for `alias`: the family's fg/bg with the alias's tile.
|
||||||
|
/// Falls back to a transparent glyph for unrecognized aliases (shouldn't
|
||||||
|
/// happen in practice since aliases are all from the macro).
|
||||||
|
pub(crate) fn default_glyph_for(self, alias: &str) -> Glyph {
|
||||||
|
// The outer repetition brings $fg/$bg into scope; the inner one selects
|
||||||
|
// the tile for the specific alias. Both are statically resolved at
|
||||||
|
// compile time.
|
||||||
|
match alias {
|
||||||
|
$(
|
||||||
|
$( $name => Glyph { tile: $tile, fg: $fg, bg: $bg }, )+
|
||||||
|
)+
|
||||||
|
_ => Glyph::transparent(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the embedded Rhai source shared by all aliases in this family.
|
||||||
|
pub(crate) fn script(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
$( Builtin::$variant => $script, )+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
builtins! {
|
||||||
|
Gem => ["gem" => 4] {
|
||||||
|
behavior: Behavior { solid: true, opaque: false, pushable: Pushable::Any, grab: true },
|
||||||
|
fg: Rgba8 { r: 0x50, g: 0x50, b: 0xFF, a: 255 },
|
||||||
|
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||||
|
script: include_str!("scripts/gem.rhai"),
|
||||||
|
},
|
||||||
|
Pusher => ["pusher_north" => 30, "pusher_south" => 31, "pusher_east" => 16, "pusher_west" => 17] {
|
||||||
|
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false },
|
||||||
|
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
||||||
|
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||||
|
script: include_str!("scripts/pusher.rhai"),
|
||||||
|
},
|
||||||
|
Spinner => ["spinner_cw" => 47, "spinner_ccw" => 92] {
|
||||||
|
behavior: Behavior { solid: true, opaque: true, pushable: Pushable::No, grab: false },
|
||||||
|
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
||||||
|
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||||
|
script: include_str!("scripts/spinner.rhai"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/// A class of board cell, encoding its default behavior and appearance.
|
/// A class of board cell, encoding its default behavior and appearance.
|
||||||
///
|
///
|
||||||
/// `Archetype` is an enum of the element types the engine knows about. Each
|
/// `Archetype` is an enum of the element types the engine knows about. Each
|
||||||
@@ -12,13 +122,18 @@ use color::Rgba8;
|
|||||||
/// Map files reference archetypes by [`name`](Archetype::name) (e.g. `"wall"`),
|
/// Map files reference archetypes by [`name`](Archetype::name) (e.g. `"wall"`),
|
||||||
/// so the list of variants can be reordered without breaking saved games.
|
/// so the list of variants can be reordered without breaking saved games.
|
||||||
///
|
///
|
||||||
/// ## Object special case
|
/// ## Script-backed archetypes
|
||||||
///
|
///
|
||||||
/// `Archetype::Object` currently returns a static default `Behavior`.
|
/// The [`Builtin`] variant covers all script-backed types (pushers, spinners,
|
||||||
/// TODO: In the future, Object passability and opacity will come from the cell's Rhai script
|
/// gems). These are map-file keywords only: at load they expand into scripted
|
||||||
/// rather than from this enum. [`Board::is_passable`] will need a special branch
|
/// [`ObjectDef`]s carrying the embedded Rhai source and a `BUILTIN_<alias>` tag
|
||||||
/// at that point. `ErrorBlock` is used as a sentinel for unrecognized archetype
|
/// (see [`crate::builtin_scripts`] and [`Board::expand_builtin_archetypes`]).
|
||||||
/// names in map files — it should never appear in a valid board.
|
///
|
||||||
|
/// `ErrorBlock` is used as a sentinel for unrecognized archetype names in map
|
||||||
|
/// files — it should never appear in a valid board.
|
||||||
|
///
|
||||||
|
/// [`ObjectDef`]: crate::object_def::ObjectDef
|
||||||
|
/// [`Board::expand_builtin_archetypes`]: crate::board::Board::expand_builtin_archetypes
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum Archetype {
|
pub enum Archetype {
|
||||||
/// An open cell; the player and other entities can pass through it.
|
/// An open cell; the player and other entities can pass through it.
|
||||||
@@ -32,46 +147,26 @@ pub enum Archetype {
|
|||||||
HCrate,
|
HCrate,
|
||||||
/// A crate that can only be pushed north/south (vertical axis). Glyph ↕.
|
/// A crate that can only be pushed north/south (vertical axis). Glyph ↕.
|
||||||
VCrate,
|
VCrate,
|
||||||
/// A directional pusher — solid, opaque, non-pushable. This is a map-file
|
/// A script-backed archetype expanded from a map-file keyword.
|
||||||
/// *keyword* only: at load it expands into a scripted [`ObjectDef`] carrying the
|
///
|
||||||
/// embedded `pusher.rhai` and a `BUILTIN_pusher_<dir>` tag (see
|
/// - `Builtin` is the family (e.g. `Builtin::Pusher`), which selects the
|
||||||
/// [`crate::builtin_scripts`]). The script self-propels it one cell at a time in
|
/// shared script and behavior.
|
||||||
/// the facing direction, shoving any pushable chain ahead — no engine special-case.
|
/// - `&'static str` is the specific alias matched during parsing (e.g.
|
||||||
Pusher(Direction),
|
/// `"pusher_north"`), used as the per-alias glyph key and the
|
||||||
/// A rotating machine — solid, opaque, non-pushable. Like [`Pusher`](Archetype::Pusher)
|
/// `BUILTIN_<alias>` tag on the expanded object.
|
||||||
/// this is a map-file *keyword* only: at load it expands into a scripted
|
///
|
||||||
/// [`ObjectDef`] carrying the embedded `spinner.rhai` and a `BUILTIN_spinner_<dir>`
|
/// See [`Builtin`] and the [`builtins!`] invocation for the full registry.
|
||||||
/// tag (see [`crate::builtin_scripts`]). The script reads the tag for its spin
|
Builtin(Builtin, &'static str),
|
||||||
/// direction, rotates the 8 neighbouring cells one step each 0.5 s, and animates
|
|
||||||
/// its own glyph through a spinning line `/ ─ \ │` (slash-swapped for counter-clockwise).
|
|
||||||
Spinner(SpinDirection),
|
|
||||||
/// A collectible gem — solid, non-opaque, pushable, and **grab**bable. Like
|
|
||||||
/// [`Pusher`](Archetype::Pusher)/[`Spinner`](Archetype::Spinner) this is a
|
|
||||||
/// map-file *keyword* only: at load it expands into a scripted [`ObjectDef`]
|
|
||||||
/// carrying the embedded `gem.rhai` (see [`crate::builtin_scripts`]). Walking
|
|
||||||
/// onto a gem (or pushing one into the player) fires its `grab()` hook, which
|
|
||||||
/// increments the player's gem count and removes the gem. Glyph: blue ♦.
|
|
||||||
Gem,
|
|
||||||
/// Sentinel for map files that reference an unknown archetype name.
|
/// Sentinel for map files that reference an unknown archetype name.
|
||||||
/// Renders as a yellow `?` on red to make the error visible in-game.
|
/// Renders as a yellow `?` on red to make the error visible in-game.
|
||||||
ErrorBlock,
|
ErrorBlock,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which way a [`Spinner`](Archetype::Spinner) rotates the ring of cells around it.
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub enum SpinDirection {
|
|
||||||
/// Rotate neighbours clockwise (N→NE→E→…).
|
|
||||||
Clockwise,
|
|
||||||
/// Rotate neighbours counter-clockwise (N→NW→W→…).
|
|
||||||
CounterClockwise,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Archetype {
|
impl Archetype {
|
||||||
/// Returns the default [`Behavior`] for this archetype.
|
/// Returns the default [`Behavior`] for this archetype.
|
||||||
///
|
|
||||||
/// For `Object`, this is a placeholder until Rhai scripts drive the behavior.
|
|
||||||
pub fn behavior(&self) -> Behavior {
|
pub fn behavior(&self) -> Behavior {
|
||||||
match self {
|
match self {
|
||||||
|
Archetype::Builtin(b, _) => b.behavior(),
|
||||||
Archetype::Empty => Behavior {
|
Archetype::Empty => Behavior {
|
||||||
solid: false,
|
solid: false,
|
||||||
opaque: false,
|
opaque: false,
|
||||||
@@ -102,25 +197,6 @@ impl Archetype {
|
|||||||
pushable: Pushable::Vertical,
|
pushable: Pushable::Vertical,
|
||||||
grab: false,
|
grab: false,
|
||||||
},
|
},
|
||||||
Archetype::Pusher(_) => Behavior {
|
|
||||||
solid: true,
|
|
||||||
opaque: true,
|
|
||||||
pushable: Pushable::No,
|
|
||||||
grab: false,
|
|
||||||
},
|
|
||||||
Archetype::Spinner(_) => Behavior {
|
|
||||||
solid: true,
|
|
||||||
opaque: true,
|
|
||||||
pushable: Pushable::No,
|
|
||||||
grab: false,
|
|
||||||
},
|
|
||||||
// A gem doesn't block sight, is shovable, and is grabbed on contact.
|
|
||||||
Archetype::Gem => Behavior {
|
|
||||||
solid: true,
|
|
||||||
opaque: false,
|
|
||||||
pushable: Pushable::Any,
|
|
||||||
grab: true,
|
|
||||||
},
|
|
||||||
Archetype::ErrorBlock => Behavior {
|
Archetype::ErrorBlock => Behavior {
|
||||||
solid: true,
|
solid: true,
|
||||||
opaque: true,
|
opaque: true,
|
||||||
@@ -133,18 +209,12 @@ impl Archetype {
|
|||||||
/// Returns the canonical name used to reference this archetype in map files.
|
/// Returns the canonical name used to reference this archetype in map files.
|
||||||
pub fn name(&self) -> &'static str {
|
pub fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
Archetype::Builtin(_, alias) => alias,
|
||||||
Archetype::Empty => "empty",
|
Archetype::Empty => "empty",
|
||||||
Archetype::Wall => "wall",
|
Archetype::Wall => "wall",
|
||||||
Archetype::Crate => "crate",
|
Archetype::Crate => "crate",
|
||||||
Archetype::HCrate => "hcrate",
|
Archetype::HCrate => "hcrate",
|
||||||
Archetype::VCrate => "vcrate",
|
Archetype::VCrate => "vcrate",
|
||||||
Archetype::Pusher(Direction::North) => "pusher_north",
|
|
||||||
Archetype::Pusher(Direction::South) => "pusher_south",
|
|
||||||
Archetype::Pusher(Direction::East) => "pusher_east",
|
|
||||||
Archetype::Pusher(Direction::West) => "pusher_west",
|
|
||||||
Archetype::Spinner(SpinDirection::Clockwise) => "spinner_cw",
|
|
||||||
Archetype::Spinner(SpinDirection::CounterClockwise) => "spinner_ccw",
|
|
||||||
Archetype::Gem => "gem",
|
|
||||||
Archetype::ErrorBlock => "error_block",
|
Archetype::ErrorBlock => "error_block",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,6 +226,7 @@ impl Archetype {
|
|||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub fn default_glyph(&self) -> Glyph {
|
pub fn default_glyph(&self) -> Glyph {
|
||||||
match self {
|
match self {
|
||||||
|
Archetype::Builtin(b, alias) => b.default_glyph_for(alias),
|
||||||
Archetype::Empty => Glyph {
|
Archetype::Empty => Glyph {
|
||||||
tile: 0,
|
tile: 0,
|
||||||
fg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
fg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||||
@@ -168,48 +239,18 @@ impl Archetype {
|
|||||||
},
|
},
|
||||||
Archetype::Crate => Glyph {
|
Archetype::Crate => Glyph {
|
||||||
tile: 254, // CP437 ■ (small filled square)
|
tile: 254, // CP437 ■ (small filled square)
|
||||||
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 }, // light gray on black
|
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||||
},
|
},
|
||||||
Archetype::HCrate => Glyph {
|
Archetype::HCrate => Glyph {
|
||||||
tile: 29, // CP437 ↔ (left-right arrow) — pushable east/west
|
tile: 29, // CP437 ↔ (left-right arrow) — pushable east/west
|
||||||
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 }, // same colors as Crate
|
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||||
},
|
},
|
||||||
Archetype::VCrate => Glyph {
|
Archetype::VCrate => Glyph {
|
||||||
tile: 18, // CP437 ↕ (up-down arrow) — pushable north/south
|
tile: 18, // CP437 ↕ (up-down arrow) — pushable north/south
|
||||||
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 }, // same colors as Crate
|
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
|
||||||
},
|
|
||||||
Archetype::Pusher(dir) => {
|
|
||||||
let tile = match dir {
|
|
||||||
Direction::North => 30, // CP437 ▲
|
|
||||||
Direction::South => 31, // CP437 ▼
|
|
||||||
Direction::East => 16, // CP437 ►
|
|
||||||
Direction::West => 17, // CP437 ◄
|
|
||||||
};
|
|
||||||
Glyph {
|
|
||||||
tile,
|
|
||||||
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
||||||
}
|
|
||||||
}
|
|
||||||
Archetype::Spinner(dir) => {
|
|
||||||
// First frame of the spin animation: '/' clockwise, '\' counter.
|
|
||||||
let tile = match dir {
|
|
||||||
SpinDirection::Clockwise => 47, // '/'
|
|
||||||
SpinDirection::CounterClockwise => 92, // '\'
|
|
||||||
};
|
|
||||||
Glyph {
|
|
||||||
tile,
|
|
||||||
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
|
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Archetype::Gem => Glyph {
|
|
||||||
tile: 4, // CP437 ♦ (diamond)
|
|
||||||
fg: Rgba8 { r: 0x50, g: 0x50, b: 0xFF, a: 255 }, // blue on black
|
|
||||||
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
|
|
||||||
},
|
},
|
||||||
// Visually distinct so malformed map files are immediately obvious.
|
// Visually distinct so malformed map files are immediately obvious.
|
||||||
Archetype::ErrorBlock => Glyph {
|
Archetype::ErrorBlock => Glyph {
|
||||||
@@ -226,24 +267,23 @@ impl TryFrom<&str> for Archetype {
|
|||||||
|
|
||||||
/// Parses an archetype by its map-file name.
|
/// Parses an archetype by its map-file name.
|
||||||
///
|
///
|
||||||
/// Returns an error for unrecognized names; the caller should substitute
|
/// Checks the [`Builtin`] registry first (pushers, spinners, gems), then
|
||||||
/// [`Archetype::ErrorBlock`] and log the error so the problem is visible.
|
/// falls through to the hard-coded terrain archetypes. Returns an error for
|
||||||
|
/// unrecognized names; the caller should substitute [`Archetype::ErrorBlock`]
|
||||||
|
/// and log the error so the problem is visible.
|
||||||
fn try_from(name: &str) -> Result<Self, Self::Error> {
|
fn try_from(name: &str) -> Result<Self, Self::Error> {
|
||||||
|
// Script-backed families (pushers, spinners, gems) are in the registry.
|
||||||
|
if let Some((b, alias)) = Builtin::from_name(name) {
|
||||||
|
return Ok(Archetype::Builtin(b, alias));
|
||||||
|
}
|
||||||
match name {
|
match name {
|
||||||
"empty" => Ok(Archetype::Empty),
|
"empty" => Ok(Archetype::Empty),
|
||||||
"wall" => Ok(Archetype::Wall),
|
"wall" => Ok(Archetype::Wall),
|
||||||
"crate" => Ok(Archetype::Crate),
|
"crate" => Ok(Archetype::Crate),
|
||||||
"hcrate" => Ok(Archetype::HCrate),
|
"hcrate" => Ok(Archetype::HCrate),
|
||||||
"vcrate" => Ok(Archetype::VCrate),
|
"vcrate" => Ok(Archetype::VCrate),
|
||||||
// "object" is intentionally absent: objects are not valid palette
|
// "object", "portal", "player" are intentionally absent: they are
|
||||||
// entries in map files. They live in [[objects]] with their own glyph.
|
// meta-kinds handled by the layer builder, not Archetype variants.
|
||||||
"pusher_north" => Ok(Archetype::Pusher(Direction::North)),
|
|
||||||
"pusher_south" => Ok(Archetype::Pusher(Direction::South)),
|
|
||||||
"pusher_east" => Ok(Archetype::Pusher(Direction::East)),
|
|
||||||
"pusher_west" => Ok(Archetype::Pusher(Direction::West)),
|
|
||||||
"spinner_cw" => Ok(Archetype::Spinner(SpinDirection::Clockwise)),
|
|
||||||
"spinner_ccw" => Ok(Archetype::Spinner(SpinDirection::CounterClockwise)),
|
|
||||||
"gem" => Ok(Archetype::Gem),
|
|
||||||
_ => Err(format!("unknown archetype: {name}")),
|
_ => Err(format!("unknown archetype: {name}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +291,7 @@ impl TryFrom<&str> for Archetype {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{Archetype, SpinDirection};
|
use super::Archetype;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn directional_crate_default_glyph_tiles() {
|
fn directional_crate_default_glyph_tiles() {
|
||||||
@@ -260,33 +300,25 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spinner_names_round_trip() {
|
fn builtin_names_glyphs_and_round_trip() {
|
||||||
for arch in [
|
// All known aliases must parse, round-trip via name(), and give the right tile.
|
||||||
Archetype::Spinner(SpinDirection::Clockwise),
|
for (name, tile) in [
|
||||||
Archetype::Spinner(SpinDirection::CounterClockwise),
|
("gem", 4u32),
|
||||||
|
("pusher_north", 30),
|
||||||
|
("pusher_south", 31),
|
||||||
|
("pusher_east", 16),
|
||||||
|
("pusher_west", 17),
|
||||||
|
("spinner_cw", 47),
|
||||||
|
("spinner_ccw", 92),
|
||||||
] {
|
] {
|
||||||
assert_eq!(Archetype::try_from(arch.name()), Ok(arch));
|
let arch = Archetype::try_from(name)
|
||||||
}
|
.unwrap_or_else(|_| panic!("'{name}' should parse as a builtin"));
|
||||||
|
assert_eq!(arch.name(), name, "'{name}' round-trips via name()");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Archetype::try_from("spinner_cw"),
|
arch.default_glyph().tile,
|
||||||
Ok(Archetype::Spinner(SpinDirection::Clockwise))
|
tile,
|
||||||
|
"'{name}' has the correct default tile"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn spinner_default_glyph_first_frame() {
|
|
||||||
// First animation frame: '/' (47) clockwise, '\' (92) counter-clockwise.
|
|
||||||
assert_eq!(
|
|
||||||
Archetype::Spinner(SpinDirection::Clockwise)
|
|
||||||
.default_glyph()
|
|
||||||
.tile,
|
|
||||||
47
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Archetype::Spinner(SpinDirection::CounterClockwise)
|
|
||||||
.default_glyph()
|
|
||||||
.tile,
|
|
||||||
92
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-18
@@ -6,7 +6,6 @@ use crate::object_def::ObjectDef;
|
|||||||
use crate::utils::Direction;
|
use crate::utils::Direction;
|
||||||
use crate::utils::{Behavior, ObjectId, Player, PortalDef, Pushable, RegistryValue, Solid};
|
use crate::utils::{Behavior, ObjectId, Player, PortalDef, Pushable, RegistryValue, Solid};
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use crate::builtin_scripts::archetype_script_key;
|
|
||||||
|
|
||||||
/// The complete state of one game board (a single room or screen).
|
/// The complete state of one game board (a single room or screen).
|
||||||
///
|
///
|
||||||
@@ -499,14 +498,14 @@ impl Board {
|
|||||||
/// the normal load path, so this is only needed for the in-memory path. Cells
|
/// the normal load path, so this is only needed for the in-memory path. Cells
|
||||||
/// already loaded as objects are untouched, so it is safe to call more than once.
|
/// already loaded as objects are untouched, so it is safe to call more than once.
|
||||||
pub fn expand_builtin_archetypes(&mut self) {
|
pub fn expand_builtin_archetypes(&mut self) {
|
||||||
use crate::builtin_scripts::{archetype_script, builtin_tag};
|
use crate::builtin_scripts::builtin_tag;
|
||||||
// Collect first: the loop below mutates both layers and the object map.
|
// Collect first: the loop below mutates both layers and the object map.
|
||||||
let mut found: Vec<(usize, usize, usize, Glyph, Archetype)> = Vec::new();
|
let mut found: Vec<(usize, usize, usize, Glyph, Archetype)> = Vec::new();
|
||||||
for z in 0..self.layers.len() {
|
for z in 0..self.layers.len() {
|
||||||
for y in 0..self.height {
|
for y in 0..self.height {
|
||||||
for x in 0..self.width {
|
for x in 0..self.width {
|
||||||
let (glyph, arch) = *self.get(z, x, y);
|
let (glyph, arch) = *self.get(z, x, y);
|
||||||
if archetype_script(arch).is_some() {
|
if matches!(arch, Archetype::Builtin(_, _)) {
|
||||||
found.push((z, x, y, glyph, arch));
|
found.push((z, x, y, glyph, arch));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -516,20 +515,25 @@ impl Board {
|
|||||||
// Vacate the terrain cell (revealing any floor beneath), then spawn the
|
// Vacate the terrain cell (revealing any floor beneath), then spawn the
|
||||||
// object — mirroring `resolve_entry`'s object template.
|
// object — mirroring `resolve_entry`'s object template.
|
||||||
*self.get_mut(z, x, y) = (Glyph::transparent(), Archetype::Empty);
|
*self.get_mut(z, x, y) = (Glyph::transparent(), Archetype::Empty);
|
||||||
let b = arch.behavior();
|
let Archetype::Builtin(b, _alias) = arch else { continue };
|
||||||
|
let beh = b.behavior();
|
||||||
let mut obj = ObjectDef::new(x, y);
|
let mut obj = ObjectDef::new(x, y);
|
||||||
obj.z = z;
|
obj.z = z;
|
||||||
obj.glyph = glyph;
|
obj.glyph = glyph;
|
||||||
obj.solid = b.solid;
|
obj.solid = beh.solid;
|
||||||
obj.opaque = b.opaque;
|
obj.opaque = beh.opaque;
|
||||||
// Carry the archetype's pushability/grab onto the object (pushers and
|
// Carry the archetype's pushability/grab onto the object (pushers and
|
||||||
// spinners are Pushable::No, so they stay unpushable; gems are pushable
|
// spinners are Pushable::No, so they stay unpushable; gems are pushable
|
||||||
// and grabbable).
|
// and grabbable).
|
||||||
obj.pushable = b.pushable != crate::utils::Pushable::No;
|
obj.pushable = beh.pushable != crate::utils::Pushable::No;
|
||||||
obj.grab = b.grab;
|
obj.grab = beh.grab;
|
||||||
obj.builtin_script = archetype_script(arch);
|
obj.builtin_script = Some(b.script());
|
||||||
obj.script_name = archetype_script_key(arch).map(|s| s.to_owned());
|
// The compile-cache key and the BUILTIN_* tag are both derived from the
|
||||||
obj.tags.insert(builtin_tag(arch));
|
// alias (e.g. "BUILTIN_pusher_east"), so each alias gets its own cached
|
||||||
|
// AST — a tiny bit less sharing than before, but the source is identical.
|
||||||
|
let tag = builtin_tag(arch);
|
||||||
|
obj.script_name = Some(tag.clone());
|
||||||
|
obj.tags.insert(tag);
|
||||||
self.add_object(obj);
|
self.add_object(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -614,7 +618,7 @@ impl Board {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::Board;
|
use super::Board;
|
||||||
use crate::archetype::{Archetype, SpinDirection};
|
use crate::archetype::{Archetype, Builtin};
|
||||||
use crate::glyph::Glyph;
|
use crate::glyph::Glyph;
|
||||||
use crate::layer::Layer;
|
use crate::layer::Layer;
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
@@ -963,12 +967,7 @@ pub(crate) mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn expand_builtin_archetypes_replaces_a_spinner_cell_with_an_object() {
|
fn expand_builtin_archetypes_replaces_a_spinner_cell_with_an_object() {
|
||||||
let mut board = open_board(3, 1, (2, 0), vec![]);
|
let mut board = open_board(3, 1, (2, 0), vec![]);
|
||||||
stamp(
|
stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::Spinner, "spinner_cw"));
|
||||||
&mut board,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
Archetype::Spinner(SpinDirection::Clockwise),
|
|
||||||
);
|
|
||||||
board.expand_builtin_archetypes();
|
board.expand_builtin_archetypes();
|
||||||
|
|
||||||
// The terrain cell is vacated and a scripted object takes its place.
|
// The terrain cell is vacated and a scripted object takes its place.
|
||||||
|
|||||||
@@ -1,70 +1,39 @@
|
|||||||
//! Built-in scripts: archetypes that are really scripted objects.
|
//! Tag helpers for script-backed archetypes expanded from map-file keywords.
|
||||||
//!
|
//!
|
||||||
//! Some archetypes (currently the `pusher_*` family) have behavior that an
|
//! When a [`Builtin`] archetype cell is expanded into an [`ObjectDef`] by
|
||||||
//! ordinary scripted object can express exactly. Rather than special-case them in
|
//! [`Board::expand_builtin_archetypes`], the object receives a `BUILTIN_<alias>`
|
||||||
//! the engine, the map loader *expands* such an archetype into an [`ObjectDef`]
|
//! tag (e.g. `"BUILTIN_pusher_north"`) so its Rhai script can read which specific
|
||||||
//! carrying an embedded Rhai script (see [`ObjectDef::builtin_script`]) plus a
|
//! variant it is (via `Me.has_tag("BUILTIN_pusher_north")`).
|
||||||
//! `BUILTIN_<archetype>` tag. The script reads that tag for any per-instance
|
|
||||||
//! parameter (the pusher reads it for its direction), and [`crate::map_file`]'s
|
|
||||||
//! save path uses the same tag to collapse the object back into its archetype
|
|
||||||
//! keyword so maps round-trip.
|
|
||||||
//!
|
//!
|
||||||
//! This module is the single place to register a new script-backed archetype:
|
//! The save path ([`map_file`]) uses [`archetype_from_builtin_tag`] to collapse
|
||||||
//! add an arm to [`archetype_script`].
|
//! an expanded object back into its original map-file keyword so worlds
|
||||||
|
//! round-trip correctly.
|
||||||
|
//!
|
||||||
|
//! The full builtin registry — which archetypes exist, their behaviors, glyphs,
|
||||||
|
//! and embedded scripts — lives in [`crate::archetype`] via the `builtins!` macro.
|
||||||
|
//!
|
||||||
|
//! [`Builtin`]: crate::archetype::Builtin
|
||||||
|
//! [`ObjectDef`]: crate::object_def::ObjectDef
|
||||||
|
//! [`Board::expand_builtin_archetypes`]: crate::board::Board::expand_builtin_archetypes
|
||||||
|
//! [`map_file`]: crate::map_file
|
||||||
|
|
||||||
use crate::archetype::Archetype;
|
use crate::archetype::Archetype;
|
||||||
|
|
||||||
/// Prefix for the tag that marks an object as an expanded built-in archetype and
|
/// Prefix for the tag that marks an object as an expanded built-in archetype and
|
||||||
/// names which archetype it came from (e.g. `"BUILTIN_pusher_east"`).
|
/// names which alias it came from (e.g. `"BUILTIN_pusher_east"`).
|
||||||
pub(crate) const BUILTIN_TAG_PREFIX: &str = "BUILTIN_";
|
pub(crate) const BUILTIN_TAG_PREFIX: &str = "BUILTIN_";
|
||||||
|
|
||||||
/// The pusher behavior, embedded into the binary. Shared by every direction —
|
/// Returns the `BUILTIN_<alias>` tag for `arch` — e.g. `"BUILTIN_pusher_east"`.
|
||||||
/// direction comes from the object's tag, so all pushers share one compiled AST.
|
///
|
||||||
const PUSHER: &str = include_str!("scripts/pusher.rhai");
|
/// For a `Builtin` archetype, `arch.name()` returns the alias (e.g. `"pusher_east"`).
|
||||||
|
/// For terrain archetypes (wall, crate, etc.) this is never called in practice.
|
||||||
/// The spinner behavior, embedded into the binary. Shared by both spin directions —
|
|
||||||
/// direction comes from the object's tag, so all spinners share one compiled AST.
|
|
||||||
const SPINNER: &str = include_str!("scripts/spinner.rhai");
|
|
||||||
|
|
||||||
/// The gem behavior, embedded into the binary: a `grab()` hook that adds a gem to
|
|
||||||
/// the player and removes the object.
|
|
||||||
const GEM: &str = include_str!("scripts/gem.rhai");
|
|
||||||
|
|
||||||
/// The tag identifying an object as the expanded form of `arch`, e.g.
|
|
||||||
/// `"BUILTIN_pusher_east"`.
|
|
||||||
pub(crate) fn builtin_tag(arch: Archetype) -> String {
|
pub(crate) fn builtin_tag(arch: Archetype) -> String {
|
||||||
format!("{BUILTIN_TAG_PREFIX}{}", arch.name())
|
format!("{BUILTIN_TAG_PREFIX}{}", arch.name())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recovers the archetype a `BUILTIN_*` tag came from, or `None` if `tag` is not a
|
/// Recovers the `Archetype` a `BUILTIN_*` tag came from, or `None` if `tag` is not
|
||||||
/// built-in tag naming a known archetype. Used by the save path to round-trip.
|
/// a built-in tag naming a known archetype. Used by the save path to round-trip.
|
||||||
pub(crate) fn archetype_from_builtin_tag(tag: &str) -> Option<Archetype> {
|
pub(crate) fn archetype_from_builtin_tag(tag: &str) -> Option<Archetype> {
|
||||||
let name = tag.strip_prefix(BUILTIN_TAG_PREFIX)?;
|
let name = tag.strip_prefix(BUILTIN_TAG_PREFIX)?;
|
||||||
Archetype::try_from(name).ok()
|
Archetype::try_from(name).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the embedded script implementing `arch` as an object, or `None` if the
|
|
||||||
/// archetype is a plain terrain cell. The one place script-backed archetypes are
|
|
||||||
/// declared.
|
|
||||||
pub(crate) fn archetype_script(arch: Archetype) -> Option<&'static str> {
|
|
||||||
match arch {
|
|
||||||
Archetype::Pusher(_) => Some(PUSHER),
|
|
||||||
Archetype::Spinner(_) => Some(SPINNER),
|
|
||||||
Archetype::Gem => Some(GEM),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the synthetic `BUILTIN_*` compile-key for `arch`'s embedded script, or
|
|
||||||
/// `None` if the archetype is a plain terrain cell. [`Board::expand_builtin_archetypes`](crate::board::Board::expand_builtin_archetypes)
|
|
||||||
/// stores this as the expanded object's `script_name` so every instance of a
|
|
||||||
/// script-backed archetype shares one compiled AST (the source comes from
|
|
||||||
/// [`archetype_script`]).
|
|
||||||
pub(crate) fn archetype_script_key(arch: Archetype) -> Option<&'static str> {
|
|
||||||
match arch {
|
|
||||||
Archetype::Pusher(_) => Some("BUILTIN_pusher"),
|
|
||||||
Archetype::Spinner(_) => Some("BUILTIN_spinner"),
|
|
||||||
Archetype::Gem => Some("BUILTIN_gem"),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -503,7 +503,7 @@ fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<Object
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::GameState;
|
use super::GameState;
|
||||||
use crate::Direction;
|
use crate::Direction;
|
||||||
use crate::archetype::Archetype;
|
use crate::archetype::{Archetype, Builtin};
|
||||||
use crate::board::tests::{crate_at, open_board, stamp, wall_at};
|
use crate::board::tests::{crate_at, open_board, stamp, wall_at};
|
||||||
use crate::object_def::ObjectDef;
|
use crate::object_def::ObjectDef;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -514,7 +514,7 @@ mod tests {
|
|||||||
// A gem terrain cell at (1,0); expanding turns it into the builtin gem
|
// A gem terrain cell at (1,0); expanding turns it into the builtin gem
|
||||||
// object running scripts/gem.rhai. The player starts at (0,0).
|
// object running scripts/gem.rhai. The player starts at (0,0).
|
||||||
let mut board = open_board(3, 1, (0, 0), vec![]);
|
let mut board = open_board(3, 1, (0, 0), vec![]);
|
||||||
stamp(&mut board, 1, 0, Archetype::Gem);
|
stamp(&mut board, 1, 0, Archetype::Builtin(Builtin::Gem, "gem"));
|
||||||
board.expand_builtin_archetypes();
|
board.expand_builtin_archetypes();
|
||||||
let mut game = GameState::new(board);
|
let mut game = GameState::new(board);
|
||||||
game.run_init();
|
game.run_init();
|
||||||
@@ -537,7 +537,7 @@ mod tests {
|
|||||||
sobj.solid = false;
|
sobj.solid = false;
|
||||||
sobj.script_name = Some("s".to_string());
|
sobj.script_name = Some("s".to_string());
|
||||||
let mut board = open_board(3, 1, (2, 0), vec![sobj]);
|
let mut board = open_board(3, 1, (2, 0), vec![sobj]);
|
||||||
stamp(&mut board, 1, 0, Archetype::Gem);
|
stamp(&mut board, 1, 0, Archetype::Builtin(Builtin::Gem, "gem"));
|
||||||
board.expand_builtin_archetypes();
|
board.expand_builtin_archetypes();
|
||||||
let scripts = HashMap::from([(
|
let scripts = HashMap::from([(
|
||||||
"s".to_string(),
|
"s".to_string(),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ mod utils;
|
|||||||
/// World type: a named collection of boards in a single `.toml` file ([`world::World`], [`world::load`]).
|
/// World type: a named collection of boards in a single `.toml` file ([`world::World`], [`world::load`]).
|
||||||
pub mod world;
|
pub mod world;
|
||||||
|
|
||||||
pub use archetype::{Archetype, SpinDirection};
|
pub use archetype::{Archetype, Builtin};
|
||||||
pub use board::Board;
|
pub use board::Board;
|
||||||
pub use utils::Direction;
|
pub use utils::Direction;
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ fn pusher_loads_as_a_tagged_scripted_solid_object() {
|
|||||||
p.builtin_script.is_some(),
|
p.builtin_script.is_some(),
|
||||||
"carries the embedded pusher script"
|
"carries the embedded pusher script"
|
||||||
);
|
);
|
||||||
// Expansion keys all pushers under one synthetic script name so they share a
|
// Each alias gets its own compile-cache key (e.g. "BUILTIN_pusher_east") so the
|
||||||
// compiled AST; the source still comes from `builtin_script`.
|
// script can read direction from Me.has_tag("BUILTIN_pusher_east").
|
||||||
assert_eq!(p.script_name.as_deref(), Some("BUILTIN_pusher"));
|
assert_eq!(p.script_name.as_deref(), Some("BUILTIN_pusher_east"));
|
||||||
assert!(!board.is_passable(0, 0), "pusher is solid");
|
assert!(!board.is_passable(0, 0), "pusher is solid");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ fn spinner_loads_as_a_tagged_scripted_solid_object() {
|
|||||||
obj.builtin_script.is_some(),
|
obj.builtin_script.is_some(),
|
||||||
"carries the embedded spinner script"
|
"carries the embedded spinner script"
|
||||||
);
|
);
|
||||||
// Expansion keys all spinners under one synthetic script name so they share a
|
// Each alias gets its own compile-cache key (e.g. "BUILTIN_spinner_cw") so the
|
||||||
// compiled AST; the source still comes from `builtin_script`.
|
// script can read direction from Me.has_tag("BUILTIN_spinner_cw").
|
||||||
assert_eq!(obj.script_name.as_deref(), Some("BUILTIN_spinner"));
|
assert_eq!(obj.script_name.as_deref(), Some("BUILTIN_spinner_cw"));
|
||||||
assert!(!board.is_passable(0, 0), "spinner is solid");
|
assert!(!board.is_passable(0, 0), "spinner is solid");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ Renders the board as text: each `Glyph.tile` index is reinterpreted as a charact
|
|||||||
|
|
||||||
**`kiln-tui/src/editor.rs`** — world editor mode:
|
**`kiln-tui/src/editor.rs`** — world editor mode:
|
||||||
- `EditorState` — a non-ticking editing session over a freshly reloaded `World`: `world`, `board_name` (board being edited), `menu: Vec<MenuLevel>` (sidebar menu stack, for breadcrumb + escape-to-parent), `dialog: Option<Dialog<EditorState>>`, `code_editor: Option<CodeEditor>` (the open script editor; replaces the board view), `glyph_dialog: Option<GlyphDialog<EditorState>>` (the open glyph picker overlay), a blinking `cursor`, the **drawing tool** (`current_archetype` + `current_glyph` — the thing stamped on `space`, defaulting to a wall — and `draw_mode`, the toggle that re-stamps on every cursor move), `sidebar_width` (mouse-resizable), blink state, `last_board_area` (written at draw, read for right-click hit-testing), editor-side `log`, and `pending_playtest: Option<GameState>` (a playtest game parked for the run loop to swap into `Mode::Play`).
|
- `EditorState` — a non-ticking editing session over a freshly reloaded `World`: `world`, `board_name` (board being edited), `menu: Vec<MenuLevel>` (sidebar menu stack, for breadcrumb + escape-to-parent), `dialog: Option<Dialog<EditorState>>`, `code_editor: Option<CodeEditor>` (the open script editor; replaces the board view), `glyph_dialog: Option<GlyphDialog<EditorState>>` (the open glyph picker overlay), a blinking `cursor`, the **drawing tool** (`current_archetype` + `current_glyph` — the thing stamped on `space`, defaulting to a wall — and `draw_mode`, the toggle that re-stamps on every cursor move), `sidebar_width` (mouse-resizable), blink state, `last_board_area` (written at draw, read for right-click hit-testing), editor-side `log`, and `pending_playtest: Option<GameState>` (a playtest game parked for the run loop to swap into `Mode::Play`).
|
||||||
- `MenuLevel` (`Main`/`World`/`Floor`/`Terrain`/`Machines`/`Pushers`/`Items`) — a sidebar menu level; (`Main` → `[i] Items` → `[g] ♦ Gem`, which `set_current(Archetype::Gem)`); `title()` (breadcrumb segment) and `entries() -> Vec<MenuEntry>` (the level's lines). A `MenuEntry` is a key-activated `Item` (`[k] Label` + a boxed `FnOnce(&mut EditorState)` action), a `CurrentValue` (a boxed `Fn(&EditorState) -> String` shown indented beneath an item, e.g. the world name), or a `Blank`/`Separator` spacer. An item's action **opens a dialog** (`World`/scripts/boards), **pushes a child level** (`Main` → `World`/`Floor`/`Terrain`/`Machines`; `Machines` → `Pushers`), **sets the drawing tool** (`Terrain`/`Pushers` → `set_current(arch)`, plus `Machines` → `[s]` CW / `[c]` CCW spinner), or **launches a playtest** (`Main` → `[p] Play board` → `playtest()`). The menu is a static letter-keyed stack — picking from a list is always a dialog, so arrow keys never conflict with the board cursor.
|
- `MenuLevel` (`Main`/`World`/`Floor`/`Terrain`/`Machines`/`Pushers`/`Items`) — a sidebar menu level; (`Main` → `[i] Items` → `[t] ♦ Gem`, which `set_current(Archetype::Builtin(Builtin::Gem, "gem"))`); `title()` (breadcrumb segment) and `entries() -> Vec<MenuEntry>` (the level's lines). A `MenuEntry` is a key-activated `Item` (`[k] Label` + a boxed `FnOnce(&mut EditorState)` action), a `CurrentValue` (a boxed `Fn(&EditorState) -> String` shown indented beneath an item, e.g. the world name), or a `Blank`/`Separator` spacer. An item's action **opens a dialog** (`World`/scripts/boards), **pushes a child level** (`Main` → `World`/`Floor`/`Terrain`/`Machines`; `Machines` → `Pushers`), **sets the drawing tool** (`Terrain`/`Pushers` → `set_current(arch)`, plus `Machines` → `[s]` CW / `[c]` CCW spinner, all using `Archetype::Builtin(Builtin::…, "alias")`), or **launches a playtest** (`Main` → `[p] Play board` → `playtest()`). The menu is a static letter-keyed stack — picking from a list is always a dialog, so arrow keys never conflict with the board cursor.
|
||||||
- Methods: `breadcrumb()`, `current_menu()`, `menu_escape(ui)` (pop a level, or at the root open the overlay `editor_menu_items()`), `menu_key(c)` (dispatch a letter → the matched `entries()` item's action via `MenuLevel::perform_key`), `playtest()` (deep-clones the world, sets its start to the edited board, **expands script-backed archetypes** in every board via `Board::expand_builtin_archetypes` so editor-stamped spinners/pushers run, builds a `GameState` + runs `init()`, and parks it in `pending_playtest` for the run loop), and the `open_{name,entry,scripts,boards}_dialog` builders (name → text dialog writing `world.name`; entry → list dialog writing `world.start`; boards → select-only; scripts → **opens the script in `code_editor`**). `open_script_editor(name)` builds a `CodeEditor` from `world.scripts[name]`; `close_script_editor()` saves its text back into `world.scripts` (in-memory, like the other dialogs).
|
- Methods: `breadcrumb()`, `current_menu()`, `menu_escape(ui)` (pop a level, or at the root open the overlay `editor_menu_items()`), `menu_key(c)` (dispatch a letter → the matched `entries()` item's action via `MenuLevel::perform_key`), `playtest()` (deep-clones the world, sets its start to the edited board, **expands script-backed archetypes** in every board via `Board::expand_builtin_archetypes` so editor-stamped spinners/pushers run, builds a `GameState` + runs `init()`, and parks it in `pending_playtest` for the run loop), and the `open_{name,entry,scripts,boards}_dialog` builders (name → text dialog writing `world.name`; entry → list dialog writing `world.start`; boards → select-only; scripts → **opens the script in `code_editor`**). `open_script_editor(name)` builds a `CodeEditor` from `world.scripts[name]`; `close_script_editor()` saves its text back into `world.scripts` (in-memory, like the other dialogs).
|
||||||
- Drawing: `set_current(arch)` sets `current_archetype` and resets `current_glyph` to its default; `open_glyph_picker()` seeds a `GlyphDialog` from `current_glyph` and writes the chosen glyph back (bound to **`g`**); `place_current()` stamps the current archetype+glyph at the cursor via `Board::place_archetype`; `toggle_draw_mode()` flips `draw_mode`. `place_current` is bound to **`space`** and `toggle_draw_mode` to **`tab`** in `handle_editor_input`; `move_cursor`/`set_cursor_at_screen` also call `place_current` when `draw_mode` is on and the cursor enters a new cell. The sidebar's bottom **drawing-controls footer** (`draw_footer_lines`) shows the archetype name, `[g] Glyph` + a live colored preview, `[spc] Place`, and `[tab] Draw mode (on/off)`.
|
- Drawing: `set_current(arch)` sets `current_archetype` and resets `current_glyph` to its default; `open_glyph_picker()` seeds a `GlyphDialog` from `current_glyph` and writes the chosen glyph back (bound to **`g`**); `place_current()` stamps the current archetype+glyph at the cursor via `Board::place_archetype`; `toggle_draw_mode()` flips `draw_mode`. `place_current` is bound to **`space`** and `toggle_draw_mode` to **`tab`** in `handle_editor_input`; `move_cursor`/`set_cursor_at_screen` also call `place_current` when `draw_mode` is on and the cursor enters a new cell. The sidebar's bottom **drawing-controls footer** (`draw_footer_lines`) shows the archetype name, `[g] Glyph` + a live colored preview, `[spc] Place`, and `[tab] Draw mode (on/off)`.
|
||||||
- `editor_menu_items()` — the overlay editor menu (`[p]` play, `[q]` quit, `[esc]` resume), opened at the root of the menu tree. `handle_dialog_input(event, ed)` — forwards to the open dialog and, on `Submit`/`Cancel`, `take`s it out and calls `finish(.., ed)`. `handle_glyph_dialog_input(event, ed)` — the same pattern for the open `glyph_dialog`. `handle_code_editor_input(event, ed)` — forwards to the open code editor and, on `Exit` (Esc), calls `close_script_editor()`. `draw_editor(frame, ed, ui)` / `editor_layout(..)` — the **code editor (when open) else** the board (with blinking cursor + coord readout) in the left area, a resizable right-hand sidebar (breadcrumb title + the menu choices, each with its optional current-value line), and the optional full-width log panel.
|
- `editor_menu_items()` — the overlay editor menu (`[p]` play, `[q]` quit, `[esc]` resume), opened at the root of the menu tree. `handle_dialog_input(event, ed)` — forwards to the open dialog and, on `Submit`/`Cancel`, `take`s it out and calls `finish(.., ed)`. `handle_glyph_dialog_input(event, ed)` — the same pattern for the open `glyph_dialog`. `handle_code_editor_input(event, ed)` — forwards to the open code editor and, on `Exit` (Esc), calls `close_script_editor()`. `draw_editor(frame, ed, ui)` / `editor_layout(..)` — the **code editor (when open) else** the board (with blinking cursor + coord readout) in the left area, a resizable right-hand sidebar (breadcrumb title + the menu choices, each with its optional current-value line), and the optional full-width log panel.
|
||||||
@@ -61,7 +61,7 @@ Renders the board as text: each `Glyph.tile` index is reinterpreted as a charact
|
|||||||
- `board_screen_pos(area, board, bx, by) -> Option<(u16, u16)>` — converts a board cell coordinate to terminal screen coordinates, returning `None` if off-screen. Used by bubble placement.
|
- `board_screen_pos(area, board, bx, by) -> Option<(u16, u16)>` — converts a board cell coordinate to terminal screen coordinates, returning `None` if off-screen. Used by bubble placement.
|
||||||
|
|
||||||
**`kiln-tui/src/status.rs`** — play-mode status sidebar widget:
|
**`kiln-tui/src/status.rs`** — play-mode status sidebar widget:
|
||||||
- `StatusSidebarWidget` — a `ratatui::widgets::Widget` built with `new(health, gems)` that draws the player's stats in a bordered `Block` titled `Status`: a `Health:` label above a row of `MAX_HEARTS` (5) `♥` glyphs (first `health` bright red, rest dark red) and a `Gems: ♦ N` line. The gem indicator is rendered from `kiln_core::Archetype::Gem.default_glyph()` (char via `cp437::tile_to_char`, color via `rgba8_to_color`) so it matches gems on the board — not a hardcoded glyph/color. Purely presentational — reads the values handed in, never mutates game state. Drawn by `draw_play` when `ui.show_status` is set; `Tab` toggles it.
|
- `StatusSidebarWidget` — a `ratatui::widgets::Widget` built with `new(health, gems)` that draws the player's stats in a bordered `Block` titled `Status`: a `Health:` label above a row of `MAX_HEARTS` (5) `♥` glyphs (first `health` bright red, rest dark red) and a `Gems: ♦ N` line. The gem indicator is rendered from `Archetype::Builtin(Builtin::Gem, "gem").default_glyph()` (char via `cp437::tile_to_char`, color via `rgba8_to_color`) so it matches gems on the board — not a hardcoded glyph/color. Purely presentational — reads the values handed in, never mutates game state. Drawn by `draw_play` when `ui.show_status` is set; `Tab` toggles it.
|
||||||
|
|
||||||
**`kiln-tui/src/log.rs`** — log panel widget:
|
**`kiln-tui/src/log.rs`** — log panel widget:
|
||||||
- `LogState { open, height, scroll }` — panel visibility, height in rows, and scroll offset. `toggle()`, `scroll_by(delta, log_len)`, `resize(target, total)`.
|
- `LogState { open, height, scroll }` — panel visibility, height in rows, and scroll offset. `toggle()`, `scroll_by(delta, log_len)`, `resize(target, total)`.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::editor::EditorState;
|
use crate::editor::EditorState;
|
||||||
use crate::menu::{MenuItem, MenuKey};
|
use crate::menu::{MenuItem, MenuKey};
|
||||||
use crate::mode::PendingMode;
|
use crate::mode::PendingMode;
|
||||||
use kiln_core::{Archetype, Direction, SpinDirection};
|
use kiln_core::{Archetype, Builtin};
|
||||||
|
|
||||||
/// One level in the editor's sidebar menu tree.
|
/// One level in the editor's sidebar menu tree.
|
||||||
///
|
///
|
||||||
@@ -80,29 +80,29 @@ impl MenuLevel {
|
|||||||
MenuLevel::Machines => vec![
|
MenuLevel::Machines => vec![
|
||||||
MenuEntry::item('p', "Pushers...", |ed| ed.menu.push(MenuLevel::Pushers)),
|
MenuEntry::item('p', "Pushers...", |ed| ed.menu.push(MenuLevel::Pushers)),
|
||||||
MenuEntry::item('s', "/ CW spinner", |ed| {
|
MenuEntry::item('s', "/ CW spinner", |ed| {
|
||||||
ed.set_current(Archetype::Spinner(SpinDirection::Clockwise))
|
ed.set_current(Archetype::Builtin(Builtin::Spinner, "spinner_cw"))
|
||||||
}),
|
}),
|
||||||
MenuEntry::item('c', "\\ CCW spinner", |ed| {
|
MenuEntry::item('c', "\\ CCW spinner", |ed| {
|
||||||
ed.set_current(Archetype::Spinner(SpinDirection::CounterClockwise))
|
ed.set_current(Archetype::Builtin(Builtin::Spinner, "spinner_ccw"))
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
MenuLevel::Pushers => vec![
|
MenuLevel::Pushers => vec![
|
||||||
MenuEntry::item('n', "▲ Pusher", |ed| {
|
MenuEntry::item('n', "▲ Pusher", |ed| {
|
||||||
ed.set_current(Archetype::Pusher(Direction::North))
|
ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_north"))
|
||||||
}),
|
}),
|
||||||
MenuEntry::item('s', "▼ Pusher", |ed| {
|
MenuEntry::item('s', "▼ Pusher", |ed| {
|
||||||
ed.set_current(Archetype::Pusher(Direction::South))
|
ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_south"))
|
||||||
}),
|
}),
|
||||||
MenuEntry::item('e', "► Pusher", |ed| {
|
MenuEntry::item('e', "► Pusher", |ed| {
|
||||||
ed.set_current(Archetype::Pusher(Direction::East))
|
ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_east"))
|
||||||
}),
|
}),
|
||||||
MenuEntry::item('w', "◄ Pusher", |ed| {
|
MenuEntry::item('w', "◄ Pusher", |ed| {
|
||||||
ed.set_current(Archetype::Pusher(Direction::West))
|
ed.set_current(Archetype::Builtin(Builtin::Pusher, "pusher_west"))
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
MenuLevel::Items => vec![MenuEntry::item('t', "♦ Gem", |ed| {
|
MenuLevel::Items => vec![MenuEntry::item('t', "♦ Gem", |ed| {
|
||||||
// gem and glyph start with the same letter
|
// 't' for 'treasure' — 'g' conflicts with glyph picker
|
||||||
ed.set_current(Archetype::Gem)
|
ed.set_current(Archetype::Builtin(Builtin::Gem, "gem"))
|
||||||
})],
|
})],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//! never mutates game state.
|
//! never mutates game state.
|
||||||
|
|
||||||
use crate::utils::rgba8_to_color;
|
use crate::utils::rgba8_to_color;
|
||||||
use kiln_core::Archetype;
|
use kiln_core::{Archetype, Builtin};
|
||||||
use kiln_core::cp437::tile_to_char;
|
use kiln_core::cp437::tile_to_char;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
@@ -54,7 +54,7 @@ impl Widget for StatusSidebarWidget {
|
|||||||
|
|
||||||
// Draw the gem indicator from the gem archetype's default glyph, so the
|
// Draw the gem indicator from the gem archetype's default glyph, so the
|
||||||
// sidebar matches what gems look like on the board (no hardcoded ♦/color).
|
// sidebar matches what gems look like on the board (no hardcoded ♦/color).
|
||||||
let gem_glyph = Archetype::Gem.default_glyph();
|
let gem_glyph = Archetype::Builtin(Builtin::Gem, "gem").default_glyph();
|
||||||
let gem_char = tile_to_char(gem_glyph.tile).to_string();
|
let gem_char = tile_to_char(gem_glyph.tile).to_string();
|
||||||
let gem_style = Style::default().fg(rgba8_to_color(gem_glyph.fg));
|
let gem_style = Style::default().fg(rgba8_to_color(gem_glyph.fg));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user