spinners and gems

This commit is contained in:
2026-06-21 18:27:45 -05:00
parent 9b53552f4a
commit d4b9278838
17 changed files with 626 additions and 56 deletions
+72 -8
View File
@@ -8,6 +8,8 @@
//! - `tick(dt)` — run every frame with the elapsed seconds (see [`ScriptHost::run_tick`]).
//! - `bump(id)` — run when another mover steps into this object's cell, with the
//! bumper's object index (`-1` for the player); see [`ScriptHost::run_bump`].
//! - `grab()` — run when the player walks onto (or pushes into themselves) a
//! `grab` object; see [`ScriptHost::run_grab`]. Typically adds a stat + `die()`s.
//!
//! ## Script constants
//!
@@ -55,7 +57,7 @@
//! `move(dir)`, `delay(secs)`, `now()`, `set_tile(n)`, `log(msg)`, `say(msg)`,
//! `set_fg(fg)`, `set_bg(bg)`, `set_color(fg, bg)`, `set_tag(target, tag, present)`,
//! `send(target_id, fn_name [, arg])`, `teleport(x, y)`, `push(x, y, dir)`,
//! `swap([[src_x, src_y, dst_x, dst_y], …])`
//! `swap([[src_x, src_y, dst_x, dst_y], …])`, `add_gems(n)`, `die()`
//!
//! ## Queue API
//!
@@ -106,6 +108,7 @@ struct CompiledScript {
has_init: bool,
has_tick: bool,
has_bump: bool,
has_grab: bool,
}
/// The runtime state of one scripted object.
@@ -122,11 +125,7 @@ struct ObjectRuntime {
/// built-in (so identical built-ins share one compiled AST), or the world-pool
/// name for a named script. `None` if the object has no script.
fn script_key(obj: &ObjectDef) -> Option<String> {
if let Some(src) = obj.builtin_script {
Some(src.to_string())
} else {
obj.script_name.clone()
}
obj.script_name.clone()
}
// ── Rhai types exposed to scripts ────────────────────────────────────────────
@@ -266,6 +265,7 @@ impl ScriptHost {
has_init: defines("init", 0),
has_tick: defines("tick", 1),
has_bump: defines("bump", 1),
has_grab: defines("grab", 0),
ast,
},
);
@@ -356,6 +356,48 @@ impl ScriptHost {
self.drain(i, 0.0);
}
/// Calls `grab()` on the object with [`ObjectId`] `object_id`, if it defines the
/// hook, then drains its queue with `dt = 0`.
///
/// Fired when the player walks onto a grab object or a grab object is pushed
/// into the player (see [`GameState`](crate::game::GameState)). The hook
/// typically increments a player stat and removes the object via `die()`.
pub fn run_grab(&mut self, object_id: ObjectId) {
let Some(i) = self.objects.iter().position(|o| o.object_id == object_id) else {
return;
};
{
let Self {
engine,
scripts,
objects,
errors,
..
} = self;
let obj = &mut objects[i];
let Some(compiled) = scripts.get(&obj.script_key) else {
return;
};
if !compiled.has_grab {
return;
}
let options = CallFnOptions::default().with_tag(object_id as i64);
if let Err(err) = engine.call_fn_with_options::<()>(
options,
&mut obj.scope,
&compiled.ast,
"grab",
(),
) {
errors.borrow_mut().push(LogLine::error(format!(
"script '{}' grab error: {err}",
obj.script_key
)));
}
}
self.drain(i, 0.0);
}
/// Calls the named function on the object with [`ObjectId`] `target_id`.
///
/// If `arg` is `Some` and the function accepts one parameter, the arg is passed.
@@ -689,8 +731,9 @@ fn register_read_api(
});
// can_shift(x, y, dir) — like can_push but inspects only the cell ahead: (x, y)
// holds a pushable and the next cell is empty or another pushable. False
// off-board. The right gate for a simultaneous shift/rotation via swap().
// holds a pushable and the next cell is empty or another pushable. Shifting
// onto the player counts only when the source is a grab thing (it gets
// grabbed). False off-board. The right gate for a shift/rotation via swap().
let b = board.clone();
engine.register_fn("can_shift", move |x: i64, y: i64, dir: Direction| -> bool {
let board = b.borrow();
@@ -706,6 +749,15 @@ fn register_read_api(
board.in_bounds((x as i32, y as i32)) && board.is_passable(x as usize, y as usize)
});
// combinable(x1, y1, x2, y2) — true if the solids at (x1, y1) and (x2, y2) can coexist in a cell
// (meaning, either one contains no solid, or one contains a grab solid and the other contains the
// player). Off-board is not combinable with anything.
let b = board.clone();
engine.register_fn("combinable", move |x1: i64, y1: i64, x2: i64, y2: i64| -> bool {
let board = b.borrow();
board.is_combinable(x1 as usize, y1 as usize, x2 as usize, y2 as usize)
});
// Board.tagged(tag) -> Array[ObjectInfo]
let b = board.clone();
engine.register_fn(
@@ -841,6 +893,18 @@ fn register_write_api(engine: &mut Engine, queues: &QueueMap) {
emit(&q, source_of(&ctx), Action::SetTile(tile as u32));
});
// add_gems(n): change the player's gem count (negative subtracts; clamped at 0).
let q = queues.clone();
engine.register_fn("add_gems", move |ctx: NativeCallContext, n: i64| {
emit(&q, source_of(&ctx), Action::AddGems(n));
});
// die(): remove the calling object from the board.
let q = queues.clone();
engine.register_fn("die", move |ctx: NativeCallContext| {
emit(&q, source_of(&ctx), Action::Die);
});
let q = queues.clone();
engine.register_fn(
"log",