This commit is contained in:
2026-08-10 22:13:21 -05:00
parent 30e09a40c4
commit a8444b25ad
11 changed files with 330 additions and 155 deletions
+95 -75
View File
@@ -2,10 +2,11 @@ use crate::action::{apply_push, apply_shift, apply_teleport, Action, BoardAction
use crate::board::Board;
use crate::log::LogLine;
use crate::script::ScriptHost;
use crate::utils::{Direction, ObjectId};
use crate::utils::{Direction, ObjectId, Point, Pushable};
use crate::world::World;
use std::cell::{Ref, RefMut};
use std::collections::{BTreeSet, HashSet, VecDeque};
use std::fmt::format;
use std::hash::Hash;
use std::time::Duration;
@@ -295,7 +296,8 @@ impl GameState {
apply_teleport(&mut self.board_mut(), target, x, y).unwrap_or_else(|e| log_sink.error(e))
}
Action::Push { x, y, dir } => {
apply_push(&mut self.board_mut(), x, y, dir).unwrap_or_else(|e| log_sink.error(e))
self.resolve_move((x as usize, y as usize), dir);
//apply_push(&mut self.board_mut(), x, y, dir).unwrap_or_else(|e| log_sink.error(e))
}
Action::Shift(cells) => {
apply_shift(&mut self.board_mut(), &cells).unwrap_or_else(|e| log_sink.error(e))
@@ -385,6 +387,96 @@ impl GameState {
self.run_init();
}
fn resolve_move(&mut self, from: (usize, usize), dir: Direction) -> bool {
// Get the target coords, if they're out of bounds then the move fails.
let target: Point = dir.from_point(from.0 as i64, from.1 as i64).into();
if !self.board().in_bounds((target.x, target.y)) { return false; }
if self.board().is_empty(target) {
// If it's empty, we can move in there; do so and return true:
self.board_mut().move_cell(from.into(), target);
} else if self.board().is_player(target) {
// Target cell contains player, who's pushable, so we'll recurse and see what happens:
if self.resolve_move((target.x as usize, target.y as usize), dir) {
self.board_mut().move_cell(from.into(), target);
}
} else {
// Otherwise, what enter response does the thing there have? We need to be able to call
// hooks on objects, so we can't hold a mut reference to the board going into this match
let id = {
if let Some(Tile::Object(b)) = self.board_mut().get(target.x as usize, target.y as usize) && let def = b.as_ref() {
def.scripting.id
} else {
unreachable!("moving into the player was handled above")
}
};
// Whether the player is what initiated the move directly. Matters for grab / swap
// let from_player = matches!(self.board().get(from.0, from.1), Some(Tile::Player));
// Call the target's bump hook and resolve whatever it did
let actions = self.scripts.run_bump(id, dir.opposite());
self.apply_actions(actions);
// First do things to try and call any hooks relevant:
// let actions = match resp {
// // We block all moves, return false
// EnterResponse::Block => { self.scripts.run_bump(id, dir.opposite()) }
//
// // The player can grab things directly, so let's do that
// EnterResponse::Grab if from_player => {
// let a = self.scripts.run_grab(id);
// self.board_mut().get_mut(target.0, target.1).take();
// a
// }
//
// // Grabbables not from the player act as pushable, we need to recurse
// EnterResponse::Grab => { self.resolve_move(target, dir); vec![] }
//
// // Pushable if we're allowed to push that way recurses
// EnterResponse::Push(p) if p.allows(dir) => { self.resolve_move(target, dir); vec![] }
//
// // Pushable in a disallowed direction blocks
// EnterResponse::Push(p) => { self.scripts.run_bump(id, dir.opposite()) }
//
// // Swaps let the player swap places, but we should return false afterward because we
// // haven't left the source cell empty. In practice this won't matter because right
// // now we never recurse _into_ a player-source-cell, all moves are initiated by the
// // player, but still.
// EnterResponse::Swap if from_player => {
// let mut board = self.board_mut();
// let mover = board.get_mut(from.0, from.1).take();
// let swapper = board.get_mut(target.0, target.1).take();
// board.get_mut(target.0, target.1).replace(mover.unwrap());
// board.get_mut(from.0, from.1).replace(swapper.unwrap());
// vec![]
// }
//
// // Swaps in a chain are just normal pushable, recurse:
// EnterResponse::Swap => { self.resolve_move(target, dir); vec![] }
//
// // Squish just lets the mover overwrite, and always leaves the source cell empty:
// EnterResponse::Squish => { self.board_mut().get_mut(from.0, from.1).take(); vec![] }
//
// // Finally, hook, we need to call a hook and let it do its thing:
// EnterResponse::Hook => { self.scripts.run_bump(id, dir.opposite()) }
// };
// If there were actions performed by the hooks, do them.
// self.apply_actions(actions);
// Now, check again if the target cell is empty. If it is, put the mover there. Also
// check that the source cell still contains something! It may have been teleported away.
if !self.board().is_empty(from.into()) && self.board().is_empty(target) {
self.board_mut().move_cell(from.into(), target);
}
}
// Either way, return whether the source cell is now empty, so calls up the chain
// can move into it (push chains)
self.board().is_empty(from.into())
}
/// Attempts to move the player one cell in `dir`.
///
/// The move is ignored if the target cell is out of bounds, or it is neither
@@ -399,80 +491,8 @@ impl GameState {
if !self.board().in_bounds(target) {
return;
}
let (nx, ny) = (target.0 as usize, target.1 as usize);
// We need to be able to call hooks on objects, so we can't hold a mut reference to the
// board going into this match
let obj_data = {
if let Some(Tile::Object(b)) = self.board_mut().get(nx, ny) && let def = b.as_ref() {
Some((def.scripting.id, def.enter_response))
} else {
None
}
};
if let Some((id, enter_response)) = obj_data {
let actions = match enter_response {
EnterResponse::Block => {
// Call the bump hook
self.scripts.run_bump(id, dir.opposite())
}
EnterResponse::Grab => {
// Call the hook to get the actions and then stamp the player on top
let a = self.scripts.run_grab(id);
{
let mut board = self.board_mut();
board.get_mut(player_loc.0, player_loc.1).take();
board.get_mut(nx, ny).replace(Tile::Player);
}
a
}
EnterResponse::Push(pushable) => {
// First, can we push?
if self.board().can_push(nx, ny, dir) {
// The player is pushable, so, this amounts to the same thing and saves a
// couple replace()s
self.board_mut().push(player_loc.0, player_loc.1, dir);
vec![] // There's no push hook, just pushing doesn't call scripts
} else {
// This is actually not pushable in this way, so we're gonna bump instead:
self.scripts.run_bump(id, dir.opposite())
}
}
EnterResponse::Hook => {
vec![] // TODO this hook needs to exist and work. It's documented in tile.rb. Has implications for push as well
// plan: get rid of can_push in board. Make a board::pushes_into_hook or something, find the hook-enter
// object at the end of this chain. Trying to push calls that, if there's no hook object then it pushes, if
// that returns false then it bumps. If there is a hook object, call the hook, run the actions. If it _leaves
// the cell empty,_ then call push. Otherwise bump.
// Maybe have a pushresult enum or something that board::push returns, "hook(id, bumpid)" or "bump(id)" or "moved".
// If the situation is: `@b++h` then moving to the east, the bumpid would be b (the thing you actually touched),
// hook id would be h (the thing with the hook enterresponse). Board can identify object chains but not actually
// call hooks.
}
EnterResponse::Swap => {
let mut board = self.board_mut();
// Swap the two
let tgt = board.get_mut(nx, ny).replace(Tile::Player);
*board.get_mut(player_loc.0, player_loc.1) = tgt;
vec![]
}
EnterResponse::Squish => {
let mut board = self.board_mut();
// Stamp over it, squishing it
board.get_mut(player_loc.0, player_loc.1).take();
board.get_mut(nx, ny).replace(Tile::Player);
vec![]
}
};
self.apply_actions(actions);
} else {
// There's not an object there, we can just move the player
let mut board = self.board_mut();
board.get_mut(player_loc.0, player_loc.1).take();
board.get_mut(nx, ny).replace(Tile::Player);
}
self.resolve_move(player_loc, dir);
// Check if we actually moved
let new_loc = self.board().player_pos();