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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user