Sloped belts and filters drawn as one belt turned to 45° (t40)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 23:50:45 -04:00
co-authored by Claude Opus 5.5
parent e19cb784d2
commit 0cf615eb3d
11 changed files with 383 additions and 79 deletions
+12 -6
View File
@@ -356,11 +356,12 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
Propagated coloured light from glowing and burning cells. Propagated coloured light from glowing and burning cells.
- functions: builtForLight, looseForLight, computeLight - functions: builtForLight, looseForLight, computeLight
### src/render/slope-belts.js (28 lines) ### src/render/slope-belts.js (127 lines)
A slope's band uses world coordinates, so its stripes never restart at block joints. Drawing only: the face stays on its simulation cells; the band hangs below it.
- functions: slopeBeltAt, slopeBeltColor - functions: slopeBeltConnections, flatBeltCaps, prepareSlopeBelts, slopeBeltSegment, slopeBeltCorner, slopeBeltShape, slopeBeltAt, slopeBeltColor
- variables: slopeBeltJoins, slopeBeltHalo, beltCaps, slopeAcross, slopeAlong, slopeDistance
### src/render/world.js (422 lines) ### src/render/world.js (440 lines)
Scale colour without changing the smooth-look class. Scale colour without changing the smooth-look class.
- functions: fogColor, diagonalTubeJoins, diagonalTubeShape, diagonalTubeBlock, drawWorld - functions: fogColor, diagonalTubeJoins, diagonalTubeShape, diagonalTubeBlock, drawWorld
- variables: TUBE_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap - variables: TUBE_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
@@ -378,7 +379,11 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace - functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes - variables: SM, smBytes
### src/render/smooth-shaders.js (393 lines) ### src/render/slope-belt-shader.js (84 lines)
Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
- variables: SM_SLOPE_HELPERS, SM_SLOPE_DRAW
### src/render/smooth-shaders.js (369 lines)
Smooth shaders: vertex and fragment programs for the rounded world look. Smooth shaders: vertex and fragment programs for the rounded world look.
- variables: SM_VS, SM_FS - variables: SM_VS, SM_FS
@@ -471,7 +476,7 @@ Gamepad input.
- functions: padPressed, pollPad - functions: padPressed, pollPad
- variables: PAD - variables: PAD
### src/ui/previews.js (227 lines) ### src/ui/previews.js (237 lines)
Cursor previews: shared grid helpers, air streaks and tool preview dispatch. Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock - functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
@@ -522,6 +527,7 @@ The frame loop and boot.
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore. - `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation. - `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
- `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly. - `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly.
- `test/t_slope_belt_art.js`: Drawing geometry against real parts, without changing transport.
- `test/t_slope_belts.js`: Sloped belts: transport, joins, sorting, persistence and the actual drag controls. - `test/t_slope_belts.js`: Sloped belts: transport, joins, sorting, persistence and the actual drag controls.
- `test/t_slopes.js`: Sloped walls: flow, sealed joints, building, refunds and persistence. - `test/t_slopes.js`: Sloped walls: flow, sealed joints, building, refunds and persistence.
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works. - `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
+8
View File
@@ -4,6 +4,14 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-25: t40 art pass: sloped belts and filters drawn as one belt (Claude Code, Codex)
- Sloped runs draw as the flat belt (rollers, studs) or filter (tinted mesh) band turned to 45°,
continuous, with elbows into flat belts, filters and tubes and rounded free ends, in both looks
(`src/render/slope-belts.js`, new `slope-belt-shader.js`, `world.js`, `smooth-shaders.js`);
ghosts and icons match. Drawing only.
- `test/t_slope_belt_art.js`; `run_all.py --browser` ALL PASSED.
## 2026-10-25: t40: sloped belts and filters (Claude Code, Codex) ## 2026-10-25: t40: sloped belts and filters (Claude Code, Codex)
- Block codes 19-22 (`BP_BELT_UP/DOWN`, `BP_FILTER_UP/DOWN`, `src/sim/pieces.js`): slope-shaped - Block codes 19-22 (`BP_BELT_UP/DOWN`, `BP_FILTER_UP/DOWN`, `src/sim/pieces.js`): slope-shaped
+1
View File
@@ -46,6 +46,7 @@
"render/machines.js", "render/machines.js",
"render/overlays.js", "render/overlays.js",
"render/smooth.js", "render/smooth.js",
"render/slope-belt-shader.js",
"render/smooth-shaders.js", "render/smooth-shaders.js",
"ui/audio.js", "ui/audio.js",
"ui/state.js", "ui/state.js",
+84
View File
@@ -0,0 +1,84 @@
// Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
const SM_SLOPE_HELPERS = `
// The same rounded strokes as slopeBeltShape; y/z carry distance along/across the band.
vec3 slopeStroke(vec2 p, vec2 a, vec2 b, float origin) {
vec2 ray = b - a; float len = length(ray); ray /= len;
float along = dot(p - a, ray), across = dot(p - a, vec2(-ray.y, ray.x));
vec2 q = abs(vec2(along - len * 0.5, across)) - vec2(len * 0.5 - 0.75, 1.1);
float sd = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - 0.75;
return vec3(sd, origin + along, across + 2.0);
}
vec3 slopeCorner(vec2 p, vec2 a, float origin) {
vec2 d = p - a; float radius = length(d);
return vec3(radius - 1.85, origin + d.x, 2.0 + d.y);
}
vec3 slopeShape(vec2 p, vec2 worldOrigin, float grade, int joins) {
float sy = grade < 0.0 ? 4.0 : 0.0;
vec2 a = vec2(-grade * 1.30814755, sy + 1.30814755), b = vec2(4.0 - grade * 1.30814755, 4.0 - sy + 1.30814755);
if ((joins & 1) != 0) { a.y = sy + 2.0; a.x = (a.y - sy - 2.61629509) / grade; }
else if ((joins & 4) != 0) a -= vec2(4.0, grade * 4.0);
if ((joins & 2) != 0) { b.y = 6.0 - sy; b.x = (b.y - sy - 2.61629509) / grade; }
else if ((joins & 8) != 0) b += vec2(4.0, grade * 4.0);
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);
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);
if (h.x < best.x) best = h;
h = slopeCorner(p, b, worldOrigin.x + b.x);
if (h.x < best.x) best = h;
}
return best;
}
`;
const SM_SLOPE_DRAW = `
// Slopes use the flat belt's band, rollers and mesh in coordinates along the run.
if ((nc == 2 || (nc >= 23 && nc <= 30)) &&
(int(cellAt(tTree, c).a * 255.0 + 0.5) & 128) != 0 &&
cellAt(tTree, c).r * 255.0 >= 22.5 && cellAt(tTree, c).r * 255.0 <= 30.5) {
int slopeClass = int(cellAt(tTree, c).r * 255.0 + 0.5);
int shape = (slopeClass - 23) / 2;
bool mesh = shape >= 2;
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);
vec3 shapePos = slopeShape(local, w - local, grade, joins);
float sd = shapePos.x, axis = shapePos.y, depth = shapePos.z;
float time = (joins & 16) != 0 ? uTime : 0.0;
vec2 lb = vec2(axis, depth), alongWorld = vec2(axis, depth);
float rg = step(0.72, fract((axis - dir * (depth < 2.0 ? 1.0 : -1.0) * time * 30.0) * 0.5));
vec3 belt;
if (-sd < 0.8) {
belt = bandColor(lb, rg);
if (mesh && ((joins & 32) != 0) == (depth < 2.0) && dot(own.rgb, vec3(1)) > 0.01)
belt = mix(belt, own.rgb * (depth < 2.0 ? 1.0 : 0.8), 0.75 * rg);
belt *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), -sd));
} else if (mesh) {
vec2 cg = abs(fract(alongWorld) - 0.5);
float bw2 = max(px * 0.9, 0.09);
float hole = 1.0 - smoothstep(0.5 - bw2 - px, 0.5 - bw2, max(cg.x, cg.y));
vec3 steel = dot(own.rgb, vec3(1)) > 0.01 ? mix(vec3(0.42, 0.44, 0.5), own.rgb, 0.4) : vec3(0.42, 0.44, 0.5);
belt = mix(steel * 0.78, C * 0.8, hole);
} else {
vec2 dv = vec2(mod(axis, 2.0) - 1.0, depth - 2.0);
float rd = length(dv);
belt = vec3(0.15, 0.16, 0.18);
if (rd < 0.95) {
float ang = atan(dv.y, dv.x) - dir * time * 30.0 / 0.95;
float spoke = smoothstep(0.55, 0.85, cos(ang * 3.0));
belt = mix(vec3(0.5, 0.36, 0.25), vec3(0.72, 0.53, 0.36), 0.5 + 0.5 * dot(normalize(dv + 1e-4), vec2(-0.6, -0.8)) * smoothstep(0.2, 0.95, rd));
belt *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
belt *= mix(1.0, 0.55, smoothstep(0.82, 0.95, rd));
}
}
C = mix(C, belt, 1.0 - smoothstep(-px, px, sd));
}
`;
+121 -22
View File
@@ -1,28 +1,127 @@
// A slope's band uses world coordinates, so its stripes never restart at block joints. // Drawing only: the face stays on its simulation cells; the band hangs below it.
function slopeBeltAt(i, x, y) { let slopeBeltJoins = new Uint8Array(0), slopeBeltHalo = new Uint8Array(0), beltCaps = new Uint8Array(0);
const b = (y >> 2) * BW + (x >> 2); let slopeAcross = 0, slopeAlong = 0, slopeDistance = 0;
if (isSlopeBelt(blockP[b])) return b; function slopeBeltConnections(bx, by, code, planned = null) {
if ((y & 3) || ((x & 3) !== 0 && (x & 3) !== 3)) return -1; const grade = beltGrade(code);
const owner = slopeSealOwner(i); let joins = 0;
return owner >= 0 && isSlopeBelt(blockP[owner]) ? owner : -1; 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_TUBE && (v === 1 || v === 3))) joins |= 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_TUBE && (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 prepareSlopeBelts(x0, y0, x1, y1) {
if (slopeBeltJoins.length !== NB) { slopeBeltJoins = new Uint8Array(NB); slopeBeltHalo = new Uint8Array(NB); beltCaps = new Uint8Array(NB); }
slopeBeltHalo.fill(0);
for (let by = Math.max(0, (y0 >> 2) - 2); by <= Math.min(BH - 1, (y1 >> 2) + 2); by++) {
for (let bx = Math.max(0, (x0 >> 2) - 2); bx <= Math.min(BW - 1, (x1 >> 2) + 2); bx++) {
const b = by * BW + bx, c = blockP[b];
if (c === BP_BELT || c === BP_FILTER) beltCaps[b] = flatBeltCaps(b);
if (!isSlopeBelt(c)) continue;
const face = beltFaceY(c, bx, by, bx * BS) * W + bx * BS;
slopeBeltJoins[b] = slopeBeltConnections(bx, by, c) | (netSat(face) > 0 ? 16 : 0) | (filterCfg.get(b)?.inv ? 32 : 0);
for (let yy = Math.max(0, by - 1); yy <= Math.min(BH - 1, by + 1); yy++)
for (let xx = Math.max(0, bx - 1); xx <= Math.min(BW - 1, bx + 1); xx++) slopeBeltHalo[yy * BW + xx] = 1;
}
}
}
// Rounded stroke, with coordinates along/across the winning segment for rollers and mesh.
function slopeBeltSegment(x, y, ax, ay, bx, by, origin) {
const dx = bx - ax, dy = by - ay, len = Math.hypot(dx, dy), ux = dx / len, uy = dy / len;
const along = (x - ax) * ux + (y - ay) * uy, across = -(x - ax) * uy + (y - ay) * ux;
const qx = Math.abs(along - len / 2) - len / 2 + 0.75, qy = Math.abs(across) - 1.1;
const sd = Math.hypot(Math.max(0, qx), Math.max(0, qy)) + Math.min(Math.max(qx, qy), 0) - 0.75;
if (sd < slopeDistance) { slopeDistance = sd; slopeAcross = across + 2; slopeAlong = origin + along; }
}
function slopeBeltCorner(x, y, ax, ay, origin) {
const dx = x - ax, dy = y - ay, radius = Math.hypot(dx, dy), sd = radius - 1.85;
if (sd < slopeDistance) { slopeDistance = sd; slopeAcross = 2 + dy; slopeAlong = origin + dx; }
}
function slopeBeltShape(x, y, b, joins = slopeBeltJoins[b], code = blockP[b]) {
const grade = beltGrade(code), ox = (b % BW) * BS, oy = ((b / BW) | 0) * BS;
x -= ox; y -= oy;
const offset = 1.85 * Math.SQRT2, sy = grade < 0 ? 4 : 0;
const normal = 1.85 * Math.SQRT1_2;
let ax = -grade * normal, ay = sy + normal, bx = 4 - grade * normal, by = 4 - sy + normal;
if (joins & 1) { ay = sy + 2; ax = (ay - sy - offset) / grade; }
else if (joins & 4) { ax -= 4; ay -= grade * 4; }
if (joins & 2) { by = 6 - sy; bx = (by - sy - offset) / grade; }
else if (joins & 8) { bx += 4; by += grade * 4; }
slopeDistance = Infinity;
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);
slopeBeltCorner(x, y, ax, ay, ox + ax);
}
if (joins & 2) {
slopeBeltSegment(x, y, bx, by, 6, by, ox + bx);
slopeBeltCorner(x, y, bx, by, ox + bx);
}
return slopeDistance;
}
function slopeBeltAt(i, x, y, smooth = false) {
const own = (y >> 2) * BW + (x >> 2), code = blockP[own];
if (!slopeBeltHalo[own]) return -1;
const slope = isSlopeBelt(code);
if (!slope && (mach[i] || occ[i] || (mat[i] !== EMPTY && mat[i] !== BELTL && mat[i] !== BELTR))) return -1;
let seal = !slope && (mat[i] === BELTL || mat[i] === BELTR || part[i] === P_FILTER) ? slopeSealOwner(i) : -1;
if (seal >= 0 && !isSlopeBelt(blockP[seal])) seal = -1;
if (!slope && seal < 0 && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || code)) return -1;
let best = smooth ? 0.8 : 0, found = slope ? own : seal;
for (let yy = Math.max(0, (y >> 2) - 1); yy <= Math.min(BH - 1, (y >> 2) + 1); yy++) {
for (let xx = Math.max(0, (x >> 2) - 1); xx <= Math.min(BW - 1, (x >> 2) + 1); xx++) {
const b = yy * BW + xx;
if (!isSlopeBelt(blockP[b])) continue;
const sd = slopeBeltShape(x + 0.5, y + 0.5, b);
if (sd < best) { best = sd; found = b; }
}
}
return found;
} }
function slopeBeltColor(b, x, y, anim, br, bg, bb) { function slopeBeltColor(b, x, y, anim, br, bg, bb) {
const c = blockP[b], grade = beltGrade(c), d = y - beltFaceY(c, b % BW, (b / BW) | 0, x); const sd = slopeBeltShape(x + 0.5, y + 0.5, b);
let r = br, g = bg, blue = bb; if (sd > 1e-8) return (br | 0) | ((bg | 0) << 8) | ((bb | 0) << 16);
if (d >= 0) { const c = blockP[b], joins = slopeBeltJoins[b], mesh = isFilter(c);
const dir = isFilter(c) ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1); const dir = mesh ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
const axis = (x + grade * y) * 0.5, stripe = (Math.floor((axis - dir * (anim >> 1)) / 2) & 1); const a = slopeAlong, d = slopeAcross, time = joins & 16 ? anim : 0;
if (d === 0) { let r = 38, g = 41, blue = 46;
r = stripe ? 92 : 56; g = stripe ? 96 : 58; blue = stripe ? 104 : 66; if (-sd < 0.8) {
if (isFilter(c) && !stripe) { const stripe = (Math.floor((a - dir * (d < 2 ? 1 : -1) * (time >> 1)) / 2) & 1), k = d < 2 ? 1 : 0.72;
const f = filterCfg.get(b), m = f && f.m[0]; r = (stripe ? 92 : 56) * k; g = (stripe ? 96 : 58) * k; blue = (stripe ? 104 : 66) * k;
if (m) { r = COLR[m] * 0.7; g = COLG[m] * 0.7; blue = COLB[m] * 0.7; } } else if (mesh) {
const f = filterCfg.get(b), m = f && f.m.length ? f.m[(Math.floor(a / 2) & 65535) % f.m.length] : 0;
if ((Math.floor(a) + Math.floor(d)) & 1) { r = br * 0.8; g = bg * 0.8; blue = bb * 0.8; }
else { r = m ? COLR[m] * 0.75 : 64; g = m ? COLG[m] * 0.75 : 68; blue = m ? COLB[m] * 0.75 : 76; }
} else {
const dx = ((a % 2 + 2) % 2) - 1, dy = d - 2;
if (dx * dx + dy * dy < 0.95 * 0.95) {
const spin = Math.cos((Math.atan2(dy, dx) - dir * time / 1.9) * 3) > 0.6;
r = spin ? 104 : 132; g = spin ? 94 : 120; blue = spin ? 84 : 108;
} }
} else if (isFilter(c)) {
if (d === 1 && ((x + (grade < 0 ? 0 : 1)) & 1)) { r = 64; g = 68; blue = 76; }
} else if (d === 1) {
r = stripe ? 132 : 104; g = stripe ? 120 : 94; blue = stripe ? 108 : 84;
} else { r = 46; g = 48; blue = 54; }
} }
return (r | 0) | ((g | 0) << 8) | ((blue | 0) << 16); return (r | 0) | ((g | 0) << 8) | ((blue | 0) << 16);
} }
+6 -30
View File
@@ -50,9 +50,8 @@ float beltSD(vec2 lb, bool endL, bool endR) {
// The outline of the belt or filter in cell c. Belts and filters side by side are one belt, so // The outline of the belt or filter in cell c. Belts and filters side by side are one belt, so
// the line only rounds off where the whole run ends. // the line only rounds off where the whole run ends.
float beltLineSD(ivec2 c, vec2 lb) { float beltLineSD(ivec2 c, vec2 lb) {
int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0))); int caps = int(cellAt(tTree, c).a * 255.0 + 0.5);
return beltSD(lb, isBelt(nl) + isF(nl) < 0.5 && !(nl >= 19 && nl <= 30) && cls(cellAt(tCol, c + ivec2(-4, -4))) < 23, return beltSD(lb, (caps & 1) != 0, (caps & 2) != 0);
isBelt(nr) + isF(nr) < 0.5 && !(nr >= 19 && nr <= 30) && cls(cellAt(tCol, c + ivec2(4, -4))) < 23);
} }
// The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and // The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and
// back along the bottom. // back along the bottom.
@@ -62,6 +61,8 @@ float bandRidge(vec2 w, vec2 lb, float dir) {
} }
vec3 bandColor(vec2 lb, float rg) { return mix(vec3(0.2, 0.21, 0.24), vec3(0.34, 0.35, 0.39), rg) * (lb.y < 2.0 ? 1.0 : 0.72); } vec3 bandColor(vec2 lb, float rg) { return mix(vec3(0.2, 0.21, 0.24), vec3(0.34, 0.35, 0.39), rg) * (lb.y < 2.0 ? 1.0 : 0.72); }
${SM_SLOPE_HELPERS}
void main() { void main() {
float zp = uView.z, px = 1.0 / zp; float zp = uView.z, px = 1.0 / zp;
vec2 sp = vec2(gl_FragCoord.x, uView.w - gl_FragCoord.y); vec2 sp = vec2(gl_FragCoord.x, uView.w - gl_FragCoord.y);
@@ -185,6 +186,8 @@ void main() {
C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT)); C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT));
} }
${SM_SLOPE_DRAW}
// ---- loose grains: little round pebbles; a pocket closed in by four grains is filled // ---- loose grains: little round pebbles; a pocket closed in by four grains is filled
int np = int(isN(n0, 2) + isN(n1, 2) + isN(n2, 2) + isN(n3, 2)); int np = int(isN(n0, 2) + isN(n1, 2) + isN(n2, 2) + isN(n3, 2));
bool closed = isM(n0) + isM(n1) + isM(n2) + isM(n3) > 3.5; bool closed = isM(n0) + isM(n1) + isM(n2) + isM(n3) > 3.5;
@@ -352,33 +355,6 @@ void main() {
} }
} }
// Sloped belts and filters: a continuous running band over a triangular frame.
if (nc >= 23 && nc <= 30) {
int shape = (nc - 23) / 2;
bool mesh = shape >= 2;
float grade = (shape == 0 || shape == 2) ? -1.0 : 1.0;
float dir = ((nc - 23) % 2) == 1 ? 1.0 : -1.0;
vec4 tag = cellAt(tTree, c);
vec2 lb = mod(w, 4.0);
if (tag.a > 0.4) lb.y += 4.0;
float face = grade < 0.0 ? 4.0 - lb.x : lb.x;
float depth = (lb.y - face) * 0.70710678;
float inside = smoothstep(-px, px, depth);
float axis = (w.x + grade * w.y) * 0.5;
float rg = step(0.72, fract((axis - dir * uTime * 30.0) * 0.5));
vec3 band = bandColor(vec2(axis, 0.5), rg);
vec3 frame = vec3(0.18, 0.19, 0.21);
float roller = 1.0 - smoothstep(0.32, 0.5, length(vec2(mod(axis, 2.0) - 1.0, depth - 0.95)));
frame = mix(frame, vec3(0.50, 0.40, 0.31), roller);
if (mesh) {
float hole = step(0.48, fract(axis)) * step(0.35, fract(depth * 1.8));
band = mix(band, dot(tag.rgb, vec3(1)) > 0.01 ? tag.rgb * 0.7 : vec3(0.28, 0.32, 0.33), hole);
frame = mix(C, vec3(0.25, 0.27, 0.30), roller * 0.7);
}
vec3 belt = mix(band, frame, smoothstep(0.55, 0.75, depth));
C = mix(C, belt, inside);
}
// ---- light, then the glow of hot things on top // ---- light, then the glow of hot things on top
if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb; if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb;
if (uGlowK > 0.0) { if (uGlowK > 0.0) {
+29 -11
View File
@@ -92,6 +92,7 @@ function drawWorld() {
if (blockP[b] === BP_TUBE && blockV[b] >= 4) diagonalJoins[b] = diagonalTubeJoins(bx, by, blockV[b]); if (blockP[b] === BP_TUBE && blockV[b] >= 4) diagonalJoins[b] = diagonalTubeJoins(bx, by, blockV[b]);
} }
} }
prepareSlopeBelts(vx0, vy0, vx0 + bw, vy0 + bh);
const useFog = SET.fog; const useFog = SET.fog;
const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black
const lighting = SET.lighting !== 'off' && !R.lens; const lighting = SET.lighting !== 'off' && !R.lens;
@@ -224,7 +225,9 @@ function drawWorld() {
} }
} }
// field parts drawn over contents // field parts drawn over contents
const sb = slopeBeltAt(i, x, y, sm);
let p = part[i]; let p = part[i];
if (p === P_FILTER && (sb >= 0 || isSlopeBelt(blockP[slopeSealOwner(i)]))) p = 0;
const db = diagonalTubeBlock(x, y, sm); const db = diagonalTubeBlock(x, y, sm);
let diagonal = false; let diagonal = false;
if (db >= 0) { if (db >= 0) {
@@ -281,8 +284,8 @@ function drawWorld() {
if (pipeBodyCell(x, y)) { thinPipeColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2]; } if (pipeBodyCell(x, y)) { thinPipeColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2]; }
} }
} }
const sb = slopeBeltAt(i, x, y), slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR); const slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
if (slopeArt) { if (slopeArt && !sm) {
const color = slopeBeltColor(sb, x, y, anim, br, bgc, bb); const color = slopeBeltColor(sb, x, y, anim, br, bgc, bb);
r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255; r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255;
} }
@@ -307,9 +310,12 @@ function drawWorld() {
// filter: 13 carries left, 14 right; the tree buffer carries its colour tag (first material) and mode // filter: 13 carries left, 14 right; the tree buffer carries its colour tag (first material) and mode
const fb = (y >> 2) * BW + (x >> 2), fc = filterCfg.get(fb), fm = fc && fc.m.length ? fc.m[0] : 0; const fb = (y >> 2) * BW + (x >> 2), fc = filterCfg.get(fb), fm = fc && fc.m.length ? fc.m[0] : 0;
cl = blockV[fb] & 128 ? 14 : 13; cl = blockV[fb] & 128 ? 14 : 13;
tpx[row + bx] = ((fc && fc.inv ? 0x30 : 0x20) << 24) | (fm ? (COLB[fm] << 16) | (COLG[fm] << 8) | COLR[fm] : 0); tpx[row + bx] = ((0x20 | (fc && fc.inv ? 0x10 : 0) | beltCaps[fb]) << 24) | (fm ? (COLB[fm] << 16) | (COLG[fm] << 8) | COLR[fm] : 0);
}
else if (m === BELTL || m === BELTR) {
cl = m === BELTL ? 7 : 8;
tpx[row + bx] = beltCaps[(y >> 2) * BW + (x >> 2)] << 24;
} }
else if (m === BELTL || m === BELTR) cl = m === BELTL ? 7 : 8;
else if (pt || PARTMAT[m] || mmask[row + bx]) cl = 4; else if (pt || PARTMAT[m] || mmask[row + bx]) cl = 4;
else if (m !== EMPTY) { else if (m !== EMPTY) {
const ph = PH[m]; const ph = PH[m];
@@ -324,12 +330,14 @@ function drawWorld() {
const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]]; const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]];
r = COLR[wm]; g = COLG[wm]; b = COLB[wm]; r = COLR[wm]; g = COLG[wm]; b = COLB[wm];
} }
if (slopeArt) { if (sb >= 0 && (slopeArt || PH[m] === PH_POWDER)) {
const code = blockP[sb], dir = isFilter(code) ? (blockV[sb] & 128 ? 1 : 0) : blockV[sb]; const code = blockP[sb], dir = isFilter(code) ? (blockV[sb] & 128 ? 1 : 0) : blockV[sb];
cl = 23 + (code - BP_BELT_UP) * 2 + dir; const slopeClass = 23 + (code - BP_BELT_UP) * 2 + dir;
const f = isFilter(code) && filterCfg.get(sb), fm = f && f.m[0]; if (slopeArt) cl = slopeClass;
tpx[row + bx] = ((sb !== (y >> 2) * BW + (x >> 2) ? 0x80 : 0x40) << 24) | 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;
(fm ? (COLB[fm] << 16) | (COLG[fm] << 8) | COLR[fm] : 0); tpx[row + bx] = ((slopeBeltJoins[sb] | 128) << 24) | slopeClass |
((y - ((sb / BW) | 0) * BS + 8) << 16) | ((x - (sb % BW) * BS + 8) << 8);
if (slopeArt) { r = fm ? COLR[fm] : 0; g = fm ? COLG[fm] : 0; b = fm ? COLB[fm] : 0; }
} }
top = cl << 24; top = cl << 24;
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 && !builtForLight(i, x, y) ? 0xff : 0); fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 && !builtForLight(i, x, y) ? 0xff : 0);
@@ -380,12 +388,22 @@ function drawWorld() {
let tr = 90, tg = 190, tb = 230; let tr = 90, tg = 190, tb = 230;
if (blocked) { tr = 230; tg = 70; tb = 60; } if (blocked) { tr = 230; tg = 70; tb = 60; }
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; } 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; }
for (let dy = 0; dy < BS; dy++) for (let dx = 0; dx < BS; dx++) { const slope = isSlopeBelt(code), joins = slope ? slopeBeltConnections(gbx, gby, code, ghostBlocks) : 0;
for (let dy = slope ? -4 : 0; dy < (slope ? 8 : BS); dy++) for (let dx = slope ? -4 : 0; dx < (slope ? 8 : BS); dx++) {
let sd = 0;
if (slope) {
const x = gbx * BS + dx, y = gby * BS + dy;
if (x < 0 || y < 0 || x >= W || y >= H) continue;
const i = y * W + x, n = (y >> 2) * BW + (x >> 2);
if (n !== b && (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || blockP[n])) continue;
sd = slopeBeltShape(x + 0.5, y + 0.5, b, joins, code);
if (sd > 0) continue;
}
if (isSlope(code) && !slopeCell(code, dx, dy)) continue; if (isSlope(code) && !slopeCell(code, dx, dy)) continue;
const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0; const sx = gbx * BS + dx - vx0, sy = gby * BS + dy - vy0;
if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue; if (sx < 0 || sy < 0 || sx >= bw || sy >= bh) continue;
const k = sy * bw + sx, c = px[k]; const k = sy * bw + sx, c = px[k];
const edge = dx === 0 || dy === 0 || dx === BS - 1 || dy === BS - 1; const edge = slope ? sd > -0.8 : dx === 0 || dy === 0 || dx === BS - 1 || dy === BS - 1;
const a = edge ? (((dx + dy + pulse) & 3) === 0 ? 0.85 : 0.6) : 0.28; 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; 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); px[k] = (sm ? 0x04000000 : 0xff000000) | ((pb + (tb - pb) * a) << 16) | ((pg + (tg - pg) * a) << 8) | (pr + (tr - pr) * a);
+2 -2
View File
@@ -12,8 +12,8 @@ const PIECE_ICONS = {
wall: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 12h18M9 5v7M15 12v7"/>', wall: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 12h18M9 5v7M15 12v7"/>',
grate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M7.5 5v14M12 5v14M16.5 5v14"/>', grate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M7.5 5v14M12 5v14M16.5 5v14"/>',
gate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M9 5v14"/><path d="M12 12h6M16 9.5l2.5 2.5-2.5 2.5"/>', gate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M9 5v14"/><path d="M12 12h6M16 9.5l2.5 2.5-2.5 2.5"/>',
belt: '<path d="M3 20V16L19 4h3v16z"/><path d="M4 17L20 5"/><circle cx="8" cy="16" r="1.5"/><circle cx="17" cy="9" r="1.5"/>', belt: '<g transform="rotate(-45 12 12)"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M4 9h16M4 15h16"/><circle cx="6" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="18" cy="12" r="2"/></g>',
filter: '<path d="M3 17L19 5l3 4L6 21zM7 15l3 3M11 12l3 3M15 9l3 3M7 5v5M5 8l2 2 2-2"/>', filter: '<g transform="rotate(-45 12 12)"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M4 9h16M4 15h16M6 9v6M10 9v6M14 9v6M18 9v6M4 12h16"/></g>',
tube: '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M6.5 18l3.5-12M11.5 18l3.5-12M16.5 18l3-10"/>', tube: '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M6.5 18l3.5-12M11.5 18l3.5-12M16.5 18l3-10"/>',
pipe: '<path d="M2 10h20M2 14h20"/><path d="M6 7.5v9M18 7.5v9"/>', pipe: '<path d="M2 10h20M2 14h20"/><path d="M6 7.5v9M18 7.5v9"/>',
pump: '<circle cx="11" cy="14" r="6"/><path d="M11 14l3.5-2.5M11 14l-1 4.2M11 14l-4-1.2"/><path d="M15.5 9.5L20 5M16 5h4v4"/>', pump: '<circle cx="11" cy="14" r="6"/><path d="M11 14l3.5-2.5M11 14l-1 4.2M11 14l-4-1.2"/><path d="M15.5 9.5L20 5M16 5h4v4"/>',
+16 -6
View File
@@ -207,14 +207,24 @@ function previewBlock(code, x, y, w, h, stroke = false, direction = -1, bx = -1,
if (stroke) ctx.stroke(); else ctx.fill(); if (stroke) ctx.stroke(); else ctx.fill();
ctx.restore(); return; ctx.restore(); return;
} }
if (isSlopeBelt(code) && isFilter(code)) { if (isSlopeBelt(code)) {
const b = by * BW + bx, joins = bx >= 0 ? slopeBeltConnections(bx, by, code, planned) : 0;
const scale = w / BS, step = SET.look === 'smooth' ? 0.25 : 1;
const ox = bx * BS, oy = by * BS;
ctx.beginPath(); ctx.beginPath();
if (beltGrade(code) < 0) { for (let yy = -4; yy < 8; yy += step) for (let xx = -4; xx < 8; xx += step) {
ctx.moveTo(x, y + h); ctx.lineTo(x + w, y); ctx.lineTo(x + w, y + h * 0.25); ctx.lineTo(x + w * 0.25, y + h); const wx = ox + xx, wy = oy + yy, ix = Math.floor(wx), iy = Math.floor(wy);
} else { if (bx >= 0 && (ix < ox || ix >= ox + BS || iy < oy || iy >= oy + BS)) {
ctx.moveTo(x, y); ctx.lineTo(x + w, y + h); ctx.lineTo(x + w * 0.75, y + h); ctx.lineTo(x, y + h * 0.25); if (ix < 0 || ix >= W || iy < 0 || iy >= H) continue;
const i = iy * W + ix, n = (iy >> 2) * BW + (ix >> 2), next = planned && planned.get(n);
if (mat[i] !== EMPTY || part[i] || mach[i] || occ[i] || (blockP[n] && !isSlopeBelt(blockP[n])) || (next && !isSlopeBelt(next[0]))) continue;
} }
ctx.closePath(); if (stroke) ctx.stroke(); else ctx.fill(); return; const sd = slopeBeltShape(wx + step / 2, wy + step / 2, b, joins, code);
if (sd <= 0 && (!stroke || sd > -step * 1.5)) ctx.rect(x + xx * scale, y + yy * scale, step * scale, step * scale);
}
if (stroke) { const fill = ctx.fillStyle; ctx.fillStyle = ctx.strokeStyle; ctx.fill(); ctx.fillStyle = fill; }
else ctx.fill();
return;
} }
code = slopeShape(code); code = slopeShape(code);
if (!isSlope(code)) { if (stroke) ctx.strokeRect(x, y, w, h); else ctx.fillRect(x, y, w, h); return; } if (!isSlope(code)) { if (stroke) ctx.strokeRect(x, y, w, h); else ctx.fillRect(x, y, w, h); return; }
+100
View File
@@ -0,0 +1,100 @@
// Drawing geometry against real parts, without changing transport.
const fs = require('fs');
const path = require('path');
const { load, suite, flatWorld } = require('./harness');
const { run } = load();
const { check, done } = suite();
run(fs.readFileSync(path.join(__dirname, '../src/render/slope-belts.js'), 'utf8'));
flatWorld(run, 160, 120, 112);
for (const code of ['BP_BELT_UP', 'BP_BELT_DOWN', 'BP_FILTER_UP', 'BP_FILTER_DOWN']) {
run(`
mat.fill(EMPTY); part.fill(0); blockP.fill(0); blockV.fill(0);
var artCode = ${code}, artGrade = beltGrade(artCode), artB = 12 * BW + 12;
for (let k = -2; k <= 2; k++) placeBlock(artCode, 1, 12 + k, 12 + artGrade * k, true);
prepareSlopeBelts(0, 0, W, H);
`);
check(`${code}: diagonal neighbours continue the band`, run('(slopeBeltJoins[artB] & 12) === 12'));
check(`${code}: corner seals use the same band`, run(`(() => {
const i = slopeSeal(artCode, 12, 12);
return i < 0 || slopeBeltAt(i, i % W, (i / W) | 0) >= 0;
})()`));
check(`${code}: band is as thick as a flat belt`, run(`(() => {
const g = artGrade, x = 50 - g * 1.85 * Math.SQRT1_2, y = 50 + 1.85 * Math.SQRT1_2;
return slopeBeltShape(x, y, artB) < -1.8 &&
slopeBeltShape(x - g * 1.7 * Math.SQRT1_2, y + 1.7 * Math.SQRT1_2, artB) < 0 &&
slopeBeltShape(x - g * 2.1 * Math.SQRT1_2, y + 2.1 * Math.SQRT1_2, artB) > 0;
})()`));
check(`${code}: air below the triangle gets the continuous band`, run(`(() => {
for (let y = 48; y < 57; y++) for (let x = 48; x < 56; x++) {
const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
if (!blockP[own] && mat[i] === EMPTY && slopeBeltAt(i, x, y) >= 0) return true;
}
return false;
})()`));
run(`
for (let end = 0; end < 2; end++) {
const side = end ? 1 : -1;
removeBlock(12 + side, 12 + side * artGrade, true);
placeBlock(BP_BELT, 1, 12 + side, 12 + (end ? artGrade > 0 : artGrade < 0), true);
}
prepareSlopeBelts(0, 0, W, H);
`);
check(`${code}: both flat joints form elbows`, run('(slopeBeltJoins[artB] & 3) === 3'));
check(`${code}: adjoining flats do not round off at the joint`, run(`(() => {
const l = (12 + (artGrade < 0)) * BW + 11, r = (12 + (artGrade > 0)) * BW + 13;
return !(flatBeltCaps(l) & 2) && !(flatBeltCaps(r) & 1);
})()`));
check(`${code}: overhang never covers terrain, parts, machines or bars`, run(`(() => {
for (let y = 48; y < 57; y++) for (let x = 48; x < 56; x++) {
const i = y * W + x, own = (y >> 2) * BW + (x >> 2);
if (blockP[own] || mat[i] !== EMPTY || slopeBeltAt(i, x, y) < 0) continue;
mat[i] = STONE; const ground = slopeBeltAt(i, x, y); mat[i] = EMPTY;
part[i] = P_GRATE; const built = slopeBeltAt(i, x, y); part[i] = 0;
mach[i] = 1; const machine = slopeBeltAt(i, x, y); mach[i] = 0;
occ[i] = 1; const bar = slopeBeltAt(i, x, y); occ[i] = 0;
return ground < 0 && built < 0 && machine < 0 && bar < 0;
}
return false;
})()`));
}
check('free end rounds its corners and stops at the end', run(`(() => {
const b = artB, x = 48 - Math.SQRT2, y = 48 + Math.SQRT2;
blockP[b] = BP_BELT_DOWN;
return slopeBeltShape(x + 0.8, y + 0.8, b, 0) < 0 && slopeBeltShape(x - 1, y - 1, b, 0) > 0;
})()`));
check('flat belts still meet diagonal tube elbows without an end cap', run(`(() => {
blockP[artB - 1] = BP_BELT;
blockP[artB] = BP_TUBE; blockV[artB] = 4;
const joined = !(flatBeltCaps(artB - 1) & 2);
blockP[artB] = BP_BELT_DOWN;
return joined;
})()`));
check('unpowered rollers and studs stand still', run(`(() => {
slopeBeltJoins[artB] &= ~16;
for (let y = 48; y < 56; y++) for (let x = 48; x < 56; x++)
if (slopeBeltColor(artB, x, y, 0, 150, 170, 190) !== slopeBeltColor(artB, x, y, 20, 150, 170, 190)) return false;
return true;
})()`));
check('powered rollers and studs reverse with the belt', run(`(() => {
slopeBeltJoins[artB] |= 16;
let moves = false;
for (let y = 48; y < 56; y++) for (let x = 48; x < 56; x++) {
blockV[artB] = 1;
const a = slopeBeltColor(artB, x, y, 12, 150, 170, 190), stopped = slopeBeltColor(artB, x, y, 0, 150, 170, 190);
blockV[artB] = 0;
if (a !== slopeBeltColor(artB, x, y, -12, 150, 170, 190)) return false;
if (a !== stopped) moves = true;
}
return moves;
})()`));
check('drawing leaves the seed, cells and saved block data unchanged', run(`(() => {
const snapshot = () => JSON.stringify([RS, Array.from(mat), Array.from(part), Array.from(blockP), Array.from(blockV)]);
const before = snapshot();
prepareSlopeBelts(0, 0, W, H);
for (let y = 40; y < 64; y++) for (let x = 40; x < 72; x++) {
const b = slopeBeltAt(y * W + x, x, y);
if (b >= 0) slopeBeltColor(b, x, y, 12, 150, 170, 190);
}
return snapshot() === before;
})()`));
done();
+3 -1
View File
@@ -138,7 +138,9 @@ fresh();
run('placeBlock(BP_BELT_UP,1,20,32,true);for(let l=1;l<=CARRY_DEPTH;l++)setCell((131-l)*W+80,SAND,20,0);tickStamp++;carryColumn(130*W+80,1,null);'); run('placeBlock(BP_BELT_UP,1,20,32,true);for(let l=1;l<=CARRY_DEPTH;l++)setCell((131-l)*W+80,SAND,20,0);tickStamp++;carryColumn(130*W+80,1,null);');
check('a slope carries the same full 96-grain column as a flat belt',run('Array.from({length:CARRY_DEPTH},(_,k)=>mat[(129-k)*W+81]===SAND).every(Boolean)')); check('a slope carries the same full 96-grain column as a flat belt',run('Array.from({length:CARRY_DEPTH},(_,k)=>mat[(129-k)*W+81]===SAND).every(Boolean)'));
run(fs.readFileSync('src/render/slope-belts.js','utf8')); run(fs.readFileSync('src/render/slope-belts.js','utf8'));
check('sloped stripes vary along the face and travel with the motor',run('(()=>{const b=32*BW+20,c=(x,y,t)=>slopeBeltColor(b,x,y,t,0,0,0);return c(80,131,0)!==c(82,129,0) && c(80,131,0)===c(82,129,4);})()')); // The renderer prepares joins and power first; stripe pitch is measured along the rotated band.
run('prepareSlopeBelts(0,0,W,H);');
check('sloped stripes vary along the face and travel with the motor',run('(()=>{const b=32*BW+20,c=(x,y,t)=>slopeBeltColor(b,x,y,t,0,0,0);return c(81,130,0)!==c(82,129,0) && c(81,130,0)===c(82,129,4);})()'));
run(fs.readFileSync('src/ui/selection.js','utf8')); run(fs.readFileSync('src/ui/selection.js','utf8'));
run(fs.readFileSync('src/ui/actions.js','utf8')); run(fs.readFileSync('src/ui/actions.js','utf8'));
run("globalThis.GAME={pieceDir:{belt:1,filter:1},filterCfg:{m:[SAND],inv:0},wallMat:RSTONE};globalThis.hudDirty=false;globalThis.aimBlock=()=>({bx:25,by:15});globalThis.toolId=()=>'build';globalThis.playSfx=()=>{};"); run("globalThis.GAME={pieceDir:{belt:1,filter:1},filterCfg:{m:[SAND],inv:0},wallMat:RSTONE};globalThis.hudDirty=false;globalThis.aimBlock=()=>({bx:25,by:15});globalThis.toolId=()=>'build';globalThis.playSfx=()=>{};");