only tick nonwaiting objects

This commit is contained in:
2026-07-11 12:04:33 -05:00
parent b1b723fd1b
commit f1eaaae5d0
7 changed files with 55 additions and 13 deletions
+5
View File
@@ -75,6 +75,11 @@ impl ObjQueue {
matches!(self.0.borrow().front(), Some(Action::Delay(_)))
}
/// Whether this queue currently holds no pending actions.
pub fn is_empty(&self) -> bool {
self.0.borrow().is_empty()
}
pub fn clear(&mut self) {
self.0.borrow_mut().clear();
}
+6 -1
View File
@@ -235,7 +235,12 @@ impl ScriptHost {
&& let Some(script) = self.scripts.get(script_key)
&& let Some(scope) = self.scopes.get_mut(&id) {
if script.has(hook) {
// `tick` only fires on an object whose previous output has fully
// drained. A non-empty queue means we just advance the pending
// actions this frame (the unconditional drain below) without
// re-running the script. Every other hook (init/bump/enter/grab/
// send) fires regardless of queued actions.
if script.has(hook) && (hook != Hook::Tick || info.queue.is_empty()) {
let mut args = vec![Dynamic::from(info.clone())];
if let Some(d) = arg { args.push(d) }
+31
View File
@@ -383,6 +383,37 @@ fn a_later_object_sees_an_earlier_objects_move_this_tick() {
assert_eq!(log_texts(&game), vec!["blocked"]);
}
#[test]
fn tick_is_gated_on_an_empty_queue() {
// `tick` fires only when the object's output queue is empty — so an *unguarded*
// tick that just `move`s east paces itself one step per drained move instead of
// piling up. `move` enqueues a Move plus a 0.25s Delay; while that Delay is still
// draining the engine skips re-running `tick`. With 100 ms frames the object steps
// once (x=1) then waits ~0.25s (the pending Delay) before the next call fires.
// Under the old every-frame model an unguarded tick would enqueue a fresh move each
// frame, racing the object east far faster.
let obj = scripted_object(0, 0, "m");
let board = open_board(6, 1, (5, 0), vec![obj]);
let mut game = GameState::with_scripts(
board,
// Note: no `if m.queue.length == 0` guard — the engine provides it.
scripts_from(&[("m", "fn tick(m, dt) { move(East); }")]),
);
game.run_init();
// Over 0.3s only the first move has resolved; the pending Delay suppresses the
// re-tick, so the object is still at x=1 rather than having stacked more moves.
for _ in 0..3 {
game.tick(Duration::from_millis(100));
}
assert_eq!(game.board().objects[&1].x, 1);
// Once the Delay fully drains the queue empties, so the next frame calls `tick`
// again and the object takes its second step.
game.tick(Duration::from_millis(100));
assert_eq!(game.board().objects[&1].x, 2);
}
#[test]
fn a_send_cycle_terminates_via_the_called_guard() {
// Two objects send "go" to each other in a cycle. Without the per-invocation