builtin macro

This commit is contained in:
2026-06-23 01:08:01 -05:00
parent a67f3fa8cf
commit 2e160d6c61
12 changed files with 237 additions and 237 deletions
+167 -135
View File
@@ -1,7 +1,117 @@
use crate::glyph::Glyph;
use crate::utils::{Behavior, Direction, Pushable};
use crate::utils::{Behavior, Pushable};
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.
///
/// `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"`),
/// 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`.
/// TODO: In the future, Object passability and opacity will come from the cell's Rhai script
/// rather than from this enum. [`Board::is_passable`] will need a special branch
/// at that point. `ErrorBlock` is used as a sentinel for unrecognized archetype
/// names in map files — it should never appear in a valid board.
/// The [`Builtin`] variant covers all script-backed types (pushers, spinners,
/// gems). These are map-file keywords only: at load they expand into scripted
/// [`ObjectDef`]s carrying the embedded Rhai source and a `BUILTIN_<alias>` tag
/// (see [`crate::builtin_scripts`] and [`Board::expand_builtin_archetypes`]).
///
/// `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)]
pub enum Archetype {
/// An open cell; the player and other entities can pass through it.
@@ -32,46 +147,26 @@ pub enum Archetype {
HCrate,
/// A crate that can only be pushed north/south (vertical axis). Glyph ↕.
VCrate,
/// A directional pusher — solid, opaque, non-pushable. This is a map-file
/// *keyword* only: at load it expands into a scripted [`ObjectDef`] carrying the
/// embedded `pusher.rhai` and a `BUILTIN_pusher_<dir>` tag (see
/// [`crate::builtin_scripts`]). The script self-propels it one cell at a time in
/// the facing direction, shoving any pushable chain ahead — no engine special-case.
Pusher(Direction),
/// A rotating machine — solid, opaque, non-pushable. Like [`Pusher`](Archetype::Pusher)
/// 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>`
/// tag (see [`crate::builtin_scripts`]). The script reads the tag for its spin
/// 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,
/// A script-backed archetype expanded from a map-file keyword.
///
/// - `Builtin` is the family (e.g. `Builtin::Pusher`), which selects the
/// shared script and behavior.
/// - `&'static str` is the specific alias matched during parsing (e.g.
/// `"pusher_north"`), used as the per-alias glyph key and the
/// `BUILTIN_<alias>` tag on the expanded object.
///
/// See [`Builtin`] and the [`builtins!`] invocation for the full registry.
Builtin(Builtin, &'static str),
/// Sentinel for map files that reference an unknown archetype name.
/// Renders as a yellow `?` on red to make the error visible in-game.
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 {
/// Returns the default [`Behavior`] for this archetype.
///
/// For `Object`, this is a placeholder until Rhai scripts drive the behavior.
pub fn behavior(&self) -> Behavior {
match self {
Archetype::Builtin(b, _) => b.behavior(),
Archetype::Empty => Behavior {
solid: false,
opaque: false,
@@ -102,25 +197,6 @@ impl Archetype {
pushable: Pushable::Vertical,
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 {
solid: true,
opaque: true,
@@ -133,18 +209,12 @@ impl Archetype {
/// Returns the canonical name used to reference this archetype in map files.
pub fn name(&self) -> &'static str {
match self {
Archetype::Builtin(_, alias) => alias,
Archetype::Empty => "empty",
Archetype::Wall => "wall",
Archetype::Crate => "crate",
Archetype::HCrate => "hcrate",
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",
}
}
@@ -156,6 +226,7 @@ impl Archetype {
#[rustfmt::skip]
pub fn default_glyph(&self) -> Glyph {
match self {
Archetype::Builtin(b, alias) => b.default_glyph_for(alias),
Archetype::Empty => Glyph {
tile: 0,
fg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
@@ -168,47 +239,17 @@ impl Archetype {
},
Archetype::Crate => Glyph {
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 },
},
Archetype::HCrate => Glyph {
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 },
},
Archetype::VCrate => Glyph {
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 },
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
fg: Rgba8 { r: 0xAA, g: 0xAA, b: 0xAA, a: 255 },
bg: Rgba8 { r: 0, g: 0, b: 0, a: 255 },
},
// Visually distinct so malformed map files are immediately obvious.
@@ -226,24 +267,23 @@ impl TryFrom<&str> for Archetype {
/// Parses an archetype by its map-file name.
///
/// Returns an error for unrecognized names; the caller should substitute
/// [`Archetype::ErrorBlock`] and log the error so the problem is visible.
/// Checks the [`Builtin`] registry first (pushers, spinners, gems), then
/// 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> {
// 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 {
"empty" => Ok(Archetype::Empty),
"wall" => Ok(Archetype::Wall),
"crate" => Ok(Archetype::Crate),
"hcrate" => Ok(Archetype::HCrate),
"vcrate" => Ok(Archetype::VCrate),
// "object" is intentionally absent: objects are not valid palette
// entries in map files. They live in [[objects]] with their own glyph.
"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),
// "object", "portal", "player" are intentionally absent: they are
// meta-kinds handled by the layer builder, not Archetype variants.
_ => Err(format!("unknown archetype: {name}")),
}
}
@@ -251,7 +291,7 @@ impl TryFrom<&str> for Archetype {
#[cfg(test)]
mod tests {
use super::{Archetype, SpinDirection};
use super::Archetype;
#[test]
fn directional_crate_default_glyph_tiles() {
@@ -260,33 +300,25 @@ mod tests {
}
#[test]
fn spinner_names_round_trip() {
for arch in [
Archetype::Spinner(SpinDirection::Clockwise),
Archetype::Spinner(SpinDirection::CounterClockwise),
fn builtin_names_glyphs_and_round_trip() {
// All known aliases must parse, round-trip via name(), and give the right tile.
for (name, tile) in [
("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!(
arch.default_glyph().tile,
tile,
"'{name}' has the correct default tile"
);
}
assert_eq!(
Archetype::try_from("spinner_cw"),
Ok(Archetype::Spinner(SpinDirection::Clockwise))
);
}
#[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
View File
@@ -6,7 +6,6 @@ use crate::object_def::ObjectDef;
use crate::utils::Direction;
use crate::utils::{Behavior, ObjectId, Player, PortalDef, Pushable, RegistryValue, Solid};
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).
///
@@ -499,14 +498,14 @@ impl Board {
/// 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.
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.
let mut found: Vec<(usize, usize, usize, Glyph, Archetype)> = Vec::new();
for z in 0..self.layers.len() {
for y in 0..self.height {
for x in 0..self.width {
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));
}
}
@@ -516,20 +515,25 @@ impl Board {
// Vacate the terrain cell (revealing any floor beneath), then spawn the
// object — mirroring `resolve_entry`'s object template.
*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);
obj.z = z;
obj.glyph = glyph;
obj.solid = b.solid;
obj.opaque = b.opaque;
obj.solid = beh.solid;
obj.opaque = beh.opaque;
// Carry the archetype's pushability/grab onto the object (pushers and
// spinners are Pushable::No, so they stay unpushable; gems are pushable
// and grabbable).
obj.pushable = b.pushable != crate::utils::Pushable::No;
obj.grab = b.grab;
obj.builtin_script = archetype_script(arch);
obj.script_name = archetype_script_key(arch).map(|s| s.to_owned());
obj.tags.insert(builtin_tag(arch));
obj.pushable = beh.pushable != crate::utils::Pushable::No;
obj.grab = beh.grab;
obj.builtin_script = Some(b.script());
// The compile-cache key and the BUILTIN_* tag are both derived from the
// 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);
}
}
@@ -614,7 +618,7 @@ impl Board {
#[cfg(test)]
pub(crate) mod tests {
use super::Board;
use crate::archetype::{Archetype, SpinDirection};
use crate::archetype::{Archetype, Builtin};
use crate::glyph::Glyph;
use crate::layer::Layer;
use crate::object_def::ObjectDef;
@@ -963,12 +967,7 @@ pub(crate) mod tests {
#[test]
fn expand_builtin_archetypes_replaces_a_spinner_cell_with_an_object() {
let mut board = open_board(3, 1, (2, 0), vec![]);
stamp(
&mut board,
0,
0,
Archetype::Spinner(SpinDirection::Clockwise),
);
stamp(&mut board, 0, 0, Archetype::Builtin(Builtin::Spinner, "spinner_cw"));
board.expand_builtin_archetypes();
// The terrain cell is vacated and a scripted object takes its place.
+23 -54
View File
@@ -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
//! ordinary scripted object can express exactly. Rather than special-case them in
//! the engine, the map loader *expands* such an archetype into an [`ObjectDef`]
//! carrying an embedded Rhai script (see [`ObjectDef::builtin_script`]) plus a
//! `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.
//! When a [`Builtin`] archetype cell is expanded into an [`ObjectDef`] by
//! [`Board::expand_builtin_archetypes`], the object receives a `BUILTIN_<alias>`
//! tag (e.g. `"BUILTIN_pusher_north"`) so its Rhai script can read which specific
//! variant it is (via `Me.has_tag("BUILTIN_pusher_north")`).
//!
//! This module is the single place to register a new script-backed archetype:
//! add an arm to [`archetype_script`].
//! The save path ([`map_file`]) uses [`archetype_from_builtin_tag`] to collapse
//! 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;
/// 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_";
/// The pusher behavior, embedded into the binary. Shared by every direction —
/// direction comes from the object's tag, so all pushers share one compiled AST.
const PUSHER: &str = include_str!("scripts/pusher.rhai");
/// 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"`.
/// Returns the `BUILTIN_<alias>` tag for `arch` — e.g. `"BUILTIN_pusher_east"`.
///
/// 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.
pub(crate) fn builtin_tag(arch: Archetype) -> String {
format!("{BUILTIN_TAG_PREFIX}{}", arch.name())
}
/// Recovers the archetype a `BUILTIN_*` tag came from, or `None` if `tag` is not a
/// built-in tag naming a known archetype. Used by the save path to round-trip.
/// Recovers the `Archetype` a `BUILTIN_*` tag came from, or `None` if `tag` is not
/// 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> {
let name = tag.strip_prefix(BUILTIN_TAG_PREFIX)?;
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,
}
}
+3 -3
View File
@@ -503,7 +503,7 @@ fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<Object
mod tests {
use super::GameState;
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::object_def::ObjectDef;
use std::collections::HashMap;
@@ -514,7 +514,7 @@ mod tests {
// 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).
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();
let mut game = GameState::new(board);
game.run_init();
@@ -537,7 +537,7 @@ mod tests {
sobj.solid = false;
sobj.script_name = Some("s".to_string());
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();
let scripts = HashMap::from([(
"s".to_string(),
+1 -1
View File
@@ -23,7 +23,7 @@ mod utils;
/// World type: a named collection of boards in a single `.toml` file ([`world::World`], [`world::load`]).
pub mod world;
pub use archetype::{Archetype, SpinDirection};
pub use archetype::{Archetype, Builtin};
pub use board::Board;
pub use utils::Direction;
+3 -3
View File
@@ -36,9 +36,9 @@ fn pusher_loads_as_a_tagged_scripted_solid_object() {
p.builtin_script.is_some(),
"carries the embedded pusher script"
);
// Expansion keys all pushers under one synthetic script name so they share a
// compiled AST; the source still comes from `builtin_script`.
assert_eq!(p.script_name.as_deref(), Some("BUILTIN_pusher"));
// Each alias gets its own compile-cache key (e.g. "BUILTIN_pusher_east") so the
// script can read direction from Me.has_tag("BUILTIN_pusher_east").
assert_eq!(p.script_name.as_deref(), Some("BUILTIN_pusher_east"));
assert!(!board.is_passable(0, 0), "pusher is solid");
}
+3 -3
View File
@@ -25,9 +25,9 @@ fn spinner_loads_as_a_tagged_scripted_solid_object() {
obj.builtin_script.is_some(),
"carries the embedded spinner script"
);
// Expansion keys all spinners under one synthetic script name so they share a
// compiled AST; the source still comes from `builtin_script`.
assert_eq!(obj.script_name.as_deref(), Some("BUILTIN_spinner"));
// Each alias gets its own compile-cache key (e.g. "BUILTIN_spinner_cw") so the
// script can read direction from Me.has_tag("BUILTIN_spinner_cw").
assert_eq!(obj.script_name.as_deref(), Some("BUILTIN_spinner_cw"));
assert!(!board.is_passable(0, 0), "spinner is solid");
}