wip 5 transporters work right
This commit is contained in:
@@ -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<RefCell<Board>>;
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>("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());
|
||||
|
||||
@@ -488,6 +488,15 @@ impl Board {
|
||||
pub fn is_object<P: Into<Point>>(&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<P: Into<Point>>(&self, p: P) -> bool {
|
||||
if let Some(Tile::Object(o)) = self.get(p) {
|
||||
o.mobile
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -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::<Vec<Point>>();
|
||||
@@ -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) {
|
||||
|
||||
+10
-3
@@ -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<Vec<(i64, i64)>, ()> {
|
||||
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::<Point>() {
|
||||
pairs.push((*p).into());
|
||||
continue;
|
||||
}
|
||||
let coords: Option<Vec<i64>> = elem.read_lock::<Array>().and_then(|inner| {
|
||||
inner.iter().map(|v| v.as_int().ok()).collect()
|
||||
});
|
||||
|
||||
@@ -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_<dir>` 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); }
|
||||
}
|
||||
+14
-2
@@ -131,7 +131,10 @@ 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))))
|
||||
// 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>("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.
|
||||
|
||||
Reference in New Issue
Block a user