From 38c58a2c5ff7e51552640edb6db4603801b52196 Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Tue, 11 Aug 2026 01:02:38 -0500 Subject: [PATCH] wip 2 pointifying --- kiln-core/src/action.rs | 10 +- kiln-core/src/api/board.rs | 10 +- kiln-core/src/api/object_info.rs | 22 +-- kiln-core/src/api/player.rs | 4 +- kiln-core/src/board.rs | 172 +++--------------- kiln-core/src/board_spec.rs | 4 +- kiln-core/src/floor.rs | 6 +- kiln-core/src/game.rs | 93 ++++------ kiln-core/src/portal.rs | 11 +- kiln-core/src/tests/actions.rs | 8 +- kiln-core/src/tests/game_portals.rs | 6 +- kiln-core/src/tests/map_file/fill_sparse.rs | 4 +- kiln-core/src/tests/map_file/grid_errors.rs | 2 +- .../src/tests/map_file/object_placement.rs | 2 +- .../src/tests/map_file/player_placement.rs | 4 +- kiln-core/src/tests/map_file/pushers.rs | 2 +- kiln-core/src/tests/map_file/round_trip.rs | 6 +- kiln-core/src/tests/movement.rs | 30 +-- kiln-core/src/tests/scripting.rs | 6 +- kiln-core/src/tile.rs | 25 ++- kiln-core/src/utils.rs | 21 ++- kiln-core/src/world.rs | 6 +- kiln-tui/src/editor.rs | 2 +- kiln-tui/src/render.rs | 6 +- kiln-tui/src/speech.rs | 10 +- 25 files changed, 163 insertions(+), 309 deletions(-) diff --git a/kiln-core/src/action.rs b/kiln-core/src/action.rs index 558b1e0..c0a5b63 100644 --- a/kiln-core/src/action.rs +++ b/kiln-core/src/action.rs @@ -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(()) } \ No newline at end of file diff --git a/kiln-core/src/api/board.rs b/kiln-core/src/api/board.rs index 0f34464..83b1729 100644 --- a/kiln-core/src/api/board.rs +++ b/kiln-core/src/api/board.rs @@ -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) diff --git a/kiln-core/src/api/object_info.rs b/kiln-core/src/api/object_info.rs index 85e3ddb..60e82f9 100644 --- a/kiln-core/src/api/object_info.rs +++ b/kiln-core/src/api/object_info.rs @@ -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, 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") - .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) }); } } \ No newline at end of file diff --git a/kiln-core/src/api/player.rs b/kiln-core/src/api/player.rs index 34791f3..37f6224 100644 --- a/kiln-core/src/api/player.rs +++ b/kiln-core/src/api/player.rs @@ -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); } } diff --git a/kiln-core/src/board.rs b/kiln-core/src/board.rs index e3646d2..0e196da 100644 --- a/kiln-core/src/board.rs +++ b/kiln-core/src/board.rs @@ -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>(&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>(&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>(&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, String> { + pub fn apply_shift(&mut self, cells: &[Point]) -> Result, 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] diff --git a/kiln-core/src/board_spec.rs b/kiln-core/src/board_spec.rs index b64bc61..981d30b 100644 --- a/kiln-core/src/board_spec.rs +++ b/kiln-core/src/board_spec.rs @@ -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::>(); diff --git a/kiln-core/src/floor.rs b/kiln-core/src/floor.rs index 869c8b7..d62e032 100644 --- a/kiln-core/src/floor.rs +++ b/kiln-core/src/floor.rs @@ -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 { + pub(crate) fn glyph_at>(&self, p: P, width: usize) -> Option { + 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(), } } } diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index c0dfdb3..1402295 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -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::>(); 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>(&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] diff --git a/kiln-core/src/portal.rs b/kiln-core/src/portal.rs index 060fb42..cd41450 100644 --- a/kiln-core/src/portal.rs +++ b/kiln-core/src/portal.rs @@ -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 } - -impl Portal { - pub fn location(&self) -> (usize, usize) { - (self.x, self.y) - } -} diff --git a/kiln-core/src/tests/actions.rs b/kiln-core/src/tests/actions.rs index 5b5f92e..243776d 100644 --- a/kiln-core/src/tests/actions.rs +++ b/kiln-core/src/tests/actions.rs @@ -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] diff --git a/kiln-core/src/tests/game_portals.rs b/kiln-core/src/tests/game_portals.rs index 121dbf3..77a2624 100644 --- a/kiln-core/src/tests/game_portals.rs +++ b/kiln-core/src/tests/game_portals.rs @@ -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"); } diff --git a/kiln-core/src/tests/map_file/fill_sparse.rs b/kiln-core/src/tests/map_file/fill_sparse.rs index 6884437..60fe207 100644 --- a/kiln-core/src/tests/map_file/fill_sparse.rs +++ b/kiln-core/src/tests/map_file/fill_sparse.rs @@ -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); } diff --git a/kiln-core/src/tests/map_file/grid_errors.rs b/kiln-core/src/tests/map_file/grid_errors.rs index f02581d..7205e59 100644 --- a/kiln-core/src/tests/map_file/grid_errors.rs +++ b/kiln-core/src/tests/map_file/grid_errors.rs @@ -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) ); } diff --git a/kiln-core/src/tests/map_file/object_placement.rs b/kiln-core/src/tests/map_file/object_placement.rs index 3544fc3..0ec75c8 100644 --- a/kiln-core/src/tests/map_file/object_placement.rs +++ b/kiln-core/src/tests/map_file/object_placement.rs @@ -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] diff --git a/kiln-core/src/tests/map_file/player_placement.rs b/kiln-core/src/tests/map_file/player_placement.rs index 43c79f7..3154cff 100644 --- a/kiln-core/src/tests/map_file/player_placement.rs +++ b/kiln-core/src/tests/map_file/player_placement.rs @@ -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()); } diff --git a/kiln-core/src/tests/map_file/pushers.rs b/kiln-core/src/tests/map_file/pushers.rs index 459c3a3..f95e232 100644 --- a/kiln-core/src/tests/map_file/pushers.rs +++ b/kiln-core/src/tests/map_file/pushers.rs @@ -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" ); diff --git a/kiln-core/src/tests/map_file/round_trip.rs b/kiln-core/src/tests/map_file/round_trip.rs index cf5a5e7..d1ce18e 100644 --- a/kiln-core/src/tests/map_file/round_trip.rs +++ b/kiln-core/src/tests/map_file/round_trip.rs @@ -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))); } diff --git a/kiln-core/src/tests/movement.rs b/kiln-core/src/tests/movement.rs index 57e8f12..8c5eaf4 100644 --- a/kiln-core/src/tests/movement.rs +++ b/kiln-core/src/tests/movement.rs @@ -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")); } diff --git a/kiln-core/src/tests/scripting.rs b/kiln-core/src/tests/scripting.rs index 5351b9c..34ff1f9 100644 --- a/kiln-core/src/tests/scripting.rs +++ b/kiln-core/src/tests/scripting.rs @@ -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")); } diff --git a/kiln-core/src/tile.rs b/kiln-core/src/tile.rs index 5fc33e6..edbcd69 100644 --- a/kiln-core/src/tile.rs +++ b/kiln-core/src/tile.rs @@ -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, #[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 { diff --git a/kiln-core/src/utils.rs b/kiln-core/src/utils.rs index af65552..46ace7a 100644 --- a/kiln-core/src/utils.rs +++ b/kiln-core/src/utils.rs @@ -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>(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") .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 { diff --git a/kiln-core/src/world.rs b/kiln-core/src/world.rs index 4a4ef71..8eecb6d 100644 --- a/kiln-core/src/world.rs +++ b/kiln-core/src/world.rs @@ -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"])); } diff --git a/kiln-tui/src/editor.rs b/kiln-tui/src/editor.rs index 1c66c7f..5ab7682 100644 --- a/kiln-tui/src/editor.rs +++ b/kiln-tui/src/editor.rs @@ -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)) } diff --git a/kiln-tui/src/render.rs b/kiln-tui/src/render.rs index ab7d0aa..78f7e96 100644 --- a/kiln-tui/src/render.rs +++ b/kiln-tui/src/render.rs @@ -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)), diff --git a/kiln-tui/src/speech.rs b/kiln-tui/src/speech.rs index ae9c220..6b9e5bd 100644 --- a/kiln-tui/src/speech.rs +++ b/kiln-tui/src/speech.rs @@ -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;