Joins follow the travel direction: the next piece goes right after the end (t67 phase 2b)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 14:09:28 -04:00
co-authored by Claude Opus 5.5
parent fd35bf334f
commit 45538cc18e
16 changed files with 363 additions and 236 deletions
+2 -1
View File
@@ -14,7 +14,8 @@ function diagonalScrewJoins(bx, by, v, planned = null) {
const code = next ? next[0] : blockP[nb], nv = next ? next[1] : blockV[nb];
if (pieceEnds(code, nv)[join & 1].diagonal) continue;
const horizontal = code !== BP_SCREW || (nv & 1);
const kind = ((nb / BW) | 0) === by ? (end ? 2 : 4) : horizontal ? 5 : 6;
const ny = (nb / BW) | 0, nx = nb % BW;
const kind = ny === by ? (nx > bx ? 2 : 4) : nx === bx ? (ny < by ? 1 : 3) : horizontal ? 5 : 6;
joins |= kind << (end * 3);
}
return joins;
+19 -4
View File
@@ -23,13 +23,27 @@ vec3 slopeShape(vec2 p, vec2 worldOrigin, float grade, int joins) {
vec2 axis = vec2(1.0, grade) * 0.70710678;
vec3 best = slopeStroke(p, a, b, dot(worldOrigin + a, axis));
if ((joins & 1) != 0) {
vec3 h = slopeStroke(p, vec2(-2.0, a.y), a, worldOrigin.x - 2.0);
vec3 h;
if ((joins & 128) != 0 && grade < 0.0) {
h = slopeStroke(p, vec2(-2, 2), vec2(0, 2), worldOrigin.x - 2.0);
if (h.x < best.x) best = h;
h = slopeStroke(p, vec2(0, 2), vec2(0, a.y), worldOrigin.x);
if (h.x < best.x) best = h;
h = slopeStroke(p, vec2(0, a.y), a, worldOrigin.x);
} else h = slopeStroke(p, vec2(-2.0, a.y), a, worldOrigin.x - 2.0);
if (h.x < best.x) best = h;
h = slopeCorner(p, a, worldOrigin.x + a.x);
if (h.x < best.x) best = h;
}
if ((joins & 2) != 0) {
vec3 h = slopeStroke(p, b, vec2(6, b.y), worldOrigin.x + b.x);
vec3 h;
if ((joins & 128) != 0 && grade > 0.0) {
h = slopeStroke(p, b, vec2(4, b.y), worldOrigin.x + b.x);
if (h.x < best.x) best = h;
h = slopeStroke(p, vec2(4, b.y), vec2(4, 2), worldOrigin.x + 4.0);
if (h.x < best.x) best = h;
h = slopeStroke(p, vec2(4, 2), vec2(6, 2), worldOrigin.x + 4.0);
} else h = slopeStroke(p, b, vec2(6, b.y), worldOrigin.x + b.x);
if (h.x < best.x) best = h;
h = slopeCorner(p, b, worldOrigin.x + b.x);
if (h.x < best.x) best = h;
@@ -77,8 +91,9 @@ const SM_SLOPE_DRAW = `
float grade = (shape == 0 || shape == 2) ? -1.0 : 1.0;
float dir = ((slopeClass - 23) % 2) == 1 ? 1.0 : -1.0;
vec4 tag = cellAt(tTree, c);
int joins = int(tag.a * 255.0 + 0.5);
vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w);
int packedX = int(tag.g * 255.0 + 0.5);
int joins = (int(tag.a * 255.0 + 0.5) & 127) | (packedX & 128);
vec2 local = vec2(float(packedX & 127), floor(tag.b * 255.0 + 0.5)) - 8.0 + fract(w);
vec3 shapePos = slopeShape(local, w - local, grade, joins);
float sd = shapePos.x, axis = shapePos.y, depth = shapePos.z;
float time = !mesh && (joins & 16) != 0 ? uTime : 0.0;
+15 -3
View File
@@ -7,7 +7,11 @@ function slopeBeltConnections(bx, by, code, planned = null) {
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;
const slope = isSlopeBelt(next ? next[0] : blockP[nb]) &&
((nb / BW) | 0) - by === beltGrade(code) * (nb % BW - bx);
joins |= (slope ? 4 : 1) << end;
if (!slope && beltGrade(code) * (end ? 1 : -1) > 0 &&
((nb / BW) | 0) === by) joins |= 128; // low-end feed drops from the same-row flat
}
return joins;
}
@@ -56,11 +60,19 @@ function slopeBeltShape(x, y, b, joins = slopeBeltJoins[b], code = blockP[b]) {
const origin = (ox + grade * oy) * Math.SQRT1_2;
slopeBeltSegment(x, y, ax, ay, bx, by, origin + (ax + grade * ay) * Math.SQRT1_2);
if (joins & 1) {
slopeBeltSegment(x, y, -2, ay, ax, ay, ox - 2);
if ((joins & 128) && grade < 0) {
slopeBeltSegment(x, y, -2, 2, 0, 2, ox - 2);
slopeBeltSegment(x, y, 0, 2, 0, ay, ox);
slopeBeltSegment(x, y, 0, ay, ax, ay, ox);
} else slopeBeltSegment(x, y, -2, ay, ax, ay, ox - 2);
slopeBeltCorner(x, y, ax, ay, ox + ax);
}
if (joins & 2) {
slopeBeltSegment(x, y, bx, by, 6, by, ox + bx);
if ((joins & 128) && grade > 0) {
slopeBeltSegment(x, y, bx, by, 4, by, ox + bx);
slopeBeltSegment(x, y, 4, by, 4, 2, ox + 4);
slopeBeltSegment(x, y, 4, 2, 6, 2, ox + 4);
} else slopeBeltSegment(x, y, bx, by, 6, by, ox + bx);
slopeBeltCorner(x, y, bx, by, ox + bx);
}
return slopeDistance;
+2 -1
View File
@@ -279,8 +279,9 @@ function drawWorld() {
const slopeClass = 23 + (code - BP_BELT_UP) * 2 + dir;
if (slopeArt) cl = slopeClass;
const f = isFilter(code) && filterCfg.get(sb), fm = f && f.m.length ? f.m[(Math.floor((x + beltGrade(code) * y) / 2) & 65535) % f.m.length] : 0;
// Alpha bit 128 marks slope tags; the spare high bit of local X carries the drop.
tpx[row + bx] = ((slopeBeltJoins[sb] | 128) << 24) | slopeClass |
((y - ((sb / BW) | 0) * BS + 8) << 16) | ((x - (sb % BW) * BS + 8) << 8);
((y - ((sb / BW) | 0) * BS + 8) << 16) | (((x - (sb % BW) * BS + 8) | (slopeBeltJoins[sb] & 128)) << 8);
if (slopeArt) { r = fm ? COLR[fm] : 0; g = fm ? COLG[fm] : 0; b = fm ? COLB[fm] : 0; }
}
if (slopeDetail && slopeWallOwner >= 0) {
+4 -16
View File
@@ -185,22 +185,10 @@ function straightScrewNext(i, p) {
const dx = SCREW_DX[p], dy = SCREW_DY[p], x = i % W, y = (i / W) | 0;
const xx = x + dx, yy = y + dy, next = yy * W + xx;
if ((xx >> 2) === (x >> 2) && (yy >> 2) === (y >> 2)) return next;
const end = dx + dy > 0 ? 1 : 0;
const q = part[next], nb = (yy >> 2) * BW + (xx >> 2);
if (blockP[nb] && !(SCREW_DX[q] && SCREW_DY[q])) return next;
const join = endJoin(x >> 2, y >> 2, end);
if (join >= 0) {
const target = join >> 1, v = blockV[target];
if (blockP[target] === BP_SCREW && v >= 4) {
if (target === nb) return next;
if (!blockP[nb]) {
const lane = dx ? y & 3 : x & 3;
const tx = target % BW * BS + (join & 1 ? BS - 1 : 0);
return (((target / BW) | 0) * BS + lane) * W + tx;
}
}
}
return SCREW_DX[q] && SCREW_DY[q] ? -1 : next;
const q = part[next];
if (!(SCREW_DX[q] && SCREW_DY[q])) return next;
const join = endJoin(x >> 2, y >> 2, dx + dy > 0 ? 1 : 0);
return join >= 0 && (join >> 1) === (yy >> 2) * BW + (xx >> 2) ? next : -1;
}
// Four lanes cross a block in four beats; only a mutual geometric join can receive them.
function diagonalScrewNext(i, p) {
+18 -8
View File
@@ -93,13 +93,6 @@ function entScrewTick(e, p) {
const frontX = e.ix + (dx > 0 ? BAR_W - 1 : 0), frontY = e.iy + (dy > 0 ? BAR_H - 1 : 0);
const front = frontY * W + frontX, dest = straightScrewNext(front, p);
if (dest < 0) return;
if (dest !== front + dx + dy * W) {
const q = part[dest];
if (!screwAccepts(q, dx, dy)) return;
const x = (dest % W) & ~3, y = entScrewLane((dest / W) | 0, e.iy);
if (entScrewMove(e, x, y, true)) stats.moved++;
return;
}
let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0;
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const i = entScrewEdge(e, dx, dy, k);
@@ -149,6 +142,23 @@ function entDiagonalScrewTick(e, p) {
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
if (entScrewMove(e, x, y, false, dy < 0)) { e.vx = dx * 0.5; e.vy = dy * 0.6; stats.moved++; }
}
// A same-row flat belt drops its bar into a joined screw, just as grains fall off its face.
function entBeltScrewEnter(e) {
if (e.ix & 3) return false;
const y = e.iy + BAR_H;
if (y >= H || (y & 3)) return false;
const b = (y >> 2) * BW + (e.ix >> 2), c = blockP[b], v = blockV[b];
if (c !== BP_BELT && c !== BP_FILTER) return false;
if (c === BP_FILTER && filterPassAt(e.m, y * W + e.ix)) return false;
if (netSat(y * W + e.ix) <= 0) return false;
const d = c === BP_FILTER ? (v & 128 ? 1 : -1) : (v & 1 ? 1 : -1);
const join = endJoin(b % BW, (b / BW) | 0, d > 0 ? 1 : 0);
if (join < 0 || blockP[join >> 1] !== BP_SCREW) return false;
e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
const nb = join >> 1;
if (!(tick & 1) && entScrewMove(e, nb % BW * BS, ((nb / BW) | 0) * BS, true, true)) stats.moved++;
return true;
}
// Bars stay level; their highest contact follows the face one grain per belt beat.
function entSlopeBeltTick(e) {
const bottom = e.iy + BAR_H;
@@ -188,7 +198,7 @@ function entTick() {
let ix = e.ix; const iy = e.iy;
const screw = entScrewAt(ix, iy);
if (screw) entScrewTick(e, screw);
else if (!entSlopeBeltTick(e)) {
else if (!entBeltScrewEnter(e) && !entSlopeBeltTick(e)) {
// what is underneath?
let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H;
if (!support) {
+41 -39
View File
@@ -1,46 +1,48 @@
// 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];
// Travel-directed transport ends. Precomputed probes; world/plan queries allocate nothing.
// End indices stay geometric: left/right, or up/down for vertical screws.
const JOIN_ENDS = [[]];
for (const [kind, tx, ty] of [
...SCREW_DIRS.map(([x,y]) => [x && y ? 'screw-diagonal' : x ? 'screw-horizontal' : 'screw-vertical',x,y]),
['belt-flat',-1,0], ['belt-flat',1,0],
['belt-sloped',-1,1], ['belt-sloped',1,-1],
['belt-sloped',-1,-1], ['belt-sloped',1,1]
]) {
const outlet = (tx || ty) > 0 ? 1 : 0;
JOIN_ENDS.push([0,1].map(e => ({
dx: (e ? 1 : -1) * Math.abs(tx), dy: (e ? 1 : -1) * (tx ? ty * tx : 1),
diagonal: !!(tx && ty), kind, tx, ty, outlet: e === outlet, targets: []
})));
}
// Compile the diagonal-end rule and its inverse into bounded neighbour probes.
for (const ends of JOIN_ENDS) for (const end of ends) {
function pieceEnds(code, v) {
if (code === BP_SCREW) return JOIN_ENDS[v >= 0 && v < 8 ? v + 1 : 0];
if (!isBelt(code) && !isFilter(code)) return JOIN_ENDS[0];
const d = isFilter(code) ? (v & 128 ? 1 : 0) : (v & 1);
return JOIN_ENDS[9 + d + (beltGrade(code) < 0 ? 2 : beltGrade(code) > 0 ? 4 : 0)];
}
for (const ends of JOIN_ENDS) for (let e = 0; e < ends.length; e++) {
const a = ends[e];
if (!a.outlet) continue;
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;
const b = others[n], screw = a.kind.startsWith('screw'), otherScrew = b.kind.startsWith('screw');
if (b.outlet || a.tx * b.tx + a.ty * b.ty <= 0) continue;
if (screw !== otherScrew && (a.kind === 'screw-vertical' || b.kind === 'screw-vertical' ||
(a.diagonal && b.diagonal))) continue;
if (a.diagonal && b.diagonal && (a.tx !== b.tx || a.ty !== b.ty)) continue;
const dy = !screw && a.ty < 0 ? 0 : a.ty;
const offsets = [[a.tx, dy]];
// Grains on a flat belt below an uphill low end already reach its carrying face.
if (!screw && !otherScrew && !a.ty && b.ty < 0) offsets.push([a.tx, -1]);
for (const [dx, yy] of offsets) {
a.targets.push({ dx, dy: yy, ends: others, end: n });
b.targets.push({ dx: -dx, dy: -yy, ends, end: e });
}
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));
}
for (const ends of JOIN_ENDS) for (const end of ends) {
const sameShape = t => t.ends[t.end].kind === end.kind &&
t.ends[t.end].tx === end.tx && t.ends[t.end].ty === end.ty;
end.targets.sort((a,b) => end.outlet ? Math.abs(a.dy - end.ty) - Math.abs(b.dy - end.ty) :
Number(sameShape(b)) - Number(sameShape(a)) || a.dy - b.dy || a.dx - b.dx);
}
// Result: neighbour block index * 2 + its end index; -1 means an open end.
function joinCandidate(bx, by, endIndex, planned) {
+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 || isSlopeBelt(code) ? new Map(cells.map(([x, y]) => [y * BW + x, [code, dIdx]])) : null;
const planned = code === BP_SCREW || isSlopeBelt(code) ? new Map(cells.map(([x, y]) => [y * BW + x, [code, isFilter(code) ? v : 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));