made grab simpler
This commit is contained in:
+22
-100
@@ -2,7 +2,7 @@ use crate::action::Action;
|
||||
use crate::board::Board;
|
||||
use crate::log::LogLine;
|
||||
use crate::script::ScriptHost;
|
||||
use crate::utils::{Direction, ObjectId, Player, ScriptArg, Solid};
|
||||
use crate::utils::{Direction, ObjectId, Player, ScriptArg};
|
||||
use crate::world::World;
|
||||
use std::cell::{Ref, RefMut};
|
||||
use std::time::Duration;
|
||||
@@ -210,9 +210,6 @@ impl GameState {
|
||||
// below also borrows `self`.
|
||||
let mut logs: Vec<LogLine> = Vec::new();
|
||||
let mut bumps: Vec<(ObjectId, i64)> = Vec::new();
|
||||
// Grab hooks to fire (player walked into / pushed a grab thing); fired after
|
||||
// the board borrow drops, like bumps.
|
||||
let mut grabs: Vec<ObjectId> = Vec::new();
|
||||
// Net change to the player's gem count from AddGems actions; applied to
|
||||
// `self` after the board borrow drops.
|
||||
let mut gem_delta: i64 = 0;
|
||||
@@ -225,13 +222,9 @@ impl GameState {
|
||||
for ba in actions {
|
||||
match ba.action {
|
||||
Action::Move(dir) => {
|
||||
let outcome = step_object(&mut board, ba.source, dir);
|
||||
if let Some(bumped) = outcome.bumped {
|
||||
if let Some(bumped) = step_object(&mut board, ba.source, dir) {
|
||||
bumps.push((bumped, ba.source as i64));
|
||||
}
|
||||
if let Some(grabbed) = outcome.grabbed {
|
||||
grabs.push(grabbed);
|
||||
}
|
||||
}
|
||||
Action::SetTile(tile) => {
|
||||
if let Some(obj) = board.objects.get_mut(&ba.source) {
|
||||
@@ -311,23 +304,14 @@ impl GameState {
|
||||
Action::Push { x, y, dir } => {
|
||||
if !board.in_bounds((x, y)) {
|
||||
logs.push(LogLine::error(format!("push({x},{y}): out of bounds")));
|
||||
} else if let Some(grabbed) =
|
||||
board.pushed_grab_into_player(x as usize, y as usize, dir)
|
||||
{
|
||||
// A grab thing shoved into the player is grabbed, not pushed.
|
||||
grabs.push(grabbed);
|
||||
} else {
|
||||
board.push(x as usize, y as usize, dir);
|
||||
}
|
||||
}
|
||||
// apply_swap reads all sources then writes all destinations, so a
|
||||
// whole batch of moves resolves simultaneously (cycles included).
|
||||
// A grab thing swapped onto the player is reported back for its
|
||||
// grab() hook (fired in phase B with the other grabs).
|
||||
Action::Swap(pairs) => {
|
||||
let (errs, grabbed) = board.apply_swap(&pairs);
|
||||
logs.extend(errs);
|
||||
grabs.extend(grabbed);
|
||||
logs.extend(board.apply_swap(&pairs));
|
||||
}
|
||||
// Accumulated and applied to `self.player_gems` after the borrow drops.
|
||||
Action::AddGems(n) => gem_delta += n,
|
||||
@@ -355,11 +339,6 @@ impl GameState {
|
||||
for (bumped, bumper) in bumps {
|
||||
self.scripts.run_bump(bumped, bumper);
|
||||
}
|
||||
// Fire grab() hooks (player walked into / pushed a grab thing). The hook
|
||||
// typically adds a stat and calls die(); those actions resolve next pass.
|
||||
for grabbed in grabs {
|
||||
self.scripts.run_grab(grabbed);
|
||||
}
|
||||
for (target, fn_name, arg) in sends {
|
||||
self.scripts.run_send(target, &fn_name, arg);
|
||||
}
|
||||
@@ -454,10 +433,10 @@ impl GameState {
|
||||
grabbed = board.grab_object_at(nx, ny);
|
||||
// A solid object in the way is bumped by the player (id -1) — but a
|
||||
// grab thing fires grab() instead of bump(), so don't also bump it.
|
||||
bumped = match board.solid_at(nx, ny) {
|
||||
Some(Solid::Object(_)) if grabbed.is_none() => board.solid_object_id_at(nx, ny),
|
||||
_ => None,
|
||||
};
|
||||
bumped = board
|
||||
.solid_at(nx, ny)
|
||||
.filter(|_| grabbed.is_none())
|
||||
.and_then(|s| s.object_id());
|
||||
if grabbed.is_some() || board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
|
||||
// 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).
|
||||
@@ -494,61 +473,31 @@ impl GameState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The side effects of a [`step_object`] call that the caller resolves after the
|
||||
/// board borrow drops: a `bump`ed object and/or a `grab`bed object.
|
||||
#[derive(Default)]
|
||||
struct StepOutcome {
|
||||
/// A solid object the mover collided with (target cell was occupied by it).
|
||||
bumped: Option<ObjectId>,
|
||||
/// A grab object the move shoved into the player (its `grab()` hook should fire).
|
||||
grabbed: Option<ObjectId>,
|
||||
}
|
||||
|
||||
/// Moves object `id` one cell in `dir` on `board`, returning the objects it bumped
|
||||
/// and/or grabbed (see [`StepOutcome`]).
|
||||
/// 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.
|
||||
///
|
||||
/// 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 whenever a solid object occupies the target — whether it gets
|
||||
/// pushed aside or blocks the move — since something tried to move into it. Walls and
|
||||
/// crates carry no script, so only solid objects yield a bump.
|
||||
///
|
||||
/// If the push would shove a **grab** thing into the player (see
|
||||
/// [`Board::pushed_grab_into_player`]), the push is skipped and the grab thing is
|
||||
/// reported instead of sliding the player along.
|
||||
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> StepOutcome {
|
||||
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<ObjectId> {
|
||||
let (dx, dy): (i32, i32) = dir.into();
|
||||
let Some((ox, oy)) = board.objects.get(&id).map(|o| (o.x, o.y)) else {
|
||||
return StepOutcome::default();
|
||||
};
|
||||
let (ox, oy) = board.objects.get(&id).map(|o| (o.x, o.y))?;
|
||||
let target = (ox as i32 + dx, oy as i32 + dy);
|
||||
if !board.in_bounds(target) {
|
||||
return StepOutcome::default();
|
||||
return None;
|
||||
}
|
||||
let (nx, ny) = (target.0 as usize, target.1 as usize);
|
||||
// A push that would drive a grab thing into the player grabs it instead of
|
||||
// sliding the player; skip the move entirely (the grab thing despawns itself).
|
||||
if let Some(grabbed) = board.pushed_grab_into_player(nx, ny, dir) {
|
||||
return StepOutcome {
|
||||
bumped: None,
|
||||
grabbed: Some(grabbed),
|
||||
};
|
||||
}
|
||||
// Capture the bumped object before any push relocates it (its id is stable).
|
||||
let bumped = match board.solid_at(nx, ny) {
|
||||
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
|
||||
_ => None,
|
||||
};
|
||||
let bumped = board.solid_at(nx, ny).and_then(|s| s.object_id());
|
||||
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
|
||||
let obj = board.objects.get_mut(&id).expect("id checked above");
|
||||
obj.x = nx;
|
||||
obj.y = ny;
|
||||
}
|
||||
StepOutcome {
|
||||
bumped,
|
||||
grabbed: None,
|
||||
}
|
||||
bumped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -580,9 +529,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pushing_a_gem_into_the_player_grabs_it() {
|
||||
// A non-solid script object at (0,0) pushes the gem at (1,0) east into the
|
||||
// player at (2,0); the gem is grabbed rather than sliding the player.
|
||||
fn pushing_a_gem_into_the_player_does_not_grab() {
|
||||
// A non-solid script object at (0,0) pushes the gem at (1,0) east toward the
|
||||
// player at (2,0). Grab now fires only on player movement, so the gem is an
|
||||
// ordinary solid here: the chain can't move (the player is backed against
|
||||
// the board edge), so nothing happens and the gem is not collected.
|
||||
let mut sobj = ObjectDef::new(0, 0);
|
||||
sobj.solid = false;
|
||||
sobj.script_name = Some("s".to_string());
|
||||
@@ -596,41 +547,12 @@ mod tests {
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
game.run_init();
|
||||
|
||||
// First tick: the push resolves into a grab (gem's grab() is queued).
|
||||
// Second tick: the gem's add_gems()/die() resolve.
|
||||
game.tick(Duration::from_millis(16));
|
||||
game.tick(Duration::from_millis(16));
|
||||
|
||||
assert_eq!(game.player_gems, 1);
|
||||
// The gem (the only non-script object) is gone; the player never moved.
|
||||
assert!(!game.board().objects.values().any(|o| o.grab));
|
||||
assert_eq!((game.board().player.x, game.board().player.y), (2, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swapping_a_gem_into_the_player_grabs_it() {
|
||||
// A non-solid script object swaps the gem at (1,0) onto the player at (2,0).
|
||||
// apply_swap reports the grab; the gem's grab() collects it and die()s.
|
||||
let mut sobj = ObjectDef::new(0, 0);
|
||||
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);
|
||||
board.expand_builtin_archetypes();
|
||||
let scripts = HashMap::from([(
|
||||
"s".to_string(),
|
||||
"fn tick(dt) { if Queue.length() == 0 { swap([[1, 0, 2, 0]]); } }".to_string(),
|
||||
)]);
|
||||
let mut game = GameState::with_scripts(board, scripts);
|
||||
game.run_init();
|
||||
|
||||
// First tick: the swap reports the grab (gem's grab() is queued).
|
||||
// Second tick: the gem's add_gems()/die() resolve.
|
||||
game.tick(Duration::from_millis(16));
|
||||
game.tick(Duration::from_millis(16));
|
||||
|
||||
assert_eq!(game.player_gems, 1);
|
||||
assert!(!game.board().objects.values().any(|o| o.grab));
|
||||
// No grab: the gem is untouched and the player never moved.
|
||||
assert_eq!(game.player_gems, 0);
|
||||
assert!(game.board().objects.values().any(|o| o.grab));
|
||||
assert_eq!((game.board().player.x, game.board().player.y), (2, 0));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user