wip 2 pointifying
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
//! object that issued it).
|
||||
|
||||
use std::fmt::Debug;
|
||||
use crate::utils::{Direction, ObjectId};
|
||||
use crate::utils::{Direction, ObjectId, Point};
|
||||
use color::Rgba8;
|
||||
use rhai::Dynamic;
|
||||
use crate::Board;
|
||||
@@ -171,15 +171,15 @@ pub fn apply_teleport(board: &mut Board, target: i64, x: i64, y: i64) -> Result<
|
||||
let from = if target == -1 {
|
||||
Some(board.player_pos())
|
||||
} else if let Some(obj) = board.get_hookable(target as ObjectId) {
|
||||
Some(obj.location())
|
||||
Some(obj.location().into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(from) = from {
|
||||
// Check if we're blocked:
|
||||
if (from.0 != x || from.1 != y) && board.get((x, y)).is_none() {
|
||||
if (from.ux() != x || from.uy() != y) && board.get((x, y)).is_none() {
|
||||
// Not blocked, move it
|
||||
board.move_cell(from.into(), (x, y).into());
|
||||
board.move_cell(from, (x, y));
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -188,7 +188,7 @@ pub fn apply_teleport(board: &mut Board, target: i64, x: i64, y: i64) -> Result<
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_shift(board: &mut Board, cells: &[(i64, i64)]) -> Result<(), String> {
|
||||
pub fn apply_shift(board: &mut Board, cells: &[Point]) -> Result<(), String> {
|
||||
board.apply_shift(cells)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -8,7 +8,6 @@
|
||||
//!
|
||||
//! ## Cell queries
|
||||
//!
|
||||
//! - `board.can_push(x, y, dir) -> bool` — is the pushable chain at `(x, y)` shovable in `dir`
|
||||
//! - `board.passable(x, y) -> bool` — is `(x, y)` on-board and free of any solid (a hole)
|
||||
//! cell (one empty, or a grab thing and the player)
|
||||
|
||||
@@ -30,19 +29,12 @@ impl Registerable for BoardRef {
|
||||
engine.register_get("width", |b: &mut BoardRef| b.borrow().width as i64);
|
||||
engine.register_get("height", |b: &mut BoardRef| b.borrow().height as i64);
|
||||
|
||||
// can_push(x, y, dir) — true if (x, y) holds a pushable whose chain can be
|
||||
// shoved one step in dir (the read-only half of push()). False off-board.
|
||||
engine.register_fn("can_push", move |board: BoardRef, x: i64, y: i64, dir: Direction| -> bool {
|
||||
let board = board.borrow();
|
||||
board.in_bounds((x, y)) && board.can_push(x as usize, y as usize, dir)
|
||||
});
|
||||
|
||||
// passable(x, y) — true if (x, y) is on-board and holds no solid (an empty cell
|
||||
// a mover could enter). Off-board is not passable. Lets a script distinguish a
|
||||
// hole from a blocked solid (which can_shift/can_push alone cannot).
|
||||
engine.register_fn("passable", move |board: BoardRef, x: i64, y: i64| -> bool {
|
||||
let board = board.borrow();
|
||||
board.in_bounds((x, y)) && board.is_passable(x as usize, y as usize)
|
||||
board.in_bounds((x, y)) && board.is_empty((x, y))
|
||||
});
|
||||
|
||||
// Board.get(id) -> ObjectInfo | () (unknown id logs error)
|
||||
|
||||
@@ -29,7 +29,7 @@ use crate::action::BoardAction;
|
||||
use crate::object_def::ObjectDef;
|
||||
use crate::script::Registerable;
|
||||
use crate::tile::{Hookable, Optics, ScriptAttributes, ScriptKey, Tile};
|
||||
use crate::utils::{LogSink, ObjectId};
|
||||
use crate::utils::{LogSink, ObjectId, Point};
|
||||
|
||||
/// A snapshot of one board object, returned by `Board.tagged`, `Board.named`,
|
||||
/// and `Board.get`. Passed by value — scripts read fields, not a live reference.
|
||||
@@ -39,8 +39,7 @@ use crate::utils::{LogSink, ObjectId};
|
||||
#[derive(Clone)]
|
||||
pub struct ObjectInfo {
|
||||
pub id: ObjectId,
|
||||
pub x: i64,
|
||||
pub y: i64,
|
||||
pub location: Point,
|
||||
pub board: BoardRef,
|
||||
pub script_key: ScriptKey,
|
||||
pub queue: ObjQueue
|
||||
@@ -54,11 +53,9 @@ impl ObjectInfo {
|
||||
}
|
||||
|
||||
pub fn from_hookable(hookable: Box<dyn Hookable + '_>, board: BoardRef) -> ObjectInfo {
|
||||
let (x, y) = hookable.location();
|
||||
Self {
|
||||
id: hookable.id(),
|
||||
x: x as i64,
|
||||
y: y as i64,
|
||||
location: hookable.location(),
|
||||
board,
|
||||
script_key: hookable.scriptable().script_name.clone(),
|
||||
queue: hookable.scriptable().queue.clone(),
|
||||
@@ -75,8 +72,8 @@ impl ObjectInfo {
|
||||
impl Registerable for ObjectInfo {
|
||||
fn register(engine: &mut Engine, _log_sink: LogSink) {
|
||||
engine.register_type_with_name::<ObjectInfo>("ObjectInfo")
|
||||
.register_get("x", |obj: &mut ObjectInfo| obj.x)
|
||||
.register_get("y", |obj: &mut ObjectInfo| obj.y)
|
||||
.register_get("x", |obj: &mut ObjectInfo| obj.location.x)
|
||||
.register_get("y", |obj: &mut ObjectInfo| obj.location.y)
|
||||
.register_get("id", |obj: &mut ObjectInfo| obj.id as i64)
|
||||
.register_get("waiting", |obj: &mut ObjectInfo| obj.queue.waiting())
|
||||
.register_get("queue", |obj: &mut ObjectInfo| obj.queue.clone());
|
||||
@@ -122,15 +119,10 @@ impl Registerable for ObjectInfo {
|
||||
|
||||
engine.register_fn("delay", |o: &mut ObjectInfo, dt: f64| o.queue.delay(dt));
|
||||
|
||||
engine.register_fn("can_push", move |o: &mut ObjectInfo, dir: Direction| {
|
||||
let board = o.board.borrow();
|
||||
board.in_bounds((o.x, o.y)) && board.can_push(o.x as usize, o.y as usize, dir)
|
||||
});
|
||||
|
||||
engine.register_fn("blocked", move |o: &mut ObjectInfo, dir: Direction| -> bool {
|
||||
let board = o.board.borrow();
|
||||
let (tx, ty) = dir.from_point(o.x, o.y);
|
||||
!board.in_bounds((tx, ty)) || !board.is_passable(tx as usize, ty as usize)
|
||||
let t = dir.from_point(o.location);
|
||||
!board.in_bounds(t) || !board.is_empty(t)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ impl Registerable for PlayerWithPos {
|
||||
.register_get("health", |player: &mut PlayerWithPos| player.0.borrow().health)
|
||||
.register_get("max_health", |player: &mut PlayerWithPos| player.0.borrow().max_health)
|
||||
.register_get("keys", |player: &mut PlayerWithPos| player.0.borrow().keys)
|
||||
.register_get("x", |player: &mut PlayerWithPos| player.1.borrow().player_pos().0 as i64)
|
||||
.register_get("y", |player: &mut PlayerWithPos| player.1.borrow().player_pos().1 as i64);
|
||||
.register_get("x", |player: &mut PlayerWithPos| player.1.borrow().player_pos().x)
|
||||
.register_get("y", |player: &mut PlayerWithPos| player.1.borrow().player_pos().y);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-142
@@ -129,7 +129,7 @@ impl Board {
|
||||
let p = p.into();
|
||||
let grid_glyph = self.get(p).as_ref().map(Tile::glyph);
|
||||
|
||||
let sensors = self.sensors.iter().filter(|&s| s.x == p.ux() && s.y == p.uy());
|
||||
let sensors = self.sensors.iter().filter(|&s| s.location == p);
|
||||
|
||||
// Is there a sensor above the grid?
|
||||
if let Some(above) = sensors.clone().find(|s| s.draw_layer == DrawLayer::Above && s.scripting.glyph.is_visible()) {
|
||||
@@ -148,7 +148,7 @@ impl Board {
|
||||
|
||||
// Otherwise the floor, or the canonical black empty cell.
|
||||
self.floor
|
||||
.glyph_at(p.ux(), p.uy(), self.width)
|
||||
.glyph_at(p, self.width)
|
||||
.unwrap_or_else(|| Glyph::transparent())
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl Board {
|
||||
/// Panics if `x` or `y` are out of bounds.
|
||||
pub fn is_opaque_at<P: Into<Point>>(&self, p: P) -> bool {
|
||||
let p = p.into();
|
||||
if self.sensors.iter().any(|s| s.x == p.ux() && s.y == p.uy() && s.scripting.optics.opaque) {
|
||||
if self.sensors.iter().any(|s| s.location == p && s.scripting.optics.opaque) {
|
||||
true
|
||||
} else {
|
||||
match self.get(p) {
|
||||
@@ -240,110 +240,13 @@ impl Board {
|
||||
// Glowing sensors
|
||||
for s in self.sensors.iter() {
|
||||
if s.optics().glow > 0 {
|
||||
add_source(&mut lighting, s.x, s.y, s.optics().glow, color_to_rgb(s.scripting.glyph.fg));
|
||||
add_source(&mut lighting, s.location.ux(), s.location.uy(), s.optics().glow, color_to_rgb(s.scripting.glyph.fg));
|
||||
}
|
||||
}
|
||||
|
||||
Some(lighting)
|
||||
}
|
||||
|
||||
// /// Whether the solid at `(x, y)` can be **shifted** one step in `dir`: it is
|
||||
// /// itself a pushable solid *and* the next cell is either empty or holds another
|
||||
// /// pushable solid.
|
||||
// ///
|
||||
// /// Unlike [`can_push`](Board::can_push) this inspects only the single cell
|
||||
// /// ahead — it does **not** verify the whole chain ends in open space. It is the
|
||||
// /// right test for a simultaneous rotation/shift (applied via
|
||||
// /// [`apply_swap`](Board::apply_swap)), where a destination is occupied by
|
||||
// /// another pushable that is itself moving the same frame. Returns `false` if
|
||||
// /// `(x, y)` holds no pushable, or the cell ahead runs off the board.
|
||||
// ///
|
||||
// /// The **player** is always a blocker: a shift can't relocate the player, and
|
||||
// /// `apply_swap` refuses to overwrite it, so a cell holding the player is never
|
||||
// /// an acceptable shift destination.
|
||||
// pub fn can_shift(&self, x: usize, y: usize, dir: Direction) -> bool {
|
||||
// // The source must hold a solid pushable in `dir`.
|
||||
// if !self.is_pushable(x, y, dir) {
|
||||
// return false;
|
||||
// }
|
||||
// let (dx, dy): (i64, i64) = dir.into();
|
||||
// let next = (x as i64 + dx, y as i64 + dy);
|
||||
// if !self.in_bounds(next) {
|
||||
// return false; // nothing to shift into off the board
|
||||
// }
|
||||
// let (nx, ny) = (next.0 as usize, next.1 as usize);
|
||||
// // The player always blocks a shift: a shift can't relocate it and
|
||||
// // `apply_swap` refuses to overwrite it. (The player reads as pushable, so it
|
||||
// // must be excluded explicitly before the cell-ahead test below.)
|
||||
// if self.get(nx, ny).as_ref().is_some_and(Tile::player) {
|
||||
// return false;
|
||||
// }
|
||||
// // The cell ahead is acceptable if it is empty or another pushable solid.
|
||||
// self.is_passable(nx, ny) || self.is_pushable(nx, ny, dir)
|
||||
// }
|
||||
|
||||
// /// Shoves the chain of pushable solids starting at `(x, y)` one step in `dir`,
|
||||
// /// leaving `Empty` floor behind each moved cell.
|
||||
// ///
|
||||
// /// No-op when the chain can't move (it self-checks via [`can_push`](Board::can_push)),
|
||||
// /// so it is safe to call unconditionally.
|
||||
// /// 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 Vec::new();
|
||||
// }
|
||||
// let (dx, dy): (i64, i64) = dir.into();
|
||||
// // can_push guaranteed the chain ends at an in-bounds passable cell, so
|
||||
// // re-walk it (no bounds checks needed) and shift the far end first, which
|
||||
// // keeps each destination cell vacated before its occupant arrives.
|
||||
// let mut chain: Vec<(usize, usize)> = Vec::new();
|
||||
// let (mut cx, mut cy) = (x, y);
|
||||
// while !self.is_passable(cx, cy) {
|
||||
// chain.push((cx, cy));
|
||||
// cx = (cx as i64 + dx) as usize;
|
||||
// cy = (cy as i64 + dy) as usize;
|
||||
// }
|
||||
// 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()
|
||||
// }
|
||||
|
||||
// /// Tries to walk the object in x,y in the given direction. Pushes the cell in that direction
|
||||
// /// first. This won't move anything if:
|
||||
// /// - the cell x,y is empty; no-op
|
||||
// /// - x,y is out of bounds, no-op
|
||||
// /// - the target cell can't be pushed in that direction, either because it's `Block` or
|
||||
// /// something in its chain is
|
||||
// pub fn move_object<P: Into<Point>>(&mut self, p: P, dir: Direction) {
|
||||
// let p = p.into();
|
||||
// // Is this even a cell?
|
||||
// if self.in_bounds(p) {
|
||||
// // Is there anything in this cell?
|
||||
// if !self.is_empty(p) {
|
||||
// let (tx, ty) = dir.from_point(p.x, p.y);
|
||||
// // Is the _target_ in bounds?
|
||||
// if self.in_bounds((tx, ty)) {
|
||||
// // Attempt to push the target out of the way
|
||||
// self.push(tx as usize, ty as usize, dir);
|
||||
// // Is the cell now empty?
|
||||
// if self.get(tx as usize, ty as usize).is_none() {
|
||||
// // Then finally, move the thing:
|
||||
// let thing = self.grid[x + y * self.width].take();
|
||||
// self.grid[tx as usize + ty as usize * self.width] = thing;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/// 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
|
||||
@@ -353,7 +256,7 @@ impl Board {
|
||||
let p = p.into();
|
||||
self.sensors
|
||||
.iter()
|
||||
.filter(|s| s.x == p.ux() && s.y == p.uy())
|
||||
.filter(|s| s.location == p)
|
||||
.map(|s| s.scripting.id)
|
||||
.collect()
|
||||
}
|
||||
@@ -361,7 +264,7 @@ impl Board {
|
||||
/// Find and return the portal at the given location
|
||||
pub fn portal_at<P: Into<Point>>(&self, p: P) -> Option<&Portal> {
|
||||
let p = p.into();
|
||||
self.portals.iter().find(|&portal| portal.location() == (p.ux(), p.uy()))
|
||||
self.portals.iter().find(|&portal| portal.location == p)
|
||||
}
|
||||
|
||||
/// Editor primitive: stamps `arch` (with visual `glyph`) into the cell at
|
||||
@@ -391,7 +294,7 @@ impl Board {
|
||||
/// `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)]) -> Result<Vec<((i64, i64), (i64, i64))>, String> {
|
||||
pub fn apply_shift(&mut self, cells: &[Point]) -> Result<Vec<(Point, Point)>, String> {
|
||||
// Validate all the cells are in bounds, error if not:
|
||||
if cells.iter().any(|&c| !self.in_bounds(c)) {
|
||||
return Err("Called shift() with a cell out of bounds".to_string())
|
||||
@@ -474,7 +377,7 @@ impl Board {
|
||||
|
||||
for (i, tile) in self.grid.iter().enumerate() {
|
||||
if let Some(Tile::Object(obj)) = tile && obj.scripting.id == id {
|
||||
return Some(Box::new(LocatedObject(obj, (i % self.width, i / self.width))))
|
||||
return Some(Box::new(LocatedObject(obj, (i % self.width, i / self.width).into())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +397,7 @@ impl Board {
|
||||
if let Some(Tile::Object(obj)) = tile &&
|
||||
let Some(n) = obj.scripting.name.as_ref() &&
|
||||
n.as_str() == name {
|
||||
return Some(Box::new(LocatedObject(obj, (i % self.width, i / self.width))))
|
||||
return Some(Box::new(LocatedObject(obj, (i % self.width, i / self.width).into())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +433,7 @@ impl Board {
|
||||
// Add the objects into it
|
||||
for (i, tile) in self.grid.iter().enumerate() {
|
||||
if let Some(Tile::Object(obj)) = tile {
|
||||
hookables.push(Box::new(LocatedObject(obj, (i % self.width, i / self.width))))
|
||||
hookables.push(Box::new(LocatedObject(obj, (i % self.width, i / self.width).into())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,10 +456,9 @@ impl Board {
|
||||
|
||||
pub fn move_sensor(&mut self, id: ObjectId, dir: Direction) {
|
||||
if let Some((sensor_idx, _)) = self.sensors.iter().enumerate().find(|(idx, s)| s.scripting.id == id) {
|
||||
let new_loc = (self.sensors[sensor_idx].x as i64 + dir.dx(), self.sensors[sensor_idx].y as i64 + dir.dy());
|
||||
let new_loc = self.sensors[sensor_idx].location.in_dir(dir);
|
||||
if self.in_bounds(new_loc) {
|
||||
self.sensors[sensor_idx].x = new_loc.0 as usize;
|
||||
self.sensors[sensor_idx].y = new_loc.1 as usize;
|
||||
self.sensors[sensor_idx].location = new_loc
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,7 +497,7 @@ pub(crate) mod tests {
|
||||
use crate::builtin::Builtin;
|
||||
use crate::floor::Floor;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::utils::{Direction, ObjectId};
|
||||
use crate::utils::{Direction, ObjectId, Point};
|
||||
use color::Rgba8;
|
||||
use std::collections::HashMap;
|
||||
use crate::object_def::ObjectDef;
|
||||
@@ -638,17 +540,17 @@ pub(crate) mod tests {
|
||||
|
||||
/// Stamps a crate cell onto the grid.
|
||||
pub(crate) fn crate_at(board: &mut Board, x: usize, y: usize) {
|
||||
*board.get_mut(x, y) = Some(TileSpec::krate().into_tile(&mut board.next_object_id).unwrap());
|
||||
*board.get_mut((x, y)) = Some(TileSpec::krate().into_tile(&mut board.next_object_id).unwrap());
|
||||
}
|
||||
|
||||
/// Stamps a wall cell onto the grid.
|
||||
pub(crate) fn wall_at(board: &mut Board, x: usize, y: usize) {
|
||||
*board.get_mut(x, y) = Some(TileSpec::wall().into_tile(&mut board.next_object_id).unwrap());
|
||||
*board.get_mut((x, y)) = Some(TileSpec::wall().into_tile(&mut board.next_object_id).unwrap());
|
||||
}
|
||||
|
||||
/// Stamps a gem cell onto the grid.
|
||||
pub(crate) fn gem_at(board: &mut Board, x: usize, y: usize) {
|
||||
*board.get_mut(x, y) = Some(TileSpec::gem().into_tile(&mut board.next_object_id).unwrap());
|
||||
*board.get_mut((x, y)) = Some(TileSpec::gem().into_tile(&mut board.next_object_id).unwrap());
|
||||
}
|
||||
|
||||
/// Stamps the builtin named `kind` (any alias accepted by [`Builtin::from_name`],
|
||||
@@ -666,7 +568,7 @@ pub(crate) mod tests {
|
||||
Tile::Object(obj) => obj.scripting.id,
|
||||
Tile::Player => unreachable!("a builtin never resolves to the player"),
|
||||
};
|
||||
*board.get_mut(x, y) = Some(tile);
|
||||
*board.get_mut((x, y)) = Some(tile);
|
||||
id
|
||||
}
|
||||
|
||||
@@ -697,7 +599,7 @@ pub(crate) mod tests {
|
||||
Tile::Object(obj) => obj.scripting.id,
|
||||
Tile::Player => unreachable!("an object spec never resolves to the player"),
|
||||
};
|
||||
*board.get_mut(x, y) = Some(tile);
|
||||
*board.get_mut((x, y)) = Some(tile);
|
||||
id
|
||||
}
|
||||
|
||||
@@ -726,7 +628,7 @@ pub(crate) mod tests {
|
||||
Tile::Object(obj) => obj.scripting.id,
|
||||
Tile::Player => unreachable!("an object spec never resolves to the player"),
|
||||
};
|
||||
*board.get_mut(x, y) = Some(tile);
|
||||
*board.get_mut((x, y)) = Some(tile);
|
||||
id
|
||||
}
|
||||
|
||||
@@ -755,7 +657,7 @@ pub(crate) mod tests {
|
||||
|
||||
/// Stamps a player cell onto the grid.
|
||||
pub(crate) fn player_at(board: &mut Board, x: usize, y: usize) {
|
||||
*board.get_mut(x, y) = Some(TileSpec::player().into_tile(&mut board.next_object_id).unwrap());
|
||||
*board.get_mut((x, y)) = Some(TileSpec::player().into_tile(&mut board.next_object_id).unwrap());
|
||||
}
|
||||
|
||||
pub(crate) fn lamp_at(board: &mut Board, x: usize, y: usize) {
|
||||
@@ -778,7 +680,7 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
pub(crate) fn is_builtin(board: &Board, x: usize, y: usize, tag: &str) -> bool {
|
||||
if let Some(Tile::Object(obj)) = board.get(x, y) {
|
||||
if let Some(Tile::Object(obj)) = board.get((x, y)) {
|
||||
obj.scripting.tags.contains(&format!("BUILTIN_{tag}"))
|
||||
} else { false }
|
||||
}
|
||||
@@ -794,20 +696,6 @@ pub(crate) mod tests {
|
||||
assert!(!board.in_bounds((0, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_push_is_read_only_and_correct() {
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
crate_at(&mut board, 1, 0);
|
||||
assert!(board.can_push(1, 0, Direction::East));
|
||||
assert_matches!(board.get(1, 0), Some(Tile::Object(_)));
|
||||
assert_matches!(board.get(2, 0), None);
|
||||
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
crate_at(&mut board, 1, 0);
|
||||
wall_at(&mut board, 2, 0);
|
||||
assert!(!board.can_push(1, 0, Direction::East));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_shift_only_checks_the_cell_ahead() {
|
||||
// Source must be pushable.
|
||||
@@ -817,7 +705,7 @@ pub(crate) mod tests {
|
||||
// Crate with open space ahead: shiftable.
|
||||
crate_at(&mut board, 0, 0);
|
||||
assert!(board.can_shift(0, 0, Direction::East));
|
||||
assert_matches!(board.get(0, 0), Some(Tile::Object(_)));
|
||||
assert_matches!(board.get((0, 0)), Some(Tile::Object(_)));
|
||||
|
||||
// Crate with another pushable crate ahead: still shiftable (unlike can_push,
|
||||
// which would follow the chain to the wall and fail).
|
||||
@@ -861,9 +749,9 @@ pub(crate) mod tests {
|
||||
crate_at(&mut board, 1, 0);
|
||||
assert!(board.can_push(1, 0, Direction::East));
|
||||
board.push(1, 0, Direction::East);
|
||||
assert!(board.get(1, 0).is_none());
|
||||
assert!(board.get((1, 0)).is_none());
|
||||
assert!(is_builtin(&board, 2, 0,"crate"));
|
||||
assert_eq!(board.player_pos(), (3, 0));
|
||||
assert_eq!(board.player_pos(), Point { x: 3, y: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -875,7 +763,7 @@ pub(crate) mod tests {
|
||||
assert!(!board.can_push(1, 0, Direction::East));
|
||||
board.push(1, 0, Direction::East); // no-op
|
||||
assert!(is_builtin(&board, 1, 0, "crate"));
|
||||
assert_eq!(board.player_pos(), (2, 0));
|
||||
assert_eq!(board.player_pos(), Point { x: 2, y: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -901,8 +789,8 @@ pub(crate) mod tests {
|
||||
add_floor(&mut board, floor_glyph);
|
||||
wall_at(&mut board, 0, 0);
|
||||
// The wall (solid) draws over the floor; the empty cell reveals the floor.
|
||||
assert_eq!(board.glyph_at(0, 0), Builtin::Wall.default_glyph_for("wall"));
|
||||
assert_eq!(board.glyph_at(1, 0), floor_glyph);
|
||||
assert_eq!(board.glyph_at((0, 0)), Builtin::Wall.default_glyph_for("wall"));
|
||||
assert_eq!(board.glyph_at((1, 0)), floor_glyph);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -939,7 +827,7 @@ pub(crate) mod tests {
|
||||
assert!(is_builtin(&board, 0, 0, "crate")); // wrapped from (4,0)
|
||||
assert!(is_builtin(&board, 1, 0, "crate")); // moved from (0,0)
|
||||
assert!(is_builtin(&board, 2, 0, "wall")); // blocked, immobile
|
||||
assert!(board.get(3, 0).is_none()); // cleared, crate moved
|
||||
assert!(board.get((3, 0)).is_none()); // cleared, crate moved
|
||||
assert!(is_builtin(&board, 4, 0, "crate")); // moved from (3,0)
|
||||
}
|
||||
|
||||
@@ -955,8 +843,8 @@ pub(crate) mod tests {
|
||||
fn wall_is_opaque_empty_is_not() {
|
||||
let mut board = open_board(3, 1, (0, 0));
|
||||
wall_at(&mut board, 1, 0);
|
||||
assert!(board.is_opaque_at(1, 0)); // wall blocks sight
|
||||
assert!(!board.is_opaque_at(2, 0)); // empty cell is transparent
|
||||
assert!(board.is_opaque_at((1, 0))); // wall blocks sight
|
||||
assert!(!board.is_opaque_at((2, 0))); // empty cell is transparent
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -154,9 +154,9 @@ impl BoardSpec {
|
||||
}
|
||||
}
|
||||
|
||||
for Portal { name, x, y, .. } in self.portals.iter() {
|
||||
for Portal { name, location, .. } in self.portals.iter() {
|
||||
count(&mut portal_names, name);
|
||||
count(&mut portal_locations, &(x + y * self.width));
|
||||
count(&mut portal_locations, &(location.ux() + location.uy() * self.width));
|
||||
}
|
||||
|
||||
let obj_dupes = obj_names.into_iter().filter_map(|(name, count)| if count > 1 { Some(name) } else { None }).collect::<Vec<_>>();
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::glyph::Glyph;
|
||||
use color::Rgba8;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tinyrand::{Probability, Rand, Seeded, StdRand};
|
||||
use crate::utils::Point;
|
||||
|
||||
/// A board's floor: the cosmetic backdrop drawn beneath everything, replacing the
|
||||
/// old dedicated floor *layer*. A board has exactly one [`Floor`] (see
|
||||
@@ -62,11 +63,12 @@ impl Floor {
|
||||
/// The floor glyph to draw at `(x, y)`, or `None` for [`Floor::Blank`].
|
||||
///
|
||||
/// `width` is the board width, needed to index a biome's row-major buffer.
|
||||
pub(crate) fn glyph_at(&self, x: usize, y: usize, width: usize) -> Option<Glyph> {
|
||||
pub(crate) fn glyph_at<P: Into<Point>>(&self, p: P, width: usize) -> Option<Glyph> {
|
||||
let p = p.into();
|
||||
match self {
|
||||
Floor::Blank => None,
|
||||
Floor::Fixed(g) => Some(*g),
|
||||
Floor::Biome { glyphs, .. } => glyphs.get(y * width + x).copied(),
|
||||
Floor::Biome { glyphs, .. } => glyphs.get(p.uy() * width + p.ux()).copied(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
-58
@@ -231,7 +231,8 @@ impl GameState {
|
||||
for ba in actions {
|
||||
match ba.action {
|
||||
Action::Move(dir) => {
|
||||
step_object(&mut self.board_mut(), ba.source, dir);
|
||||
let loc = self.board().get_hookable(ba.source).map(|o| o.location());
|
||||
loc.map(|loc| self.resolve_move(loc, dir));
|
||||
}
|
||||
Action::SetTile(tile) => {
|
||||
if let Some(scr) = self.board_mut().scripting_mut(ba.source) {
|
||||
@@ -296,10 +297,11 @@ impl GameState {
|
||||
apply_teleport(&mut self.board_mut(), target, x, y).unwrap_or_else(|e| log_sink.error(e))
|
||||
}
|
||||
Action::Push { x, y, dir } => {
|
||||
self.resolve_move((x as usize, y as usize), dir);
|
||||
self.resolve_move((x, y), dir);
|
||||
//apply_push(&mut self.board_mut(), x, y, dir).unwrap_or_else(|e| log_sink.error(e))
|
||||
}
|
||||
Action::Shift(cells) => {
|
||||
let cells = cells.into_iter().map(|c| c.into()).collect::<Vec<Point>>();
|
||||
apply_shift(&mut self.board_mut(), &cells).unwrap_or_else(|e| log_sink.error(e))
|
||||
}
|
||||
Action::AddGems(n) => {
|
||||
@@ -354,8 +356,8 @@ impl GameState {
|
||||
// Find the named arrival portal on the target board (borrow then release).
|
||||
let arrival = self.world.boards[target_map]
|
||||
.borrow()
|
||||
.named_portal(target_entry).map(Portal::location);
|
||||
let (ax, ay) = match arrival {
|
||||
.named_portal(target_entry).map(|p| p.location);
|
||||
let new_board_pos = match arrival {
|
||||
Some(pos) => pos,
|
||||
None => {
|
||||
self.log.push(LogLine::error(format!(
|
||||
@@ -371,9 +373,9 @@ impl GameState {
|
||||
self.current_board_name = target_map.to_string();
|
||||
// Clear any player instance that's already on the new board
|
||||
let new_board_player_pos = self.board().player_pos();
|
||||
self.board_mut().get_mut(new_board_player_pos.0, new_board_player_pos.1).take();
|
||||
self.board_mut().get_mut(new_board_player_pos).take();
|
||||
// place the player at the arrival portal.
|
||||
self.board_mut().get_mut(ax, ay).replace(Tile::Player);
|
||||
self.board_mut().get_mut(new_board_pos).replace(Tile::Player);
|
||||
self.board_mut().clear_all_queues();
|
||||
// Rebuild the script host for the new board's objects.
|
||||
self.scripts = ScriptHost::new(
|
||||
@@ -387,24 +389,25 @@ impl GameState {
|
||||
self.run_init();
|
||||
}
|
||||
|
||||
fn resolve_move(&mut self, from: (usize, usize), dir: Direction) -> bool {
|
||||
fn resolve_move<P: Into<Point>>(&mut self, from: P, dir: Direction) -> bool {
|
||||
let from = from.into();
|
||||
// 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; }
|
||||
let target: Point = dir.from_point(from).into();
|
||||
if !self.board().in_bounds(target) { 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);
|
||||
self.board_mut().move_cell(from, 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);
|
||||
if self.resolve_move(target, dir) {
|
||||
self.board_mut().move_cell(from, 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() {
|
||||
if let Some(Tile::Object(b)) = self.board_mut().get(target) && let def = b.as_ref() {
|
||||
def.scripting.id
|
||||
} else {
|
||||
unreachable!("moving into the player was handled above")
|
||||
@@ -412,7 +415,7 @@ impl GameState {
|
||||
};
|
||||
|
||||
// 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));
|
||||
// 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());
|
||||
@@ -426,7 +429,7 @@ impl GameState {
|
||||
// // 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();
|
||||
// self.board_mut().get_mut((target.0, target.1)).take();
|
||||
// a
|
||||
// }
|
||||
//
|
||||
@@ -445,10 +448,10 @@ impl GameState {
|
||||
// // 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());
|
||||
// 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![]
|
||||
// }
|
||||
//
|
||||
@@ -456,7 +459,7 @@ impl GameState {
|
||||
// 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![] }
|
||||
// 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()) }
|
||||
@@ -467,14 +470,14 @@ impl GameState {
|
||||
|
||||
// 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);
|
||||
if !self.board().is_empty(from) && self.board().is_empty(target) {
|
||||
self.board_mut().move_cell(from, 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())
|
||||
self.board().is_empty(from)
|
||||
}
|
||||
|
||||
/// Attempts to move the player one cell in `dir`.
|
||||
@@ -487,7 +490,7 @@ impl GameState {
|
||||
pub fn try_move(&mut self, dir: Direction) {
|
||||
let (dx, dy): (i64, i64) = dir.into();
|
||||
let player_loc = self.board().player_pos();
|
||||
let target = (player_loc.0 as i64 + dx, player_loc.1 as i64 + dy);
|
||||
let target = (player_loc.x + dx, player_loc.y + dy);
|
||||
if !self.board().in_bounds(target) {
|
||||
return;
|
||||
}
|
||||
@@ -499,7 +502,7 @@ impl GameState {
|
||||
if new_loc != player_loc {
|
||||
// Portals take priority: if we're on a portal it doesn't matter what else we entered:
|
||||
let portal_info = {
|
||||
if let Some(Portal { target_board, target_name, ..}) = self.board().portal_at(new_loc.0, new_loc.1) {
|
||||
if let Some(Portal { target_board, target_name, ..}) = self.board().portal_at(new_loc) {
|
||||
Some((target_board.clone(), target_name.clone()))
|
||||
} else { None }
|
||||
};
|
||||
@@ -508,7 +511,7 @@ impl GameState {
|
||||
self.enter_board(&target_board, &target_name)
|
||||
} else {
|
||||
// We're still on the board, so see if we stepped on any sensors:
|
||||
let sensor_ids = self.board().sensor_ids_at(new_loc.0, new_loc.1);
|
||||
let sensor_ids = self.board().sensor_ids_at(new_loc);
|
||||
for id in sensor_ids {
|
||||
let actions = self.scripts.run_enter(id, dir.opposite());
|
||||
self.apply_actions(actions)
|
||||
@@ -520,37 +523,11 @@ impl GameState {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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. 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) {
|
||||
// TODO when an object pushes the player, it should still trigger actions on
|
||||
// what the player is pushed into. But, for right now, just call board::push
|
||||
let (loc, solid) = if let Some(obj) = board.get_hookable(id) {
|
||||
(obj.location(), obj.solid())
|
||||
} else { return };
|
||||
|
||||
if solid {
|
||||
// This is a real object on the board, try and push it
|
||||
board.move_object((loc.0, loc.1), dir);
|
||||
} else {
|
||||
// This is a sensor, we can just teleport it
|
||||
board.move_sensor(id, dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::GameState;
|
||||
use crate::Direction;
|
||||
use crate::utils::Point;
|
||||
use crate::board::tests::{
|
||||
builtin_at, crate_at, gem_at, is_builtin, open_board, sensor_at, wall_at,
|
||||
};
|
||||
@@ -572,7 +549,7 @@ mod tests {
|
||||
// The gem was grabbed: gem count up, gem object gone, player on its cell.
|
||||
assert_eq!(game.player.borrow().gems, 1);
|
||||
assert!(game.board().all_ids().is_empty());
|
||||
assert_eq!(game.board().player_pos(), (1, 0));
|
||||
assert_eq!(game.board().player_pos(), Point { x: 1, y: 0 });
|
||||
}
|
||||
|
||||
|
||||
@@ -600,7 +577,7 @@ mod tests {
|
||||
let mut game = game_with_object_script(5, "fn tick(me, dt) { shift([[2, 0], [3, 0]]); }");
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
assert!(b.get(2, 0).is_none());
|
||||
assert!(b.get((2, 0)).is_none());
|
||||
assert!(is_builtin(&b, 3, 0, "crate"));
|
||||
}
|
||||
|
||||
@@ -709,7 +686,7 @@ mod tests {
|
||||
assert!(is_builtin(&b, 2, 0, "crate")); // NE: filled by N
|
||||
assert!(is_builtin(&b, 1, 0, "crate")); // N: filled by NW
|
||||
assert!(is_builtin(&b, 0, 0, "crate")); // NW: filled by W
|
||||
assert!(b.get(0, 1).is_none()); // W: vacated (hole moved here)
|
||||
assert!(b.get((0, 1)).is_none()); // W: vacated (hole moved here)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -720,8 +697,8 @@ mod tests {
|
||||
game.tick(Duration::from_millis(16));
|
||||
let b = game.board();
|
||||
assert!(is_builtin(&b, 0, 0, "crate")); // NW: crate rotated counter-clockwise
|
||||
assert!(b.get(2, 0).is_none()); // NE: untouched
|
||||
assert!(b.get(1, 0).is_none()); // N: vacated
|
||||
assert!(b.get((2, 0)).is_none()); // NE: untouched
|
||||
assert!(b.get((1, 0)).is_none()); // N: vacated
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::glyph::Glyph;
|
||||
use crate::utils::Point;
|
||||
|
||||
/// A portal that teleports the player to a named entry point on another board.
|
||||
///
|
||||
@@ -16,8 +17,8 @@ use crate::glyph::Glyph;
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct Portal {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
#[serde(flatten)]
|
||||
pub location: Point,
|
||||
/// Board-unique name for this portal, also used as `target_entry` by portals
|
||||
/// on other boards that want to arrive here.
|
||||
pub name: String,
|
||||
@@ -28,9 +29,3 @@ pub struct Portal {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub glyph: Option<Glyph>
|
||||
}
|
||||
|
||||
impl Portal {
|
||||
pub fn location(&self) -> (usize, usize) {
|
||||
(self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::board::tests::{crate_at, is_builtin, object_at, open_board, wall_at};
|
||||
use crate::game::GameState;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::tile::EnterResponse;
|
||||
use crate::utils::ObjectId;
|
||||
use crate::utils::{ObjectId, Point};
|
||||
use std::time::Duration;
|
||||
|
||||
/// The cell object `id` currently occupies.
|
||||
@@ -85,7 +85,7 @@ fn object_pushes_crate_on_init() {
|
||||
assert_eq!(loc(&game, id), (2, 0));
|
||||
let b = game.board();
|
||||
assert!(is_builtin(&b, 3, 0, "crate")); // crate shoved one east
|
||||
assert!(b.get(1, 0).is_none()); // the object's old cell is vacated
|
||||
assert!(b.get((1, 0)).is_none()); // the object's old cell is vacated
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -93,7 +93,7 @@ fn object_push_into_player() {
|
||||
// A scripted object moving into the player pushes the player when there's room.
|
||||
let (game, id) = game_with_mover(5, 1, (2, 0), (1, 0), "fn init(me) { move(East); }");
|
||||
assert_eq!(loc(&game, id), (2, 0));
|
||||
assert_eq!(game.board().player_pos(), (3, 0));
|
||||
assert_eq!(game.board().player_pos(), Point { x: 3, y: 0 });
|
||||
|
||||
// With a wall behind the player, the object is blocked and nothing moves.
|
||||
let mut board = open_board(4, 1, (2, 0));
|
||||
@@ -104,7 +104,7 @@ fn object_push_into_player() {
|
||||
game.run_init();
|
||||
|
||||
assert_eq!(loc(&game, id), (1, 0));
|
||||
assert_eq!(game.board().player_pos(), (2, 0));
|
||||
assert_eq!(game.board().player_pos(), Point { x: 2, y: 0 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::board::Board;
|
||||
use crate::board::tests::open_board;
|
||||
use crate::game::GameState;
|
||||
use crate::portal::Portal;
|
||||
use crate::utils::Direction;
|
||||
use crate::utils::{Direction, Point};
|
||||
use crate::world::World;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -75,7 +75,7 @@ fn enter_board_places_player_at_arrival_portal() {
|
||||
let mut game = GameState::from_world(two_board_world());
|
||||
game.enter_board("b2", "from_b1");
|
||||
// Arrival portal "from_b1" is at (1, 1) on b2.
|
||||
assert_eq!(game.board().player_pos(), (1, 1));
|
||||
assert_eq!(game.board().player_pos(), Point { x: 1, y: 1 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -90,7 +90,7 @@ fn entering_a_board_leaves_exactly_one_player_on_it() {
|
||||
let b = game.board();
|
||||
let players = (0..b.height)
|
||||
.flat_map(|y| (0..b.width).map(move |x| (x, y)))
|
||||
.filter(|&(x, y)| b.get(x, y).as_ref().is_some_and(|t| t.player()))
|
||||
.filter(|&(x, y)| b.get((x, y)).as_ref().is_some_and(|t| t.player()))
|
||||
.count();
|
||||
assert_eq!(players, 1, "arriving on a board must not duplicate the player");
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ palette = { " " = { kind = "empty" }, "@" = { kind = "player" }, "O" = { kind =
|
||||
assert_eq!(board.objects.len(), 1);
|
||||
assert_eq!((board.objects[&1].x, board.objects[&1].y), (2, 0));
|
||||
// An unlisted sparse cell is a transparent empty.
|
||||
assert_eq!(board.get(1, 0).1, Archetype::Empty);
|
||||
assert_eq!(board.get((1, 0)).1, Archetype::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -96,5 +96,5 @@ palette = { " " = { kind = "empty" } }
|
||||
!board.is_valid(),
|
||||
"a non-single-char fill is a nonfatal error"
|
||||
);
|
||||
assert_eq!(board.get(1, 0).1, Archetype::Empty);
|
||||
assert_eq!(board.get((1, 0)).1, Archetype::Empty);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ fn unknown_kind_produces_error_block() {
|
||||
);
|
||||
let board = load_board(&toml);
|
||||
assert_eq!(
|
||||
*board.get(0, 0),
|
||||
*board.get((0, 0)),
|
||||
(Archetype::ErrorBlock.default_glyph(), Archetype::ErrorBlock)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ fn palette_placement_puts_object_on_empty_floor() {
|
||||
let board = load_board(&map_3x1_object("G.@", "G", ""));
|
||||
assert_eq!(board.objects.len(), 1);
|
||||
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0));
|
||||
assert_eq!(board.get(0, 0).1, Archetype::Empty);
|
||||
assert_eq!(board.get((0, 0)).1, Archetype::Empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -13,7 +13,7 @@ const WALL: (&str, &str) = (
|
||||
fn player_char_places_on_empty_floor() {
|
||||
let b = load_board(&map(3, 1, &grid(".@.", &[EMPTY, PLAYER])));
|
||||
assert_eq!((b.player.x, b.player.y), (1, 0));
|
||||
assert_eq!(b.get(1, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get((1, 0)).1, Archetype::Empty);
|
||||
assert!(b.is_valid());
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ fn player_fallback_to_origin_clears_solid_terrain() {
|
||||
// player wins its cell: the wall is cleared. The fallback is still reported.
|
||||
let b = load_board(&map(3, 1, &grid("#..", &[WALL, EMPTY])));
|
||||
assert_eq!((b.player.x, b.player.y), (0, 0));
|
||||
assert_eq!(b.get(0, 0).1, Archetype::Empty);
|
||||
assert_eq!(b.get((0, 0)).1, Archetype::Empty);
|
||||
assert!(!b.is_valid());
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ fn pusher_advances_and_shoves_a_crate() {
|
||||
let b = game.board();
|
||||
assert!(b.objects[&pid].x > 0, "pusher advanced east");
|
||||
assert_eq!(
|
||||
b.get(1, 0).1,
|
||||
b.get((1, 0)).1,
|
||||
Archetype::Empty,
|
||||
"crate left its start cell"
|
||||
);
|
||||
|
||||
@@ -131,10 +131,10 @@ fn fixed_floor_attribute_round_trips_through_toml() {
|
||||
};
|
||||
let board = load_board(toml);
|
||||
// An empty grid cell reveals the fixed floor.
|
||||
assert_eq!(board.glyph_at(1, 0), fixed);
|
||||
assert_eq!(board.glyph_at((1, 0)), fixed);
|
||||
|
||||
let board2 = round_trip(toml);
|
||||
assert_eq!(board2.glyph_at(1, 0), fixed);
|
||||
assert_eq!(board2.glyph_at((1, 0)), fixed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -153,5 +153,5 @@ fn biome_floor_round_trips_generator_name() {
|
||||
);
|
||||
// Reloaded floor glyphs match (deterministic seed).
|
||||
let board2 = load_board(&toml_out);
|
||||
assert_eq!(board2.glyph_at(1, 0), board.glyph_at(1, 0));
|
||||
assert_eq!(board2.glyph_at((1, 0)), board.glyph_at((1, 0)));
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::board::tests::{add_floor, builtin_at, crate_at, is_builtin, open_boar
|
||||
use crate::game::GameState;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::tile::EnterResponse;
|
||||
use crate::utils::{Direction, Pushable};
|
||||
use crate::utils::{Direction, Point, Pushable};
|
||||
use color::Rgba8;
|
||||
|
||||
#[test]
|
||||
@@ -30,10 +30,10 @@ fn pushing_a_crate_reveals_the_floor_underneath() {
|
||||
game.try_move(Direction::East);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (1, 0));
|
||||
assert_eq!(b.player_pos(), Point { x: 1, y: 0 });
|
||||
assert!(is_builtin(&b, 2, 0, "crate"));
|
||||
assert!(b.get(0, 0).is_none()); // vacated
|
||||
assert_eq!(b.glyph_at(0, 0), floor_glyph);
|
||||
assert!(b.get((0, 0)).is_none()); // vacated
|
||||
assert_eq!(b.glyph_at((0, 0)), floor_glyph);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -44,9 +44,9 @@ fn player_pushes_single_crate() {
|
||||
game.try_move(Direction::East);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (1, 0));
|
||||
assert_eq!(b.player_pos(), Point { x: 1, y: 0 });
|
||||
assert!(is_builtin(&b, 2, 0, "crate"));
|
||||
assert!(b.get(0, 0).is_none());
|
||||
assert!(b.get((0, 0)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -58,7 +58,7 @@ fn push_blocked_by_wall_moves_nothing() {
|
||||
game.try_move(Direction::East);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (0, 0));
|
||||
assert_eq!(b.player_pos(), Point { x: 0, y: 0 });
|
||||
assert!(is_builtin(&b, 1, 0, "crate"));
|
||||
assert!(is_builtin(&b, 2, 0, "wall"));
|
||||
}
|
||||
@@ -72,7 +72,7 @@ fn push_blocked_by_edge_moves_nothing() {
|
||||
game.try_move(Direction::East);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (0, 0));
|
||||
assert_eq!(b.player_pos(), Point { x: 0, y: 0 });
|
||||
assert!(is_builtin(&b, 1, 0, "crate"));
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ fn cascade_pushes_two_crates() {
|
||||
game.try_move(Direction::East);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (1, 0));
|
||||
assert_eq!(b.player_pos(), Point { x: 1, y: 0 });
|
||||
assert!(is_builtin(&b, 2, 0, "crate"));
|
||||
assert!(is_builtin(&b, 3, 0, "crate"));
|
||||
}
|
||||
@@ -100,7 +100,7 @@ fn cascade_blocked_by_wall_moves_nothing() {
|
||||
game.try_move(Direction::East);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (0, 0));
|
||||
assert_eq!(b.player_pos(), Point { x: 0, y: 0 });
|
||||
assert!(is_builtin(&b, 1, 0, "crate"));
|
||||
assert!(is_builtin(&b, 2, 0, "crate"));
|
||||
}
|
||||
@@ -116,7 +116,7 @@ fn player_pushes_pushable_solid_object() {
|
||||
game.try_move(Direction::East);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (1, 0));
|
||||
assert_eq!(b.player_pos(), Point { x: 1, y: 0 });
|
||||
assert_eq!(
|
||||
b.get_hookable(id).expect("object still on the board").location(),
|
||||
(2, 0)
|
||||
@@ -132,7 +132,7 @@ fn hcrate_pushes_east_but_not_north() {
|
||||
game.try_move(Direction::East);
|
||||
{
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (1, 0));
|
||||
assert_eq!(b.player_pos(), Point { x: 1, y: 0 });
|
||||
assert!(is_builtin(&b, 2, 0, "hcrate"));
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ fn hcrate_pushes_east_but_not_north() {
|
||||
game.try_move(Direction::North);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (0, 3));
|
||||
assert_eq!(b.player_pos(), Point { x: 0, y: 3 });
|
||||
assert!(is_builtin(&b, 0, 2, "hcrate"));
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ fn vcrate_pushes_north_but_not_east() {
|
||||
game.try_move(Direction::North);
|
||||
{
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (0, 2));
|
||||
assert_eq!(b.player_pos(), Point { x: 0, y: 2 });
|
||||
assert!(is_builtin(&b, 0, 1, "vcrate"));
|
||||
}
|
||||
|
||||
@@ -168,6 +168,6 @@ fn vcrate_pushes_north_but_not_east() {
|
||||
game.try_move(Direction::East);
|
||||
|
||||
let b = game.board();
|
||||
assert_eq!(b.player_pos(), (0, 0));
|
||||
assert_eq!(b.player_pos(), Point { x: 0, y: 0 });
|
||||
assert!(is_builtin(&b, 1, 0, "vcrate"));
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::board::Board;
|
||||
use crate::board::tests::{object_at, open_board, plain_object_at, sensor_at};
|
||||
use crate::game::{GameState, ScrollLine};
|
||||
use crate::tile::EnterResponse;
|
||||
use crate::utils::{Direction, ObjectId};
|
||||
use crate::utils::{Direction, ObjectId, Point};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -313,7 +313,7 @@ fn player_bump_reports_the_direction_it_came_from() {
|
||||
);
|
||||
game.run_init();
|
||||
game.try_move(Direction::East);
|
||||
assert_eq!(game.board().player_pos(), (0, 0)); // blocked
|
||||
assert_eq!(game.board().player_pos(), Point { x: 0, y: 0 }); // blocked
|
||||
// log() bypasses the queue, so no tick is needed to flush it.
|
||||
assert!(log_texts(&game).iter().any(|t| t == "bumped from West"));
|
||||
}
|
||||
@@ -538,6 +538,6 @@ fn player_walking_onto_a_sensor_fires_enter_from_the_travel_side() {
|
||||
game.run_init();
|
||||
game.try_move(Direction::East);
|
||||
// A sensor is off-grid, so it never blocks: the player moves onto the cell.
|
||||
assert_eq!(game.board().player_pos(), (1, 0));
|
||||
assert_eq!(game.board().player_pos(), Point { x: 1, y: 0 });
|
||||
assert!(log_texts(&game).iter().any(|t| t == "entered from West"));
|
||||
}
|
||||
|
||||
+11
-14
@@ -6,7 +6,7 @@ use crate::builtin::BUILTIN_SOURCES;
|
||||
use crate::floor::{Floor, FloorBiome};
|
||||
use crate::glyph::Glyph;
|
||||
use crate::object_def::ObjectDef;
|
||||
use crate::utils::{ObjectId, Pushable};
|
||||
use crate::utils::{ObjectId, Point, Pushable};
|
||||
use crate::utils::Pushable::No;
|
||||
|
||||
/// The various ways that a tile might respond to another tile trying to move on top of it
|
||||
@@ -198,10 +198,8 @@ pub struct ScriptAttributes {
|
||||
/// affect board movement, as it doesn't live in the grid.
|
||||
#[derive(Clone)]
|
||||
pub struct Sensor {
|
||||
/// Where it is on the board: x coord
|
||||
pub x: usize,
|
||||
/// Where it is on the board: y coord
|
||||
pub y: usize,
|
||||
/// Where it is on the board:
|
||||
pub location: Point,
|
||||
/// Whether this is drawn above or below the grid
|
||||
pub draw_layer: DrawLayer,
|
||||
/// Sensors have scripting ability
|
||||
@@ -211,8 +209,8 @@ pub struct Sensor {
|
||||
/// The serialized representation of a Sensor. Can be turned into a Sensor, or vice versa
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct SensorSpec {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
#[serde(flatten)]
|
||||
pub location: Point,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub script: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Glyph::is_transparent")]
|
||||
@@ -232,8 +230,7 @@ impl SensorSpec {
|
||||
let id = *next_object_id;
|
||||
*next_object_id += 1;
|
||||
Sensor {
|
||||
x: self.x,
|
||||
y: self.y,
|
||||
location: self.location,
|
||||
draw_layer: self.draw_layer,
|
||||
scripting: ScriptAttributes {
|
||||
id,
|
||||
@@ -250,7 +247,7 @@ impl SensorSpec {
|
||||
|
||||
pub trait Hookable {
|
||||
fn scriptable(&self) -> &ScriptAttributes;
|
||||
fn location(&self) -> (usize, usize);
|
||||
fn location(&self) -> Point;
|
||||
fn id(&self) -> ObjectId {
|
||||
self.scriptable().id
|
||||
}
|
||||
@@ -282,21 +279,21 @@ impl Hookable for &Sensor {
|
||||
fn scriptable(&self) -> &ScriptAttributes {
|
||||
&self.scripting
|
||||
}
|
||||
fn location(&self) -> (usize, usize) {
|
||||
(self.x, self.y)
|
||||
fn location(&self) -> Point {
|
||||
self.location
|
||||
}
|
||||
fn solid(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LocatedObject<'a>(pub &'a ObjectDef, pub (usize, usize));
|
||||
pub struct LocatedObject<'a>(pub &'a ObjectDef, pub Point);
|
||||
|
||||
impl Hookable for LocatedObject<'_> {
|
||||
fn scriptable(&self) -> &ScriptAttributes {
|
||||
&self.0.scripting
|
||||
}
|
||||
fn location(&self) -> (usize, usize) {
|
||||
fn location(&self) -> Point {
|
||||
self.1
|
||||
}
|
||||
fn solid(&self) -> bool {
|
||||
|
||||
+16
-5
@@ -3,7 +3,6 @@ use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use rhai::{Dynamic, Engine};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::keys::Keyring;
|
||||
use crate::log::LogLine;
|
||||
use crate::script::Registerable;
|
||||
|
||||
@@ -122,8 +121,9 @@ impl Direction {
|
||||
}
|
||||
|
||||
/// Translate the given point in this direction
|
||||
pub fn from_point(self, x: i64, y: i64) -> (i64, i64) {
|
||||
(x + self.dx(), y + self.dy())
|
||||
pub fn from_point<P: Into<Point>>(self, p: P) -> Point {
|
||||
let p = p.into();
|
||||
(p.x + self.dx(), p.y + self.dy()).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,11 +131,11 @@ impl Registerable for Direction {
|
||||
fn register(engine: &mut Engine, _log_sink: LogSink) {
|
||||
engine.register_type_with_name::<Direction>("Direction")
|
||||
.register_get("opposite", |dir: &mut Direction| dir.opposite())
|
||||
.register_fn("from_point", |dir: &mut Direction, x: i64, y: i64| Point::from(dir.from_point(x, y)));
|
||||
.register_fn("from_point", |dir: &mut Direction, x: i64, y: i64| Point::from(dir.from_point((x, y))));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Point {
|
||||
pub x: i64,
|
||||
pub y: i64,
|
||||
@@ -149,10 +149,18 @@ impl From<(usize, usize)> for Point {
|
||||
fn from((x, y): (usize, usize)) -> Self { Self { x: x as i64, y: y as i64 } }
|
||||
}
|
||||
|
||||
impl From<(i32, i32)> for Point {
|
||||
fn from((x, y): (i32, i32)) -> Self { Self { x: x as i64, y: y as i64 } }
|
||||
}
|
||||
|
||||
impl Into<(i64, i64)> for Point {
|
||||
fn into(self) -> (i64, i64) { (self.x, self.y) }
|
||||
}
|
||||
|
||||
impl Into<(usize, usize)> for Point {
|
||||
fn into(self) -> (usize, usize) { (self.x as usize, self.y as usize) }
|
||||
}
|
||||
|
||||
impl Display for Point {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "({}, {})", self.x, self.y)
|
||||
@@ -162,6 +170,9 @@ impl Display for Point {
|
||||
impl Point {
|
||||
pub fn ux(&self) -> usize { self.x as usize }
|
||||
pub fn uy(&self) -> usize { self.y as usize }
|
||||
pub fn in_dir(self, dir: Direction) -> Self {
|
||||
dir.from_point(self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Registerable for Point {
|
||||
|
||||
@@ -157,11 +157,11 @@ mod tests {
|
||||
|
||||
let copy = world.deep_clone();
|
||||
// Stamp a wall into the copy's board.
|
||||
copy.boards["start"].borrow_mut().place(1, 0, Some(TileSpec::wall())).expect("could not place wall");
|
||||
copy.boards["start"].borrow_mut().place((1, 0), Some(TileSpec::wall())).expect("could not place wall");
|
||||
|
||||
// The copy changed; the original is still empty at that cell.
|
||||
assert_matches!(copy.boards["start"].borrow().get(1, 0), Some(Tile::Object(_)));
|
||||
assert!(world.boards["start"].borrow().get(1, 0).is_none());
|
||||
assert_matches!(copy.boards["start"].borrow().get((1, 0)), Some(Tile::Object(_)));
|
||||
assert!(world.boards["start"].borrow().get((1, 0)).is_none());
|
||||
// And they are genuinely different allocations.
|
||||
assert!(!Rc::ptr_eq(&world.boards["start"], ©.boards["start"]));
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ impl EditorState {
|
||||
/// cursor, applying the placement rules (see [`Board::place_archetype`]).
|
||||
pub(crate) fn place_current(&mut self) {
|
||||
let (x, y) = (self.cursor.0 as usize, self.cursor.1 as usize);
|
||||
let res = self.board_mut().place(x, y, Some(TileSpec::Builtin { kind: self.current_archetype.to_string(), glyph: Some(self.current_glyph) }));
|
||||
let res = self.board_mut().place((x, y), Some(TileSpec::Builtin { kind: self.current_archetype.to_string(), glyph: Some(self.current_glyph) }));
|
||||
if let Err(e) = res {
|
||||
self.log.push(LogLine::error(e))
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ pub struct BoardWidget<'a> {
|
||||
impl<'a> BoardWidget<'a> {
|
||||
/// Creates a widget that renders `board`, scrolling to follow the player.
|
||||
pub fn new(board: &'a Board) -> Self {
|
||||
let (x, y) = board.player_pos();
|
||||
let focus = (x as i32, y as i32);
|
||||
let p = board.player_pos();
|
||||
let focus = (p.x as i32, p.y as i32);
|
||||
Self { board, focus, fov: None }
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ impl Widget for BoardWidget<'_> {
|
||||
let visible = self.fov.is_none_or(|l| l.is_visible(bx, by));
|
||||
if let Some(cell) = buf.cell_mut((sx, sy)) {
|
||||
if visible {
|
||||
let glyph = board.glyph_at(bx, by);
|
||||
let glyph = board.glyph_at((bx, by));
|
||||
// Modulate by lighting when the board is dark; pass through otherwise.
|
||||
let (fg, bg) = match self.fov {
|
||||
Some(l) => (l.tint(bx, by, glyph.fg), l.tint(bx, by, glyph.bg)),
|
||||
|
||||
@@ -117,8 +117,8 @@ impl Widget for SpeechBubblesWidget<'_> {
|
||||
let (px, py, player_vis) = board_screen_pos(
|
||||
area,
|
||||
self.board,
|
||||
player.0,
|
||||
player.1,
|
||||
player.ux(),
|
||||
player.uy(),
|
||||
);
|
||||
let player = player_vis.then_some((px, py));
|
||||
|
||||
@@ -128,7 +128,7 @@ impl Widget for SpeechBubblesWidget<'_> {
|
||||
.iter()
|
||||
.filter_map(|b| {
|
||||
let obj = self.board.get_hookable(b.object_id)?;
|
||||
let (x, y) = obj.location();
|
||||
let (x, y) = obj.location().into();
|
||||
let (sx, sy, on_screen) = board_screen_pos(area, self.board, x, y);
|
||||
// On a dark board, a speaker the player can't see is treated like an
|
||||
// off-screen speaker: the box still draws, but its tail is suppressed.
|
||||
@@ -426,8 +426,8 @@ fn center_top(obj_sy: u16, box_h: u16, area: Rect) -> u16 {
|
||||
/// `on_screen` is `false` when clamping occurred (tail should be suppressed for that bubble).
|
||||
fn board_screen_pos(area: Rect, board: &Board, bx: usize, by: usize) -> (u16, u16, bool) {
|
||||
let player = board.player_pos();
|
||||
let (off_x, pad_x, _) = BoardWidget::axis(board.width, area.width as usize, player.0 as i32);
|
||||
let (off_y, pad_y, _) = BoardWidget::axis(board.height, area.height as usize, player.1 as i32);
|
||||
let (off_x, pad_x, _) = BoardWidget::axis(board.width, area.width as usize, player.x as i32);
|
||||
let (off_y, pad_y, _) = BoardWidget::axis(board.height, area.height as usize, player.y as i32);
|
||||
let raw_sx = area.x as i32 + pad_x as i32 + (bx as i32 - off_x as i32);
|
||||
let raw_sy = area.y as i32 + pad_y as i32 + (by as i32 - off_y as i32);
|
||||
let sx = raw_sx.clamp(area.left() as i32, area.right() as i32 - 1) as u16;
|
||||
|
||||
Reference in New Issue
Block a user