Real object ids, fixed warping bug
This commit is contained in:
+98
-52
@@ -3,7 +3,7 @@ use crate::script::{Action, Direction, ScriptHost};
|
||||
use color::Rgba8;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::{Ref, RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
@@ -280,6 +280,15 @@ pub const ALL_ARCHETYPES: &[Archetype] = &[
|
||||
Archetype::VCrate,
|
||||
];
|
||||
|
||||
/// A stable, unique identifier for a board object.
|
||||
///
|
||||
/// Ids are handed out by [`Board::add_object`] from the per-board
|
||||
/// [`Board::next_object_id`] counter (starting at 1) and never reused, so a
|
||||
/// reference to an object stays valid even as other objects are added or removed.
|
||||
/// This replaces the old "index into `Board::objects`" scheme, which shifted when
|
||||
/// objects were reordered/destroyed.
|
||||
pub type ObjectId = u32;
|
||||
|
||||
/// A scripted object placed on the board, loaded from a map file.
|
||||
///
|
||||
/// `ObjectDef` represents a tile that has Rhai script attached to it.
|
||||
@@ -440,8 +449,14 @@ pub struct Board {
|
||||
pub(crate) floor_spec: Option<crate::floor::FloorSpec>,
|
||||
/// Current player position. See [`Player`] for caveats about its future.
|
||||
pub player: Player,
|
||||
/// Scripted objects on this board. Parsed from the map file; not yet active.
|
||||
pub objects: Vec<ObjectDef>,
|
||||
/// Scripted objects on this board, keyed by stable [`ObjectId`]. A `BTreeMap`
|
||||
/// (not a `Vec`) so an object can be removed without invalidating other
|
||||
/// objects' ids; iteration is in ascending-id order, which equals load order
|
||||
/// (ids are assigned sequentially as the map loads).
|
||||
pub objects: BTreeMap<ObjectId, ObjectDef>,
|
||||
/// The next [`ObjectId`] to hand out (starts at 1, monotonically increasing).
|
||||
/// See [`Board::add_object`].
|
||||
pub next_object_id: ObjectId,
|
||||
/// Portals on this board. Parsed from the map file; not yet active.
|
||||
pub portals: Vec<PortalDef>,
|
||||
/// Optional font override for this board. When `None`, the app default is used.
|
||||
@@ -545,8 +560,7 @@ impl Board {
|
||||
return Some(Solid::Player);
|
||||
}
|
||||
// A solid object shadows the grid cell it sits on.
|
||||
if let Some(obj) = self.object_at(x, y)
|
||||
&& obj.solid
|
||||
if let Some(obj) = self.solid_object_at(x, y)
|
||||
{
|
||||
return Some(Solid::Object(obj));
|
||||
}
|
||||
@@ -645,11 +659,11 @@ impl Board {
|
||||
if self.player.x == x as i32 && self.player.y == y as i32 {
|
||||
self.player.x = tx as i32;
|
||||
self.player.y = ty as i32;
|
||||
} else if let Some(idx) = self.object_index_at(x, y)
|
||||
&& self.objects[idx].solid
|
||||
} else if let Some(id) = self.solid_object_id_at(x, y)
|
||||
{
|
||||
self.objects[idx].x = tx;
|
||||
self.objects[idx].y = ty;
|
||||
let obj = self.objects.get_mut(&id).expect("id from object_id_at");
|
||||
obj.x = tx;
|
||||
obj.y = ty;
|
||||
} else {
|
||||
let moved = *self.get(x, y);
|
||||
*self.get_mut(tx, ty) = moved;
|
||||
@@ -657,14 +671,36 @@ impl Board {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the index into [`Board::objects`] of the object at `(x, y)`, if any.
|
||||
pub fn object_index_at(&self, x: usize, y: usize) -> Option<usize> {
|
||||
self.objects.iter().position(|o| o.x == x && o.y == y)
|
||||
/// Returns the [`ObjectId`]s of the objects at `(x, y)`, if any.
|
||||
pub fn object_ids_at(&self, x: usize, y: usize) -> Vec<ObjectId> {
|
||||
self.objects
|
||||
.iter()
|
||||
.filter(|(_, o)| o.x == x && o.y == y)
|
||||
.map(|(&id, _)| id).collect()
|
||||
}
|
||||
|
||||
/// Returns a borrow of the actual object at `(x, y)` if any
|
||||
pub fn object_at(&self, x: usize, y: usize) -> Option<&ObjectDef> {
|
||||
self.object_index_at(x, y).map(|idx| &self.objects[idx])
|
||||
pub fn solid_object_at(&self, x: usize, y: usize) -> Option<&ObjectDef> {
|
||||
self.objects.values().find(|o| o.x == x && o.y == y && o.solid)
|
||||
}
|
||||
|
||||
/// 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(|(id, o)| o.x == x && o.y == y && o.solid)
|
||||
.map(|(&id, _)| id)
|
||||
}
|
||||
|
||||
/// Inserts `object`, assigning it the next free [`ObjectId`], and returns that id.
|
||||
///
|
||||
/// Ids start at 1 and increase monotonically; an id is never reused, so it
|
||||
/// stays a valid handle to this object for the board's lifetime.
|
||||
pub fn add_object(&mut self, object: ObjectDef) -> ObjectId {
|
||||
let id = self.next_object_id;
|
||||
self.next_object_id += 1;
|
||||
self.objects.insert(id, object);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -759,7 +795,7 @@ impl GameState {
|
||||
// Logs are collected here rather than pushed inline, since the board borrow
|
||||
// below also borrows `self`.
|
||||
let mut logs: Vec<LogLine> = Vec::new();
|
||||
let mut bumps: Vec<(usize, i64)> = Vec::new();
|
||||
let mut bumps: Vec<(ObjectId, i64)> = Vec::new();
|
||||
{
|
||||
let mut board = self.board_mut();
|
||||
for ba in actions {
|
||||
@@ -770,7 +806,7 @@ impl GameState {
|
||||
}
|
||||
}
|
||||
Action::SetTile(tile) => {
|
||||
if let Some(obj) = board.objects.get_mut(ba.source) {
|
||||
if let Some(obj) = board.objects.get_mut(&ba.source) {
|
||||
obj.glyph.tile = tile;
|
||||
}
|
||||
}
|
||||
@@ -803,7 +839,7 @@ impl GameState {
|
||||
let (nx, ny) = (target.0 as usize, target.1 as usize);
|
||||
// A solid object in the way is bumped by the player (id -1).
|
||||
bumped = match board.solid_at(nx, ny) {
|
||||
Some(Solid::Object(_)) => board.object_index_at(nx, ny),
|
||||
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
|
||||
_ => None,
|
||||
};
|
||||
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
|
||||
@@ -819,30 +855,30 @@ impl GameState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves object `idx` one cell in `dir` on `board`, returning the index of a solid
|
||||
/// object it bumped (its target cell was occupied by that object), if any.
|
||||
/// Moves object `id` one cell in `dir` on `board`, returning the [`ObjectId`] of a
|
||||
/// solid object it bumped (its target cell was occupied by that object), if any.
|
||||
///
|
||||
/// 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.
|
||||
fn step_object(board: &mut Board, idx: usize, dir: Direction) -> Option<usize> {
|
||||
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<ObjectId> {
|
||||
let (dx, dy): (i32, i32) = dir.into();
|
||||
let (ox, oy) = board.objects.get(idx).map(|o| (o.x, o.y))?;
|
||||
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 None;
|
||||
}
|
||||
let (nx, ny) = (target.0 as usize, target.1 as usize);
|
||||
// Capture the bumped object before any push relocates it (its index is stable).
|
||||
// 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.object_index_at(nx, ny),
|
||||
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
|
||||
_ => None,
|
||||
};
|
||||
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 = &mut board.objects[idx];
|
||||
let obj = board.objects.get_mut(&id).expect("id checked above");
|
||||
obj.x = nx;
|
||||
obj.y = ny;
|
||||
}
|
||||
@@ -866,7 +902,8 @@ mod tests {
|
||||
floor: vec![Archetype::Empty.default_glyph()],
|
||||
floor_spec: None,
|
||||
player: Player { x: 0, y: 0 },
|
||||
objects: vec![object],
|
||||
objects: BTreeMap::from([(1, object)]),
|
||||
next_object_id: 2,
|
||||
portals: Vec::new(),
|
||||
font: None,
|
||||
zoom: 1,
|
||||
@@ -896,6 +933,14 @@ mod tests {
|
||||
objects: Vec<ObjectDef>,
|
||||
scripts: &[(&str, &str)],
|
||||
) -> Board {
|
||||
// Assign sequential ids (1..=n) just like the map loader, so tests that
|
||||
// index by id can rely on first-object == id 1.
|
||||
let mut object_map: BTreeMap<ObjectId, ObjectDef> = BTreeMap::new();
|
||||
let mut next_object_id: ObjectId = 1;
|
||||
for o in objects {
|
||||
object_map.insert(next_object_id, o);
|
||||
next_object_id += 1;
|
||||
}
|
||||
Board {
|
||||
name: "test".into(),
|
||||
width: w,
|
||||
@@ -907,7 +952,8 @@ mod tests {
|
||||
x: player.0,
|
||||
y: player.1,
|
||||
},
|
||||
objects,
|
||||
objects: object_map,
|
||||
next_object_id,
|
||||
portals: Vec::new(),
|
||||
font: None,
|
||||
zoom: 1,
|
||||
@@ -935,7 +981,7 @@ mod tests {
|
||||
let mut board = open_board(4, 1, (3, 0), vec![], &[]);
|
||||
board.cells[1] = (Archetype::Wall.default_glyph(), Archetype::Wall);
|
||||
// A solid object on the otherwise-empty cell (2, 0).
|
||||
board.objects.push(ObjectDef::new(2, 0)); // solid by default
|
||||
board.add_object(ObjectDef::new(2, 0)); // solid by default
|
||||
|
||||
// Empty floor: nothing solid.
|
||||
assert!(board.solid_at(0, 0).is_none());
|
||||
@@ -1078,7 +1124,7 @@ mod tests {
|
||||
game.try_move(Direction::East);
|
||||
let b = game.board();
|
||||
assert_eq!((b.player.x, b.player.y), (1, 0));
|
||||
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 0)); // object shoved east
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object shoved east
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1095,7 +1141,7 @@ mod tests {
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 0)); // object advanced
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object advanced
|
||||
assert_eq!(b.get(2, 0).1, Archetype::Empty); // crate left (2,0)
|
||||
assert_eq!(b.get(3, 0).1, Archetype::Crate); // crate pushed to (3,0)
|
||||
}
|
||||
@@ -1268,7 +1314,7 @@ mod tests {
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
// East increments x by one; the object started at (2, 1).
|
||||
assert_eq!((b.objects[0].x, b.objects[0].y), (3, 1));
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (3, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1283,7 +1329,7 @@ mod tests {
|
||||
);
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[0].x, 0);
|
||||
assert_eq!(game.board().objects[&1].x, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1297,7 +1343,7 @@ mod tests {
|
||||
);
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[0].glyph.tile, 7);
|
||||
assert_eq!(game.board().objects[&1].glyph.tile, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1316,7 +1362,7 @@ mod tests {
|
||||
"greeter init should log a greeting"
|
||||
);
|
||||
assert!(
|
||||
game.board().objects.iter().any(|o| o.glyph.tile == 2),
|
||||
game.board().objects.values().any(|o| o.glyph.tile == 2),
|
||||
"greeter set_tile(2) should change its glyph"
|
||||
);
|
||||
}
|
||||
@@ -1339,8 +1385,8 @@ mod tests {
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
assert_eq!(b.objects[0].x, 1); // moved east from 0
|
||||
assert_eq!(b.objects[1].x, 3); // moved west from 4
|
||||
assert_eq!(b.objects[&1].x, 1); // moved east from 0
|
||||
assert_eq!(b.objects[&2].x, 3); // moved west from 4
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1356,16 +1402,16 @@ mod tests {
|
||||
);
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[0].x, 2); // first move applied (1 -> 2)
|
||||
assert_eq!(game.board().objects[&1].x, 2); // first move applied (1 -> 2)
|
||||
|
||||
// 200 ms of ticks: still inside the cooldown, no further movement.
|
||||
game.tick(Duration::from_millis(100));
|
||||
game.tick(Duration::from_millis(100));
|
||||
assert_eq!(game.board().objects[0].x, 2);
|
||||
assert_eq!(game.board().objects[&1].x, 2);
|
||||
|
||||
// Crossing the 250 ms mark releases the queued second move.
|
||||
game.tick(Duration::from_millis(100));
|
||||
assert_eq!(game.board().objects[0].x, 3);
|
||||
assert_eq!(game.board().objects[&1].x, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1384,7 +1430,7 @@ mod tests {
|
||||
game.run_init();
|
||||
// First (eastward) move is blocked by the wall: object hasn't moved.
|
||||
assert_eq!(
|
||||
(game.board().objects[0].x, game.board().objects[0].y),
|
||||
(game.board().objects[&1].x, game.board().objects[&1].y),
|
||||
(1, 1)
|
||||
);
|
||||
|
||||
@@ -1392,14 +1438,14 @@ mod tests {
|
||||
game.tick(Duration::from_millis(100));
|
||||
game.tick(Duration::from_millis(100));
|
||||
assert_eq!(
|
||||
(game.board().objects[0].x, game.board().objects[0].y),
|
||||
(game.board().objects[&1].x, game.board().objects[&1].y),
|
||||
(1, 1)
|
||||
);
|
||||
|
||||
// Past 250 ms the queued South move resolves.
|
||||
game.tick(Duration::from_millis(100));
|
||||
assert_eq!(
|
||||
(game.board().objects[0].x, game.board().objects[0].y),
|
||||
(game.board().objects[&1].x, game.board().objects[&1].y),
|
||||
(1, 2)
|
||||
);
|
||||
}
|
||||
@@ -1421,7 +1467,7 @@ mod tests {
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
assert!(log_texts(&game).iter().any(|t| t == "len=2"));
|
||||
assert_eq!(game.board().objects[0].glyph.tile, 6); // last set_tile won
|
||||
assert_eq!(game.board().objects[&1].glyph.tile, 6); // last set_tile won
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1437,7 +1483,7 @@ mod tests {
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
game.tick(Duration::from_millis(300));
|
||||
assert_eq!(game.board().objects[0].x, 1); // never moved
|
||||
assert_eq!(game.board().objects[&1].x, 1); // never moved
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1456,7 +1502,7 @@ mod tests {
|
||||
wall_at(&mut board, 2, 0);
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[0].glyph.tile, 9);
|
||||
assert_eq!(game.board().objects[&1].glyph.tile, 9);
|
||||
|
||||
// Open ahead, nothing pending: blocked() is false.
|
||||
let board = open_board(
|
||||
@@ -1471,7 +1517,7 @@ mod tests {
|
||||
);
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
assert_eq!(game.board().objects[0].glyph.tile, 7);
|
||||
assert_eq!(game.board().objects[&1].glyph.tile, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1497,8 +1543,8 @@ mod tests {
|
||||
let mut game = GameState::new(board);
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 1)); // mover advanced
|
||||
assert_eq!(b.objects[1].glyph.tile, 1); // checker saw the pending move
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 1)); // mover advanced
|
||||
assert_eq!(b.objects[&2].glyph.tile, 1); // checker saw the pending move
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1528,11 +1574,11 @@ mod tests {
|
||||
game.tick(Duration::from_millis(300));
|
||||
{
|
||||
let b = game.board();
|
||||
assert_eq!((b.objects[0].x, b.objects[0].y), (1, 0)); // obj0 won the cell
|
||||
assert_eq!((b.objects[1].x, b.objects[1].y), (2, 0)); // obj1 blocked
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); // obj0 won the cell
|
||||
assert_eq!((b.objects[&2].x, b.objects[&2].y), (2, 0)); // obj1 blocked
|
||||
}
|
||||
let logs = log_texts(&game);
|
||||
assert!(logs.iter().any(|t| t == "o0 by 1")); // obj0 bumped by obj1
|
||||
assert!(logs.iter().any(|t| t == "o0 by 2")); // obj0 bumped by obj1
|
||||
assert!(!logs.iter().any(|t| t.starts_with("o1 by"))); // obj1 not bumped
|
||||
}
|
||||
|
||||
@@ -1663,7 +1709,7 @@ mod tests {
|
||||
game.run_init();
|
||||
{
|
||||
let b = game.board();
|
||||
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 0)); // object took player's cell
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object took player's cell
|
||||
assert_eq!((b.player.x, b.player.y), (3, 0)); // player shoved east
|
||||
}
|
||||
|
||||
@@ -1679,7 +1725,7 @@ mod tests {
|
||||
let mut game = GameState::new(board);
|
||||
game.run_init();
|
||||
let b = game.board();
|
||||
assert_eq!((b.objects[0].x, b.objects[0].y), (1, 0)); // object blocked
|
||||
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); // object blocked
|
||||
assert_eq!((b.player.x, b.player.y), (2, 0)); // player not pushed
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user