diff --git a/kiln-core/src/api/board.rs b/kiln-core/src/api/board.rs index 83b1729..cc599e5 100644 --- a/kiln-core/src/api/board.rs +++ b/kiln-core/src/api/board.rs @@ -8,8 +8,8 @@ //! //! ## Cell queries //! -//! - `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) +//! - `board.passable(x, y) -> bool` / `board.passable(point) -> bool` — is the cell on-board +//! and free of any solid (a hole)? use std::cell::RefCell; use std::rc::Rc; @@ -18,7 +18,8 @@ use crate::{Board, Direction}; use crate::api::object_info::ObjectInfo; use crate::api::registry::Registry; use crate::script::Registerable; -use crate::utils::{LogSink, ObjectId}; +use crate::tile::Tile; +use crate::utils::{LogSink, ObjectId, Point}; /// A read-only handle to the world, exposed to scripts as `Board`. pub type BoardRef = Rc>; @@ -37,6 +38,18 @@ impl Registerable for BoardRef { board.in_bounds((x, y)) && board.is_empty((x, y)) }); + // The Point overload of the same query, so a script that already holds a + // cell (from `dir.from_point(…)` / `me.location`) needn't unpack it. + engine.register_fn("passable", move |board: BoardRef, p: Point| -> bool { + let board = board.borrow(); + board.in_bounds(p) && board.is_empty(p) + }); + + engine.register_fn("mobile", move |board: BoardRef, p: Point| -> bool { + let board = board.borrow(); + board.in_bounds(p) && board.is_mobile(p) + }); + // Board.get(id) -> ObjectInfo | () (unknown id logs error) engine.register_fn("get", move |board: &mut BoardRef, id: i64| -> Dynamic { if id <= 0 { @@ -72,5 +85,16 @@ impl Registerable for BoardRef { // Board.registry -> Registry engine.register_get("registry", |b: &mut BoardRef| Registry(b.clone())); + + // Board.get(id) -> ObjectInfo | () (unknown id logs error) + engine.register_fn("id_at", move |board: &mut BoardRef, point: Point| -> Dynamic { + let board = board.borrow(); + if !board.in_bounds(point) { return Dynamic::UNIT } + match board.get(point) { + Some(Tile::Object(obj)) => Dynamic::from(obj.scripting.id as i64), + Some(Tile::Player) => Dynamic::from(-1i64), + None => Dynamic::UNIT + } + }); } } diff --git a/kiln-core/src/api/object_info.rs b/kiln-core/src/api/object_info.rs index 60e82f9..5a98349 100644 --- a/kiln-core/src/api/object_info.rs +++ b/kiln-core/src/api/object_info.rs @@ -3,6 +3,7 @@ //! ### Getters //! //! - x, y -> Board location of object +//! - location -> The same cell as a single Point //! - id -> The object's board-unique id //! - name -> The object's name, or () //! - waiting -> bool for whether or not the front of this object's queue is a delay action @@ -74,6 +75,9 @@ impl Registerable for ObjectInfo { engine.register_type_with_name::("ObjectInfo") .register_get("x", |obj: &mut ObjectInfo| obj.location.x) .register_get("y", |obj: &mut ObjectInfo| obj.location.y) + // The same cell as a single `Point`, for the coordinate-taking API + // (`dir.from_point(me.location)`, `Board.passable(o.location)`, `shift`). + .register_get("location", |obj: &mut ObjectInfo| obj.location) .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()); diff --git a/kiln-core/src/board.rs b/kiln-core/src/board.rs index c9490cd..cba2229 100644 --- a/kiln-core/src/board.rs +++ b/kiln-core/src/board.rs @@ -488,6 +488,15 @@ impl Board { pub fn is_object>(&self, p: P) -> bool { matches!(self.get(p), Some(Tile::Object(_))) } + + /// Return if there's an object at the given point and if it's mobile + pub fn is_mobile>(&self, p: P) -> bool { + if let Some(Tile::Object(o)) = self.get(p) { + o.mobile + } else { + false + } + } } #[cfg(test)] diff --git a/kiln-core/src/game.rs b/kiln-core/src/game.rs index 65462c2..9e86c3e 100644 --- a/kiln-core/src/game.rs +++ b/kiln-core/src/game.rs @@ -296,7 +296,6 @@ impl GameState { } Action::Push { x, y, 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::>(); @@ -412,60 +411,10 @@ 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)); - // Call the target's bump hook and resolve whatever it did let actions = self.scripts.run_bump(id, dir.opposite()); self.apply_actions(actions); - // First do things to try and call any hooks relevant: - // let actions = match resp { - // // We block all moves, return false - // EnterResponse::Block => { self.scripts.run_bump(id, dir.opposite()) } - // - // // The player can grab things directly, so let's do that - // EnterResponse::Grab if from_player => { - // let a = self.scripts.run_grab(id); - // self.board_mut().get_mut((target.0, target.1)).take(); - // a - // } - // - // // Grabbables not from the player act as pushable, we need to recurse - // EnterResponse::Grab => { self.resolve_move(target, dir); vec![] } - // - // // Pushable if we're allowed to push that way recurses - // EnterResponse::Push(p) if p.allows(dir) => { self.resolve_move(target, dir); vec![] } - // - // // Pushable in a disallowed direction blocks - // EnterResponse::Push(p) => { self.scripts.run_bump(id, dir.opposite()) } - // - // // Swaps let the player swap places, but we should return false afterward because we - // // haven't left the source cell empty. In practice this won't matter because right - // // now we never recurse _into_ a player-source-cell, all moves are initiated by the - // // player, but still. - // EnterResponse::Swap if from_player => { - // let mut board = self.board_mut(); - // let mover = board.get_mut((from.0, from.1)).take(); - // let swapper = board.get_mut((target.0, target.1)).take(); - // board.get_mut((target.0, target.1)).replace(mover.unwrap()); - // board.get_mut((from.0, from.1)).replace(swapper.unwrap()); - // vec![] - // } - // - // // Swaps in a chain are just normal pushable, recurse: - // EnterResponse::Swap => { self.resolve_move(target, dir); vec![] } - // - // // Squish just lets the mover overwrite, and always leaves the source cell empty: - // EnterResponse::Squish => { self.board_mut().get_mut((from.0, from.1)).take(); vec![] } - // - // // Finally, hook, we need to call a hook and let it do its thing: - // EnterResponse::Hook => { self.scripts.run_bump(id, dir.opposite()) } - // }; - - // If there were actions performed by the hooks, do them. - // self.apply_actions(actions); - // Now, check again if the target cell is empty. If it is, put the mover there. Also // check that the source cell still contains something! It may have been teleported away. if !self.board().is_empty(from) && self.board().is_empty(target) { diff --git a/kiln-core/src/script.rs b/kiln-core/src/script.rs index 405e196..0c62f42 100644 --- a/kiln-core/src/script.rs +++ b/kiln-core/src/script.rs @@ -632,9 +632,10 @@ fn register_write_api(engine: &mut Engine, board: BoardRef, log_sink: LogSink) { } ); - // shift([[x, y], ...): Emits a shift action which moves the contents of each given cell in + // shift([[x, y], ...]): Emits a shift action which moves the contents of each given cell in // a loop (the last cell is moved to the first coord). Doesn't move things that aren't pushable, // and won't move anything into a cell that's not vacant (or vacated by this shift). + // Each entry may be a `[x, y]` pair or a `Point`. let b = board.clone(); let sink = log_sink.clone(); engine.register_fn("shift", move |ctx: NativeCallContext, arr: Array| { @@ -647,11 +648,17 @@ fn register_write_api(engine: &mut Engine, board: BoardRef, log_sink: LogSink) { }); } -/// Read a `Vec<(i32, i32)>` from a Rhai array, to receive a list of coordinates from a script. -/// Returns Err if the array isn't `[[x, y], ...]` +/// Read a `Vec<(i64, i64)>` from a Rhai array, to receive a list of coordinates from a script. +/// Each element may be a `Point` or an `[x, y]` pair; returns `Err` for anything else. fn read_coord_array(arr: &Array) -> Result, ()> { let mut pairs = Vec::new(); for elem in arr { + // A `Point` (from `me.location`, `dir.from_point(…)`, `xy(…)`) is taken as-is; + // otherwise the element must be a two-int array. + if let Some(p) = elem.read_lock::() { + pairs.push((*p).into()); + continue; + } let coords: Option> = elem.read_lock::().and_then(|inner| { inner.iter().map(|v| v.as_int().ok()).collect() }); diff --git a/kiln-core/src/scripts/transporter.rhai b/kiln-core/src/scripts/transporter.rhai index 92662b6..f5ae8aa 100644 --- a/kiln-core/src/scripts/transporter.rhai +++ b/kiln-core/src/scripts/transporter.rhai @@ -2,45 +2,51 @@ // // A transporter is a solid, see-through, unpushable machine that teleports // whatever bumps into it from its facing side. It animates through a 4-frame -// loop (500 ms/frame) and, on a front bump, moves the bumper either to the cell -// just past it or — if that's blocked — out of the nearest opposite-facing -// transporter further along its axis (skipping any whose entrance is blocked). +// loop and, on a front bump, moves the bumper either to the cell just past it +// or — if that's blocked — out of the far side of the nearest opposite-facing +// transporter along its axis (skipping any whose far side is blocked too). // // The direction is not baked into this source: every transporter shares one // compiled copy and reads its `BUILTIN_transporter_` tag. The world is -// reached through the global `Board`/`Player` constants. The bumper is moved by -// coordinate via `shift([[from], [to]])`, which relocates whatever solid sits at +// reached through the global `Board` constant. The bumper is moved by +// coordinate via `shift([from, to])`, which relocates whatever solid sits at // the entrance — player, object, or a pushed crate — onto the empty destination. -// This transporter's facing unit vector, from its direction tag. +// The builtin tag worn by a transporter facing `d`. +fn tag_for(d) { "BUILTIN_transporter_" + d.to_string().to_lower() } + +// This transporter's facing direction, from its tag (west if it somehow has none). fn facing(me) { - if me.has_tag("BUILTIN_transporter_north") { [0, -1] } - else if me.has_tag("BUILTIN_transporter_south") { [0, 1] } - else if me.has_tag("BUILTIN_transporter_east") { [1, 0] } - else { [-1, 0] } + for d in [North, South, East, West] { + if me.has_tag(tag_for(d)) { return d; } + } + West } -// The tag of the opposite-facing transporter this one pairs with. -fn opposite_tag(me) { - if me.has_tag("BUILTIN_transporter_north") { "BUILTIN_transporter_south" } - else if me.has_tag("BUILTIN_transporter_south") { "BUILTIN_transporter_north" } - else if me.has_tag("BUILTIN_transporter_east") { "BUILTIN_transporter_west" } - else { "BUILTIN_transporter_east" } +// The 4-frame animation loop for a facing direction. +fn frames(d) { + switch d.to_string() { + "North" => ["^", "-", "^", "~"], + "South" => ["v", "_", "v", "-"], + "East" => [")", "|", ")", ">"], + _ => ["(", "|", "(", "<"], + } } -// The 4-frame animation loop for this direction. -fn frames(me) { - if me.has_tag("BUILTIN_transporter_north") { ["^", "-", "^", "~"] } - else if me.has_tag("BUILTIN_transporter_south") { ["v", "_", "v", "-"] } - else if me.has_tag("BUILTIN_transporter_east") { [")", "|", ")", ">"] } - else { ["(", "|", "(", "<"] } +// Move whatever solid sits at `from` onto the empty cell `to`, immediately. A +// two-cell `shift` relocates the source solid — player, object, or crate — and +// leaves its old cell empty (the caller has already checked `to` is free). +fn transport(my_dir, traveler, to) { + push(to.x, to.y, my_dir); + teleport(traveler, to.x, to.y); + now(); now(); } fn tick(me, dt) { - // Advance one animation frame every 0.5s, like the spinner: frame state lives + // Advance one animation frame per drain, like the spinner: frame state lives // in the board Registry (script scope resets each tick), keyed per instance. if me.waiting { return; } - let fs = frames(me); + let fs = frames(facing(me)); let fkey = `xport_${me.id}`; let f = Board.registry.get_or(fkey, 0); set_tile(fs[f % 4]); @@ -48,65 +54,45 @@ fn tick(me, dt) { me.delay(0.15); } -// Move whatever solid sits at (sx, sy) onto the empty cell (tx, ty) immediately. -// A two-cell `shift` relocates the source solid — player, object, or crate — and -// leaves its old cell empty (the destination is checked empty by the caller). Kept -// as a helper so the nested-array literal stays at a shallow expression depth. -fn transport(sx, sy, tx, ty) { - shift([[sx, sy], [tx, ty]]); now(); -} - fn bump(me, dir) { let d = facing(me); - let dx = d[0]; - let dy = d[1]; - // Only transport things that hit us from the front — the entrance side (-facing). - // `dir` is the side the bump came from, so its offset must be the opposite of our - // facing; a bump from any other side does nothing. - if dir.dx != -dx || dir.dy != -dy { return; } + // Only transport things that hit us from the front. `dir` is the side the bump + // came from, so a front bump arrives from our entrance side — the opposite of + // our facing; a bump from any other side does nothing. + if dir != d.opposite { return; } - // Whatever solid is pressed against our entrance cell (me - d) gets moved: - // the player, another object, or a pushed crate — all handled uniformly by - // shifting the solid at that coordinate onto an empty destination. - let ex = me.x - dx; - let ey = me.y - dy; + // Whatever solid is pressed against our entrance cell gets moved: the player, + // another object, or a pushed crate — all handled uniformly by shifting the + // solid at that coordinate onto an empty destination. + let entrance = dir.from_point(me.location); + let traveler = Board.id_at(entrance); - // 1. The cell just past us (the opposite side): if nothing solid is there, - // drop the bumper onto it. - let fx = me.x + dx; - let fy = me.y + dy; - if Board.passable(fx, fy) { - transport(ex, ey, fx, fy); + // 1. The cell just past us (our facing side): if nothing solid is there, drop + // the bumper onto it. + let exit = d.from_point(me.location); + if Board.passable(exit) || Board.mobile(exit) { + transport(d, traveler, exit); return; } - // 2. Otherwise scan along our axis for the nearest opposite-facing transporter - // whose far side (the cell just past it) is free, and drop the bumper there. - // A blocked pair is skipped; give up at the board edge. - let opp = Board.tagged(opposite_tag(me)); - let cx = fx; - let cy = fy; - loop { - cx += dx; - cy += dy; - if cx < 0 || cy < 0 || cx >= Board.width || cy >= Board.height { - return; - } - // Is one of the opposite transporters sitting at (cx, cy)? - let here = false; - for o in opp { - if o.x == cx && o.y == cy { here = true; } - } - if here { - // Emerge on the paired transporter's far side — the cell just past it - // along our scan (cx + d), out its back. - let tx = cx + dx; - let ty = cy + dy; - if Board.passable(tx, ty) { - transport(ex, ey, tx, ty); - return; - } - } + // 2. Otherwise emerge from the *nearest* opposite-facing transporter ahead of + // us on our axis whose own far side is free; a blocked pair is skipped, and + // nothing happens if no pair qualifies. Walls between us don't matter — we + // only look at where the paired transporters are, not the cells in between. + let best = (); + let best_dist = 0; + for o in Board.tagged(tag_for(d.opposite)) { + // Split its offset from us into distance along our facing and distance to + // the side, so "on our axis, in front of us" is `aside == 0 && ahead > 0`. + let ahead = (o.x - me.x) * d.dx + (o.y - me.y) * d.dy; + let aside = (o.x - me.x) * d.dy - (o.y - me.y) * d.dx; + if aside != 0 || ahead < 1 { continue; } + if best_dist != 0 && ahead > best_dist { continue; } + + // Emerge out its back — the cell just past it along our scan. + best = d.from_point(o.location); + best_dist = ahead; } -} + if best_dist > 0 { transport(d, traveler, best); } +} \ No newline at end of file diff --git a/kiln-core/src/utils.rs b/kiln-core/src/utils.rs index 46ace7a..85c87e2 100644 --- a/kiln-core/src/utils.rs +++ b/kiln-core/src/utils.rs @@ -131,7 +131,10 @@ 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)))) + // The Point overload, so a script can walk a coordinate along an axis + // (`p = d.from_point(p)`) without unpacking it into two ints. + .register_fn("from_point", |dir: &mut Direction, p: Point| dir.from_point(p)); } } @@ -179,8 +182,17 @@ impl Registerable for Point { fn register(engine: &mut Engine, _log_sink: LogSink) { engine.register_type_with_name::("Point") .register_get("x", |p: &mut Point| p.x) - .register_get("y", |p: &mut Point| p.y); + .register_get("y", |p: &mut Point| p.y) + // A step from this point, the mirror of `dir.from_point(p)`. + .register_fn("in_dir", |p: &mut Point, dir: Direction| p.in_dir(dir)); engine.register_fn("xy", |x: i64, y: i64| Point::from((x, y))); + // Rhai derives no operators for custom types: register comparison so cells + // can be matched (`if o.location == here`), and printing so a Point + // interpolates as "(x, y)" in a log message. + engine.register_fn("==", |a: Point, b: Point| a == b); + engine.register_fn("!=", |a: Point, b: Point| a != b); + engine.register_fn("to_string", |p: Point| p.to_string()); + engine.register_fn("to_debug", |p: Point| p.to_string()); } } /// A value that can be stored in a board's script registry across board transitions.