Resting faces steeper than the angle of repose topple: blown sand towers collapse (t93)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 23:07:30 -04:00
co-authored by Claude Opus 5.5
parent 84dd8b8d0a
commit bd22e453ca
10 changed files with 264 additions and 24 deletions
+68 -19
View File
@@ -78,9 +78,75 @@ const SORTP_FUEL = 0.003;
// World generation settles with both at 1 and corner sealing off, so seeds keep the same world.
let LSTEP = 8, LFILM = 4;
let CORNER_SEAL = true, POWDER_FAN = true;
// Every spill checks each crossed cell, including barriers outside the source chunk.
function powderSpill(i, x, y, m, dx, limit, avalanche) {
let reach = 0;
for (let step = 1; step <= limit; step++) {
const nx = x + dx * step, side = i + dx * step, j = side + W;
if (nx < 0 || nx >= W || mat[side] !== EMPTY || part[side] || cleatMask[side] ||
!passable(m, PH_POWDER, side, dx, 0)) break;
if (mat[j] === EMPTY && !part[j] && !cleatMask[j] && passable(m, PH_POWDER, j, dx, 1)) reach = step;
}
if (!reach) return false;
let drop = 1;
for (let step = 2; step <= limit && y + step < H; step++) {
const j = i + W * step + dx * reach;
if (mat[j] !== EMPTY || part[j] || cleatMask[j] || !passable(m, PH_POWDER, j, 0, 1)) break;
drop = step;
}
if (avalanche) avalancheMove(i, x, y, dx, reach, drop);
else move(i, i + W * drop + dx * reach, x, y, x + dx * reach, y + drop);
return true;
}
// Removing support passes the collapse direction up the column; contact cancels it.
function avalancheMove(i, x, y, dx, reach, drop) {
const fl = F_AVALANCHE | (dx > 0 ? F_AVALANCHE_R : 0);
if (y && PH[mat[i - W]] === PH_POWDER && STICK[mat[i - W]] === 1) flags[i - W] = (flags[i - W] & ~F_AVALANCHE_R) | fl;
flags[i] |= fl;
move(i, i + dx * reach + W * drop, x, y, x + dx * reach, y + drop);
}
function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
if (y >= H - 1) return;
const b = i + W;
if (flags[i] & F_AVALANCHE) {
const dx = flags[i] & F_AVALANCHE_R ? 1 : -1;
flags[i] &= ~(F_AVALANCHE | F_AVALANCHE_R);
let reach = 0;
for (let step = 1; step <= 2 && mat[b] === EMPTY; step++) {
const nx = x + dx * step, j = i + dx * step;
if (nx < 0 || nx >= W || mat[j] !== EMPTY || part[j] || cleatMask[j] || !passable(m, PH_POWDER, j, dx, 0)) break;
reach = step;
}
let drop = 0;
for (let step = 1; step <= 8 && y + step < H; step++) {
const j = i + dx * reach + W * step;
if (mat[j] !== EMPTY || part[j] || cleatMask[j] || !passable(m, PH_POWDER, j, 0, 1)) break;
drop = step;
}
if (drop && mat[b] === EMPTY && !part[i] && !cleatMask[i] && !(flags[i] & F_BURN)) {
avalancheMove(i, x, y, dx, reach, drop); return;
}
}
// Tall free-flowing bodies topple from rest; short feeds and cohesive grains keep their repose.
if (POWDER_FAN && STICK[m] === 1 && (mat[b] === EMPTY || PH[mat[b]] === PH_POWDER) && !part[i] && !((flags[i] | flags[b]) & F_BURN) && x > 0 && x < W - 1) {
const dx = mat[i - 1] === EMPTY && PH[mat[i + 1]] === PH_POWDER ? -1 :
mat[i + 1] === EMPTY && PH[mat[i - 1]] === PH_POWDER ? 1 : 0;
if (dx && !cleatMask[i] && ((PH[mat[b]] === PH_POWDER && y + 2 < H && mat[b + W + dx] === EMPTY) ||
(y > 0 && PH[mat[i - W]] === PH_POWDER && mat[i - W + dx] === EMPTY && PH[mat[b - dx]] === PH_POWDER))) {
// Read at most 24 rows away: with eight-grain writes, same-colour chunks stay independent.
const insideX = x - dx * 8;
let depth = 0;
if (insideX >= 0 && insideX < W) {
const inside = i - dx * 8;
let up = 0;
while (up < 24 && y >= up && PH[mat[inside - W * up]] === PH_POWDER) up++;
let down = 1;
while (down <= 24 && up + down <= 32 && y + down < H && PH[mat[inside + W * down]] === PH_POWDER) down++;
depth = up + down - 1;
}
if (depth >= 32 && powderSpill(i, x, y, m, dx, 8, true)) return;
}
}
if ((!barriers || (!cleatMask[i] && !cleatMask[b])) && enterFree(m, PH_POWDER, b, 0, 1)) {
// A falling face fans outward; dropping straight down keeps a packed column in its way.
if (POWDER_FAN && mat[b] === EMPTY && !part[i] && !(flags[i] & F_BURN) && x > 0 && x < W - 1) {
@@ -91,25 +157,8 @@ function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
// Deep faces spill across the falling column, within the eight-grain worker halo.
let depth = 0;
while (depth < 12 && y >= depth && PH[mat[i - dx - W * depth]] === PH_POWDER) depth++;
let reach = 0;
for (let step = 1; step <= (depth === 12 && stamp[b - dx] !== tickStamp ? 8 : 1); step++) {
const nx = x + dx * step, side = i + dx * step, j = side + W;
if (nx < 0 || nx >= W || mat[side] !== EMPTY || part[side] ||
cleatMask[side] || !passable(m, PH_POWDER, side, dx, 0)) break;
if (mat[j] === EMPTY && !part[j] && !cleatMask[j] &&
passable(m, PH_POWDER, j, dx, 1)) reach = step;
}
if (reach) {
let drop = 1;
if (depth === 12 && stamp[b - dx] !== tickStamp) {
for (let step = 2; step <= 8 && y + step < H; step++) {
const j = i + W * step + dx * reach;
if (mat[j] !== EMPTY || part[j] || cleatMask[j] || !passable(m, PH_POWDER, j, 0, 1)) break;
drop = step;
}
}
move(i, i + W * drop + dx * reach, x, y, x + dx * reach, y + drop); return;
}
const reach = depth === 12 && stamp[b - dx] !== tickStamp ? 8 : 1;
if (powderSpill(i, x, y, m, dx, reach, false)) return;
}
}
move(i, b, x, y, x, y + 1); return;
+1
View File
@@ -28,6 +28,7 @@ const SOLIDMETAL = new Uint8Array(NMAT); for (const m of [COPPER, IRON, STEEL, T
// ---------- Flags & parts ----------
const F_NAT = 1, F_CAST = 2, F_BURN = 4;
const F_AVALANCHE = 8, F_AVALANCHE_R = 16; // transient outward fall, cleared on contact
const P_NONE = 0, P_SCREW_U = 1, P_SCREW_D = 2, P_SCREW_L = 3, P_SCREW_R = 4, P_GRATE = 5, P_GOPEN = 6, P_WHEEL = 7, P_INTAKE = 8, P_FILTER = 9, P_PIPE = 10, P_SCREW_UR = 11, P_SCREW_DR = 12, P_SCREW_DL = 13, P_SCREW_UL = 14;
const SCREW_PARTS = [P_SCREW_U, P_SCREW_R, P_SCREW_D, P_SCREW_L, P_SCREW_UR, P_SCREW_DR, P_SCREW_DL, P_SCREW_UL];
const SCREW_DIRS = [[0, -1], [1, 0], [0, 1], [-1, 0], [1, -1], [1, 1], [-1, 1], [-1, -1]];