2026-07-24 21:52:05 -05:00
|
|
|
// Built-in script for the `transporter_*` archetypes (see builtin).
|
2026-07-08 10:06:25 -05:00
|
|
|
//
|
|
|
|
|
// 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).
|
|
|
|
|
//
|
|
|
|
|
// The direction is not baked into this source: every transporter shares one
|
|
|
|
|
// compiled copy and reads its `BUILTIN_transporter_<dir>` tag. The world is
|
2026-07-08 13:21:27 -05:00
|
|
|
// reached through the global `Board`/`Player` constants. 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.
|
2026-07-08 10:06:25 -05:00
|
|
|
|
|
|
|
|
// This transporter's facing unit vector, from its direction tag.
|
|
|
|
|
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] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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" }
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 23:29:43 -05:00
|
|
|
// The 4-frame animation loop for this direction.
|
2026-07-08 10:06:25 -05:00
|
|
|
fn frames(me) {
|
2026-07-26 23:29:43 -05:00
|
|
|
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 { ["(", "|", "(", "<"] }
|
2026-07-08 10:06:25 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn tick(me, dt) {
|
|
|
|
|
// Advance one animation frame every 0.5s, 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 fkey = `xport_${me.id}`;
|
|
|
|
|
let f = Board.registry.get_or(fkey, 0);
|
|
|
|
|
set_tile(fs[f % 4]);
|
|
|
|
|
Board.registry.set(fkey, (f + 1) % 4);
|
|
|
|
|
me.delay(0.15);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 13:21:27 -05:00
|
|
|
// 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) {
|
2026-07-08 10:06:25 -05:00
|
|
|
let d = facing(me);
|
|
|
|
|
let dx = d[0];
|
|
|
|
|
let dy = d[1];
|
|
|
|
|
|
2026-07-09 00:18:40 -05:00
|
|
|
// 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; }
|
|
|
|
|
|
2026-07-08 13:21:27 -05:00
|
|
|
// 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;
|
2026-07-08 10:06:25 -05:00
|
|
|
|
|
|
|
|
// 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) {
|
2026-07-08 13:21:27 -05:00
|
|
|
transport(ex, ey, fx, fy);
|
2026-07-08 10:06:25 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Otherwise scan along our axis for the nearest opposite-facing transporter
|
2026-07-08 13:21:27 -05:00
|
|
|
// 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.
|
2026-07-08 10:06:25 -05:00
|
|
|
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 {
|
2026-07-08 13:21:27 -05:00
|
|
|
// 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);
|
2026-07-08 10:06:25 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|