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
+52 -8
View File
@@ -7,6 +7,19 @@ use crate::utils::Direction;
use crate::utils::{Behavior, ObjectId, PlayerPos, PortalDef, Pushable, RegistryValue, Solid};
use std::collections::{BTreeMap, HashMap, HashSet};
/// The result of [`Board::apply_shift`]: any error lines to log, plus the
/// `(from, to)` cell relocations the shift actually performed.
///
/// The `moves` let the caller fire an `enter` hook on any non-solid object each
/// shifted solid landed on; the direction is derived best-effort from `to - from`
/// (a shift can rotate cells that aren't cardinally adjacent).
pub struct ShiftOutcome {
/// Error lines (e.g. an out-of-bounds cell) for the caller to log.
pub errors: Vec<LogLine>,
/// Each `(from, to)` relocation a non-blocked solid underwent.
pub moves: Vec<((i64, i64), (i64, i64))>,
}
/// A non-solid `(glyph, archetype)` placed at a board coordinate, **outside** the
/// main grid, drawn only when the grid cell at `(x, y)` is empty.
///
@@ -373,9 +386,12 @@ impl Board {
///
/// No-op when the chain can't move (it self-checks via [`can_push`](Board::can_push)),
/// so it is safe to call unconditionally.
pub fn push(&mut self, x: usize, y: usize, dir: Direction) {
/// Returns the cells the shoved solids moved **into** (each chain cell stepped
/// one cell in `dir`), so the caller can fire `enter` on any non-solid object a
/// pushed solid landed on. Empty when nothing moved.
pub fn push(&mut self, x: usize, y: usize, dir: Direction) -> Vec<(usize, usize)> {
if !self.can_push(x, y, dir) {
return;
return Vec::new();
}
let (dx, dy): (i64, i64) = dir.into();
// can_push guaranteed the chain ends at an in-bounds passable cell, so
@@ -391,6 +407,12 @@ impl Board {
for &(px, py) in chain.iter().rev() {
self.shift_solid(px, py, dx, dy);
}
// Each solid ended up one step along `dir`; those destination cells are
// where an `enter` may need to fire.
chain
.iter()
.map(|&(px, py)| ((px as i64 + dx) as usize, (py as i64 + dy) as usize))
.collect()
}
/// Moves the single solid occupant of `(x, y)` one step by `(dx, dy)`.
@@ -421,6 +443,19 @@ impl Board {
.collect()
}
/// Returns the [`ObjectId`]s of the **non-solid** objects at `(x, y)`.
///
/// These are the targets of an `enter` hook when a solid relocates onto the
/// cell (terrain is always solid, so only objects can be non-solid). Mirrors
/// [`object_ids_at`](Board::object_ids_at) / [`solid_object_id_at`](Board::solid_object_id_at).
pub fn non_solid_object_ids_at(&self, x: usize, y: usize) -> Vec<ObjectId> {
self.objects
.iter()
.filter(|(_, o)| o.x == x && o.y == y && !o.solid)
.map(|(&id, _)| id)
.collect()
}
/// Returns a borrow of the actual object at `(x, y)` if any
pub fn solid_object_id_at(&self, x: usize, y: usize) -> Option<ObjectId> {
self.objects.iter().find_map(|(&id, o)| {
@@ -552,11 +587,16 @@ impl Board {
}
/// Shifts a set of cells, given as `(x, y)` coordinates. Backs the script
/// `shift()` fn. Returns any errors as [`LogLine`]s for the caller to log.
pub fn apply_shift(&mut self, cells: &[(i64, i64)]) -> Vec<LogLine> {
/// `shift()` fn. Returns a [`ShiftOutcome`] carrying any error [`LogLine`]s for
/// the caller to log plus the `(from, to)` relocations it performed (so the
/// caller can fire `enter` on non-solids each moved solid landed on).
pub fn apply_shift(&mut self, cells: &[(i64, i64)]) -> ShiftOutcome {
// Validate all the cells are in bounds, error if not:
if cells.iter().any(|&c| !self.in_bounds(c)) {
return vec![LogLine::error("Called shift() with a cell out of bounds")]
return ShiftOutcome {
errors: vec![LogLine::error("Called shift() with a cell out of bounds")],
moves: Vec::new(),
};
}
// Get all the Solids at these cells:
@@ -609,15 +649,19 @@ impl Board {
}
}
// Now, move anything that we've decided is not blocked:
// Now, move anything that we've decided is not blocked, recording each
// relocation so the caller can fire `enter` at every destination.
let mut moves = Vec::new();
for (curr_idx, curr) in solids.iter().enumerate() {
if let Some(solid) = curr && !blocked.contains(&curr_idx) {
let origin = cells[curr_idx];
let target = cells[(curr_idx + 1) % cells.len()];
solid.place(self, target.0 as usize, target.1 as usize);
moves.push((origin, target));
}
}
vec![]
ShiftOutcome { errors: Vec::new(), moves }
}
/// Clear the queues of all objects on this board: called when entering a board, objects
@@ -975,7 +1019,7 @@ pub(crate) mod tests {
let mut board = open_board(3, 1, (2, 0), vec![]);
crate_at(&mut board, 0, 0);
let errs = board.apply_shift(&[(0, 0), (9, 0)]);
assert_eq!(errs.len(), 1);
assert_eq!(errs.errors.len(), 1);
assert_eq!(board.get(0, 0).1, Archetype::Crate); // unchanged
}