Files
kiln/kiln-core/src/scripts/spinner.rhai
T

99 lines
3.9 KiB
Plaintext
Raw Normal View History

2026-06-21 01:32:47 -05:00
// Rotates this object's 8 neighbours one step around a ring every 0.5s, shoving
// each pushable into the next ring slot via a single simultaneous `swap`. The spin
// direction comes from the `BUILTIN_spinner_*` tag the map loader attaches (it
// defaults to clockwise when no tag is present).
//
// A neighbour may only rotate if its destination slot will actually be free: the
// slot is a hole (`passable`), or that slot's own occupant also rotates out. We
// can't decide that with `can_shift` alone — `can_shift(j) == false` is ambiguous
// between "j is empty" and "j is a blocked solid" — so we seed with `can_shift`
// and then cascade-demote using `passable` to spot the genuine holes.
fn tick(dt) {
// Only start a new rotation when the previous one (and its delay) has drained,
// exactly like the built-in pusher's pacing.
if Queue.length() != 0 { return; }
// Direction from the builtin tag; clockwise unless explicitly counter-clockwise.
let cw = !Me.has_tag("BUILTIN_spinner_ccw");
let ox = Me.x;
let oy = Me.y;
// The 8 neighbour offsets in clockwise order, starting at North.
let ring = [
[ 0, -1], // N
[ 1, -1], // NE
[ 1, 0], // E
[ 1, 1], // SE
[ 0, 1], // S
[-1, 1], // SW
[-1, 0], // W
[-1, -1], // NW
];
// The cardinal step that carries each ring cell to the *next* slot in the spin
// direction, and the index offset to that slot (+1 clockwise, -1 == +7 counter).
// CW: N->NE is East, NE->E is South, ... CCW: N->NW is West, NE->N is West, ...
let dirs = if cw {
[East, South, South, West, West, North, North, East]
} else {
[West, West, North, North, East, East, South, South]
};
let step = if cw { 1 } else { 7 };
// Animate the glyph one frame per rotation (changing only the character): the
// line spins '/'-'\'-, slash-swapped for counter-clockwise. Frame state lives in
// the board Registry, keyed per spinner, since script scope resets each tick.
let frames = if cw { [47, 0xc4, 92, 0xb3] } else { [92, 0xc4, 47, 0xb3] };
let fkey = `spin_${Me.id}`;
let f = Registry.get_or(fkey, 0);
set_tile(frames[f % 4]);
Registry.set(fkey, (f + 1) % 4);
// moves[i]: will ring cell i rotate into its destination slot? Seed with
// can_shift, which is true only when cell i holds a pushable whose immediate
// next cell isn't a wall (so it could move *if* that next cell ends up free).
let moves = [];
for i in 0..8 {
let sx = ox + ring[i][0];
let sy = oy + ring[i][1];
moves.push(can_shift(sx, sy, dirs[i]));
}
// Cascade: a cell may only move if its destination is a hole (`passable`), or
// the occupant of that destination also moves. Demote until stable. A broken
// arc collapses back from the jam; a full pushable cycle never demotes (every
// destination's occupant moves) and rotates whole via swap's cycle handling.
loop {
let changed = false;
for i in 0..8 {
let n = (i + step) % 8;
let dx = ox + ring[n][0];
let dy = oy + ring[n][1];
if moves[i] && !passable(dx, dy) && !moves[n] {
moves[i] = false;
changed = true;
}
}
if !changed { break; }
}
// Emit the surviving rotations as one simultaneous batch, then pace to twice
// a second. Coordinates are pulled into locals first to keep each expression
// simple (Rhai caps expression complexity).
let batch = [];
for i in 0..8 {
if moves[i] {
let n = (i + step) % 8;
let sx = ox + ring[i][0];
let sy = oy + ring[i][1];
let dx = ox + ring[n][0];
let dy = oy + ring[n][1];
batch.push([sx, sy, dx, dy]);
}
}
swap(batch);
delay(0.5);
}