Shared join module for transport pieces; belt drawing uses it (t67 phase 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 13:00:43 -04:00
co-authored by Claude Opus 5.5
parent 787721ad35
commit 65338b2b27
11 changed files with 274 additions and 38 deletions
+1
View File
@@ -19,6 +19,7 @@
"sim/racks.js",
"sim/pipes.js",
"sim/machine-tick.js",
"sim/joins.js",
"sim/conveyors.js",
"sim/slope-conveyors.js",
"sim/blocks.js",
+2 -2
View File
@@ -116,8 +116,8 @@ function thinPipeColor(x, y, anim) {
}
PC[0] = r; PC[1] = g; PC[2] = bl;
}
// a block that is a belt or a filter: they join up into one belt
function beltLike(b) { if (b < 0 || b >= NB) return false; const c = blockP[b]; return isBelt(c) || isFilter(c); }
// A filter frame opens only where this end has a geometric partner.
function beltLike(b, end) { return endJoin(b % BW, (b / BW) | 0, end) >= 0; }
// standing trees: the trunk shows its dark bark, so it reads apart from the pale cut wood it drops
const TREE_R = new Uint8Array(NMAT), TREE_G = new Uint8Array(NMAT), TREE_B = new Uint8Array(NMAT);
for (let m = 0; m < NMAT; m++) { TREE_R[m] = COLR[m]; TREE_G[m] = COLG[m]; TREE_B[m] = COLB[m]; }
+6 -25
View File
@@ -2,36 +2,17 @@
let slopeBeltJoins = new Uint8Array(0), slopeBeltHalo = new Uint8Array(0), beltCaps = new Uint8Array(0);
let slopeAcross = 0, slopeAlong = 0, slopeDistance = 0;
function slopeBeltConnections(bx, by, code, planned = null) {
const grade = beltGrade(code);
let joins = 0;
for (let end = 0; end < 2; end++) {
const side = end ? 1 : -1, nx = bx + side, ny = by + side * grade;
const nb = ny * BW + nx, next = planned && planned.get(nb);
if (nx >= 0 && nx < BW && ny >= 0 && ny < BH && beltGrade(next ? next[0] : blockP[nb]) === grade) {
joins |= 4 << end; continue;
}
const fy = by + (end ? grade > 0 : grade < 0), fb = fy * BW + nx;
if (nx < 0 || nx >= BW || fy < 0 || fy >= BH) continue;
const flat = planned && planned.get(fb), c = flat ? flat[0] : blockP[fb], v = flat ? flat[1] : blockV[fb];
if (c === BP_BELT || c === BP_FILTER || (c === BP_SCREW && (v === 1 || v === 3))) joins |= 1 << end;
const join = endJoin(bx, by, end, planned);
if (join < 0) continue;
const nb = join >> 1, next = planned && planned.get(nb);
joins |= (isSlopeBelt(next ? next[0] : blockP[nb]) ? 4 : 1) << end;
}
return joins;
}
function flatBeltCaps(b) {
const bx = b % BW, by = (b / BW) | 0;
let caps = 0;
for (let end = 0; end < 2; end++) {
const x = bx + (end ? 1 : -1);
let joined = false;
if (x >= 0 && x < BW) for (let dy = -1; dy <= 0; dy++) {
if (by + dy < 0) continue;
const n = (by + dy) * BW + x, c = blockP[n], g = beltGrade(c);
if ((!dy && (c === BP_BELT || c === BP_FILTER || (c === BP_SCREW && (blockV[n] === 1 || blockV[n] === 3 || blockV[n] >= 4)))) ||
(g && dy === -(end ? g < 0 : g > 0))) joined = true;
}
if (!joined) caps |= 1 << end;
}
return caps;
function flatBeltCaps(b, planned = null) {
return 3 ^ pieceJoins(b % BW, (b / BW) | 0, planned);
}
function prepareSlopeBelts(x0, y0, x1, y1) {
if (slopeBeltJoins.length !== NB) { slopeBeltJoins = new Uint8Array(NB); slopeBeltHalo = new Uint8Array(NB); beltCaps = new Uint8Array(NB); }
+3 -2
View File
@@ -201,7 +201,7 @@ function drawWorld() {
} else if (ly === 3) { // the belt's return run carries on under it, so belts and filters read as one belt
const st = ((x - dir * (anim >> 1)) >> 1) & 1;
if (m === EMPTY || st) { r = (st ? 92 : 56) * 0.72; g = (st ? 96 : 58) * 0.72; b = (st ? 104 : 66) * 0.72; }
} else if ((lx === 0 && !beltLike(fb - 1)) || (lx === 3 && !beltLike(fb + 1))) { if (m === EMPTY || ((x + y) & 1)) { r = 64; g = 66; b = 74; } } // frame only where the line ends
} else if ((lx === 0 && !beltLike(fb, 0)) || (lx === 3 && !beltLike(fb, 1))) { if (m === EMPTY || ((x + y) & 1)) { r = 64; g = 66; b = 74; } } // frame only where the line ends
else if (m === EMPTY) { // mesh holes, tinted with what falls through them
if ((x + y) & 1) { r = 40; g = 42; b = 48; }
else if (inv) { r = 58; g = 74; b = 72; } // carry mode: everything else drops
@@ -341,6 +341,7 @@ function drawWorld() {
else if (isWall(code) && WALL_OF[v]) { const wm = WALL_OF[v]; tr = (COLR[wm] + 90) / 2; tg = (COLG[wm] + 190) / 2; tb = (COLB[wm] + 230) / 2; }
const slope = isSlopeBelt(code), diagonal = code === BP_SCREW && v >= 4;
const joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : diagonal ? diagonalScrewJoins(gbx, gby, v, ghostBlocks) : 0;
const caps = code === BP_BELT || code === BP_FILTER ? flatBeltCaps(b, ghostBlocks) : 3;
for (let dy = slope ? -9 : diagonal ? -4 : 0; dy < (slope || diagonal ? 8 : BS); dy++) for (let dx = slope ? -5 : diagonal ? -4 : 0; dx < (slope ? 9 : diagonal ? 8 : BS); dx++) {
let sd = 0;
if (diagonal) {
@@ -365,7 +366,7 @@ function drawWorld() {
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
const k = sy * bw + sx, c = px[k];
const edge = slope || diagonal ? sd > -0.8 : dx === 0 || dy === 0 || dx === BS - 1 || dy === BS - 1;
const edge = slope || diagonal ? sd > -0.8 : (dx === 0 && (caps & 1)) || dy === 0 || (dx === BS - 1 && (caps & 2)) || dy === BS - 1;
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28;
const pr = c & 255, pg = (c >> 8) & 255, pb = (c >> 16) & 255;
px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
+71
View File
@@ -0,0 +1,71 @@
// Geometric transport ends. Tables are built once; querying a world or plan allocates nothing.
// Ends are left/right, except vertical screws (up/down). Travel direction is irrelevant.
const JOIN_ENDS = [[], ...[
['screw-horizontal', 1, 0], ['screw-vertical', 0, 1],
['screw-diagonal', 1, -1], ['screw-diagonal', 1, 1],
['belt-flat', 1, 0], ['belt-sloped', 1, -1], ['belt-sloped', 1, 1]
].map(([kind, dx, dy]) => [-1, 1].map(s => ({
dx: s * dx, dy: s * dy, diagonal: !!(dx && dy),
high: !!(dx && dy && s * dy < 0), low: !!(dx && dy && s * dy > 0), kind,
targets: []
})))];
function pieceEnds(code, v) {
if (code === BP_SCREW) {
if (v < 0 || v > 7) return JOIN_ENDS[0];
return JOIN_ENDS[v < 4 ? (v & 1 ? 1 : 2) : (SCREW_DIRS[v][0] * SCREW_DIRS[v][1] < 0 ? 3 : 4)];
}
if (code === BP_BELT || code === BP_FILTER) return JOIN_ENDS[5];
return isSlopeBelt(code) ? JOIN_ENDS[beltGrade(code) < 0 ? 6 : 7] : JOIN_ENDS[0];
}
// Compile the diagonal-end rule and its inverse into bounded neighbour probes.
for (const ends of JOIN_ENDS) for (const end of ends) {
for (const others of JOIN_ENDS) for (let n = 0; n < others.length; n++) {
const other = others[n], screw = end.kind.startsWith('screw'), otherScrew = other.kind.startsWith('screw');
if (screw !== otherScrew && (end.kind === 'screw-vertical' || other.kind === 'screw-vertical' ||
(end.diagonal && other.diagonal))) continue;
let dx, dy;
if (end.diagonal && other.diagonal) {
if (screw !== otherScrew || end.dx !== -other.dx || end.dy !== -other.dy) continue;
dx = end.dx; dy = end.dy;
} else if (end.diagonal) {
if (other.dx ? other.dx !== -end.dx : other.dy !== -end.dy) continue;
dx = end.dx; dy = other.dx && end.high ? 0 : end.dy;
} else if (other.diagonal) {
if (end.dx ? end.dx !== -other.dx : end.dy !== -other.dy) continue;
dx = -other.dx; dy = end.dx && other.high ? 0 : -other.dy;
} else {
if (end.dx !== -other.dx || end.dy !== -other.dy) continue;
dx = end.dx; dy = end.dy;
}
end.targets.push({ dx, dy, ends: others, end: n });
}
// Prefer a continuing diagonal run to an elbow when both are present.
end.targets.sort((a, b) => Number(b.ends[b.end].diagonal) - Number(a.ends[a.end].diagonal));
}
// Result: neighbour block index * 2 + its end index; -1 means an open end.
function joinCandidate(bx, by, endIndex, planned) {
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return -1;
const b = by * BW + bx, plan = planned && planned.get(b);
const ends = pieceEnds(plan ? plan[0] : blockP[b], plan ? plan[1] : blockV[b]);
const end = ends[endIndex];
if (!end) return -1;
for (let k = 0; k < end.targets.length; k++) {
const target = end.targets[k];
const x = bx + target.dx, y = by + target.dy;
if (x < 0 || y < 0 || x >= BW || y >= BH) continue;
const nb = y * BW + x, next = planned && planned.get(nb);
if (pieceEnds(next ? next[0] : blockP[nb], next ? next[1] : blockV[nb]) === target.ends)
return nb * 2 + target.end;
}
return -1;
}
function endJoin(bx, by, endIndex, planned = null) {
const join = joinCandidate(bx, by, endIndex, planned);
if (join < 0) return -1;
const nb = join >> 1;
// Competing neighbours cannot claim an end that chose a different partner.
return joinCandidate(nb % BW, (nb / BW) | 0, join & 1, planned) === (by * BW + bx) * 2 + endIndex ? join : -1;
}
function pieceJoins(bx, by, planned = null) {
return (endJoin(bx, by, 0, planned) >= 0 ? 1 : 0) | (endJoin(bx, by, 1, planned) >= 0 ? 2 : 0);
}
+1 -1
View File
@@ -110,7 +110,7 @@ function drawPreviews() {
if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; }
const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0);
const afford = canAfford(pieceCostNow(P), cells.length);
const planned = code === BP_SCREW && dIdx >= 4 ? new Map(cells.map(([x, y]) => [y * BW + x, [BP_SCREW, dIdx]])) : null;
const planned = (code === BP_SCREW && dIdx >= 4) || isSlopeBelt(code) ? new Map(cells.map(([x, y]) => [y * BW + x, [code, dIdx]])) : null;
for (const [bx, by] of cells) {
const err = canPlaceBlock(bx, by, code), rb = by * BW + bx;
const swap = GAME.replace && !!blockP[rb] && /already built/.test(err) && !blockIs(rb, code, v, pieceExtra(P));