wip 2 pointifying

This commit is contained in:
2026-08-11 01:02:38 -05:00
parent 3b5bf21ac5
commit 38c58a2c5f
25 changed files with 163 additions and 309 deletions
+5 -5
View File
@@ -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(())
}
+1 -9
View File
@@ -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)
+7 -15
View File
@@ -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)
});
}
}
+2 -2
View File
@@ -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
View File
@@ -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]
+2 -2
View File
@@ -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<_>>();
+4 -2
View File
@@ -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
View File
@@ -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]
+3 -8
View File
@@ -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)
}
}
+4 -4
View File
@@ -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]
+3 -3
View File
@@ -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");
}
+2 -2
View File
@@ -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);
}
+1 -1
View File
@@ -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());
}
+1 -1
View File
@@ -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"
);
+3 -3
View File
@@ -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)));
}
+15 -15
View File
@@ -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 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+3 -3
View File
@@ -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"], &copy.boards["start"]));
}
+1 -1
View File
@@ -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))
}
+3 -3
View File
@@ -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)),
+5 -5
View File
@@ -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;