enter hook

This commit is contained in:
2026-07-11 11:47:45 -05:00
parent cdeae455dc
commit b1b723fd1b
10 changed files with 371 additions and 43 deletions
+131 -22
View File
@@ -17,6 +17,9 @@ use std::time::Duration;
struct Events {
/// `(bumped object, direction the bump came from)` for each triggered `bump`.
bumps: Vec<(ObjectId, Direction)>,
/// `(entered non-solid object, direction the entrant came from)` for each
/// `enter` — a solid relocating onto a non-solid object's cell.
enters: Vec<(ObjectId, Direction)>,
/// `(target object, function name, argument)` for each `send`.
sends: Vec<(ObjectId, String, SendArg)>,
}
@@ -25,12 +28,13 @@ impl Events {
/// Appends `other`'s reactions onto `self`.
fn merge(&mut self, other: Events) {
self.bumps.extend(other.bumps);
self.enters.extend(other.enters);
self.sends.extend(other.sends);
}
/// Whether there is anything left to fire.
fn is_empty(&self) -> bool {
self.bumps.is_empty() && self.sends.is_empty()
self.bumps.is_empty() && self.enters.is_empty() && self.sends.is_empty()
}
}
@@ -267,6 +271,8 @@ impl GameState {
// also borrows `self`) is held.
let log_sink = self.scripts.log_sink().clone();
let mut bumps: Vec<(ObjectId, Direction)> = Vec::new();
// `enter` reactions: a solid relocating onto a non-solid object's cell.
let mut enters: Vec<(ObjectId, Direction)> = Vec::new();
// Net change to player stats from AddGems / AlterHealth actions; applied
// to `self` after the board borrow drops.
let mut gem_delta: i64 = 0;
@@ -282,11 +288,16 @@ impl GameState {
for ba in actions {
match ba.action {
Action::Move(dir) => {
if let Some(bumped) = step_object(&mut board, ba.source, dir) {
// The bump "comes from" the side the mover advanced from,
// i.e. the opposite of its travel direction.
let StepOutcome { bumped, entered } =
step_object(&mut board, ba.source, dir);
// The bump / enter "comes from" the side the mover advanced
// from, i.e. the opposite of its travel direction.
if let Some(bumped) = bumped {
bumps.push((bumped, dir.opposite()));
}
for id in entered {
enters.push((id, dir.opposite()));
}
}
Action::SetTile(tile) => {
if let Some(obj) = board.objects.get_mut(&ba.source) {
@@ -359,8 +370,19 @@ impl GameState {
"teleport(player,{x},{y}): destination is solid"
));
} else {
let (old_x, old_y) = (board.player.x, board.player.y);
board.player.x = ux as i64;
board.player.y = uy as i64;
// The player is solid, so it may land on non-solids:
// fire `enter` with a best-effort came-from direction
// (the jump is arbitrary, so it has no exact cardinal).
if let Some(from) =
Direction::from_delta(old_x - ux as i64, old_y - uy as i64)
{
for id in board.non_solid_object_ids_at(ux, uy) {
enters.push((id, from));
}
}
}
} else if let Ok(tid) = ObjectId::try_from(target) {
// Move object `tid` to (x, y). A solid destination blocks
@@ -372,6 +394,7 @@ impl GameState {
)),
Some(obj) => {
let source_solid = obj.solid;
let (old_x, old_y) = (obj.x as i64, obj.y as i64);
let blocked = source_solid
&& matches!(
board.solid_at(ux, uy),
@@ -381,9 +404,23 @@ impl GameState {
log_sink.error(format!(
"teleport({target},{x},{y}): destination is solid"
));
} else if let Some(obj) = board.objects.get_mut(&tid) {
obj.x = ux;
obj.y = uy;
} else {
if let Some(obj) = board.objects.get_mut(&tid) {
obj.x = ux;
obj.y = uy;
}
// Only a solid mover triggers `enter`; the
// direction is best-effort (arbitrary jump).
if source_solid
&& let Some(from) = Direction::from_delta(
old_x - ux as i64,
old_y - uy as i64,
)
{
for id in board.non_solid_object_ids_at(ux, uy) {
enters.push((id, from));
}
}
}
}
}
@@ -398,14 +435,35 @@ impl GameState {
if !board.in_bounds((x, y)) {
log_sink.error(format!("push({x},{y}): out of bounds"));
} else {
board.push(x as usize, y as usize, dir);
// Each pushed solid stepped one cell in `dir`; fire `enter`
// on any non-solid it landed on (came-from `dir.opposite()`).
for (cx, cy) in board.push(x as usize, y as usize, dir) {
for id in board.non_solid_object_ids_at(cx, cy) {
enters.push((id, dir.opposite()));
}
}
}
}
// apply_shift moves the named cells, returning any error lines.
// apply_shift moves the named cells, returning error lines plus the
// relocations it performed (for `enter` at each destination).
Action::Shift(cells) => {
for line in board.apply_shift(&cells) {
let outcome = board.apply_shift(&cells);
for line in outcome.errors {
log_sink.line(line);
}
for (from, to) in outcome.moves {
// A shift can rotate non-adjacent cells, so the came-from
// direction is best-effort (dominant axis of the jump).
if let Some(from_dir) =
Direction::from_delta(from.0 - to.0, from.1 - to.1)
{
for id in
board.non_solid_object_ids_at(to.0 as usize, to.1 as usize)
{
enters.push((id, from_dir));
}
}
}
}
// Accumulated and applied to `self.player.gems` after the borrow drops.
Action::AddGems(n) => gem_delta += n,
@@ -443,7 +501,11 @@ impl GameState {
}
}
// Return the reactions for the caller's settle pass rather than firing them here.
Events { bumps, sends }
Events {
bumps,
enters,
sends,
}
}
/// Fires the `bump` / `send` reactions in `events` (and any they cascade into)
@@ -465,6 +527,14 @@ impl GameState {
let actions = self.scripts.run_bump(id, dir);
next.merge(self.apply_actions(actions));
}
for (id, dir) in std::mem::take(&mut pending.enters) {
// Skip an enter already fired this pass (dedup key includes the direction).
if !called.insert((id, "enter".to_string(), format!("{dir:?}"))) {
continue;
}
let actions = self.scripts.run_enter(id, dir);
next.merge(self.apply_actions(actions));
}
for (id, fn_name, arg) in std::mem::take(&mut pending.sends) {
// Dedup key: target + function name + argument.
if !called.insert((id, fn_name.clone(), format!("{arg:?}"))) {
@@ -560,6 +630,9 @@ impl GameState {
let bumped;
let grabbed;
let portal_target;
// Non-solid objects the player (or the crates it shoved) landed on this move,
// collected while the board is borrowed and fired as `enter` after the borrow.
let mut entered: Vec<ObjectId> = Vec::new();
{
let (dx, dy): (i64, i64) = dir.into();
let mut board = self.board_mut();
@@ -583,10 +656,15 @@ impl GameState {
// Don't push a grab thing aside — walk onto it. Otherwise shove any
// pushable chain out of the way (no-op when there's nothing to push).
if grabbed.is_none() {
board.push(nx, ny, dir);
// Each pushed solid lands on a cell that may hold a non-solid.
for (cx, cy) in board.push(nx, ny, dir) {
entered.extend(board.non_solid_object_ids_at(cx, cy));
}
}
board.player.x = nx as i64;
board.player.y = ny as i64;
// The player is solid, so any non-solid on its new cell gets `enter`.
entered.extend(board.non_solid_object_ids_at(nx, ny));
// Check for a portal at the new position; clone strings to release the borrow.
portal_target = board
.portals
@@ -597,7 +675,7 @@ impl GameState {
portal_target = None;
}
}
// A portal takes priority: board transitions skip the bump hook.
// A portal takes priority: board transitions skip the bump/enter hooks.
if let Some((target_map, target_entry)) = portal_target {
self.enter_board(&target_map, &target_entry);
return;
@@ -613,6 +691,10 @@ impl GameState {
// The player advanced in `dir`, so the bump arrives from the opposite side.
ev.bumps.push((idx, dir.opposite()));
}
for id in entered {
// The player advanced in `dir`, so it entered from the opposite side.
ev.enters.push((id, dir.opposite()));
}
// Settle the grab/bump reactions (and any they cascade into) before returning.
let mut called = CalledSet::new();
self.settle(ev, &mut called);
@@ -620,33 +702,60 @@ impl GameState {
}
}
/// Moves object `id` one cell in `dir` on `board`, returning the object it bumped
/// (if any) for the caller to resolve after the board borrow drops.
/// What a [`step_object`] call produced, for the caller to resolve after the board
/// borrow drops.
struct StepOutcome {
/// The solid object the move pressed into (directly or at a crate-chain's end),
/// which receives a `bump`.
bumped: Option<ObjectId>,
/// Non-solid objects a solid landed on this move — the mover itself (only if it
/// is solid) and every crate it shoved — each of which receives an `enter`.
entered: Vec<ObjectId>,
}
/// Moves object `id` one cell in `dir` on `board`, reporting the `bump`/`enter`
/// reactions for the caller to resolve after the board borrow drops.
///
/// The move itself proceeds only if the target is in bounds and either passable or a
/// pushable solid the object can shove out of the way (see [`Board::can_push`]). The
/// bump is recorded for the solid object the move presses into — directly, or at the
/// end of a chain of crates being shoved (see [`Board::bump_target`]) — whether it
/// gets pushed aside or blocks the move. Walls and crates carry no script, so only
/// solid objects yield a bump.
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<ObjectId> {
/// solid objects yield a bump. An `enter` is recorded for every non-solid object a
/// solid lands on: the mover's destination cell (only when the mover is itself solid)
/// and each cell a pushed crate moved into (crates are always solid entrants).
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> StepOutcome {
let mut out = StepOutcome {
bumped: None,
entered: Vec::new(),
};
let (dx, dy): (i64, i64) = dir.into();
let (ox, oy) = board.objects.get(&id).map(|o| (o.x, o.y))?;
let Some((ox, oy, solid)) = board.objects.get(&id).map(|o| (o.x, o.y, o.solid)) else {
return out;
};
let target = (ox as i64 + dx, oy as i64 + dy);
if !board.in_bounds(target) {
return None;
return out;
}
let (nx, ny) = (target.0 as usize, target.1 as usize);
// Capture the bumped object before any push relocates it (its id is stable).
// Walks through a pushed crate chain to the object it presses against.
let bumped = board.bump_target(nx, ny, dir);
out.bumped = board.bump_target(nx, ny, dir);
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
board.push(nx, ny, dir); // shoves a crate/object out of the way; no-op otherwise
// Shove a crate/object out of the way (no-op otherwise); each pushed solid
// may land on a non-solid, which gets `enter` regardless of the mover.
for (cx, cy) in board.push(nx, ny, dir) {
out.entered.extend(board.non_solid_object_ids_at(cx, cy));
}
let obj = board.objects.get_mut(&id).expect("id checked above");
obj.x = nx;
obj.y = ny;
// Only a solid mover triggers `enter` on non-solids under its own new cell.
if solid {
out.entered.extend(board.non_solid_object_ids_at(nx, ny));
}
}
bumped
out
}
#[cfg(test)]