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.
- functions: builtForLight, looseForLight, computeLight
### src/render/slope-belts.js (28 lines)
A slope's band uses world coordinates, so its stripes never restart at block joints.
- functions: slopeBeltAt, slopeBeltColor
### src/render/slope-belts.js (127 lines)
Drawing only: the face stays on its simulation cells; the band hangs below it.
- 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.
- functions: fogColor, diagonalTubeJoins, diagonalTubeShape, diagonalTubeBlock, drawWorld
- 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
- 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.
- variables: SM_VS, SM_FS
@@ -471,7 +476,7 @@ Gamepad input.
- functions: padPressed, pollPad
- 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.
- 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_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_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_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.
+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
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)
- 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/overlays.js",
"render/smooth.js",
"render/slope-belt-shader.js",
"render/smooth-shaders.js",
"ui/audio.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.
function slopeBeltAt(i, x, y) {
const b = (y >> 2) * BW + (x >> 2);
if (isSlopeBelt(blockP[b])) return b;
if ((y & 3) || ((x & 3) !== 0 && (x & 3) !== 3)) return -1;
const owner = slopeSealOwner(i);
return owner >= 0 && isSlopeBelt(blockP[owner]) ? owner : -1;
// Drawing only: the face stays on its simulation cells; the band hangs below it.
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_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) {
const c = blockP[b], grade = beltGrade(c), d = y - beltFaceY(c, b % BW, (b / BW) | 0, x);
let r = br, g = bg, blue = bb;
if (d >= 0) {
const dir = isFilter(c) ? (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);
if (d === 0) {
r = stripe ? 92 : 56; g = stripe ? 96 : 58; blue = stripe ? 104 : 66;
if (isFilter(c) && !stripe) {
const f = filterCfg.get(b), m = f && f.m[0];
if (m) { r = COLR[m] * 0.7; g = COLG[m] * 0.7; blue = COLB[m] * 0.7; }
const sd = slopeBeltShape(x + 0.5, y + 0.5, b);
if (sd > 1e-8) return (br | 0) | ((bg | 0) << 8) | ((bb | 0) << 16);
const c = blockP[b], joins = slopeBeltJoins[b], mesh = isFilter(c);
const dir = mesh ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
const a = slopeAlong, d = slopeAcross, time = joins & 16 ? anim : 0;
let r = 38, g = 41, blue = 46;
if (-sd < 0.8) {
const stripe = (Math.floor((a - dir * (d < 2 ? 1 : -1) * (time >> 1)) / 2) & 1), k = d < 2 ? 1 : 0.72;
r = (stripe ? 92 : 56) * k; g = (stripe ? 96 : 58) * k; blue = (stripe ? 104 : 66) * k;
} 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);
}
+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 line only rounds off where the whole run ends.
float beltLineSD(ivec2 c, vec2 lb) {
int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0)));
return beltSD(lb, isBelt(nl) + isF(nl) < 0.5 && !(nl >= 19 && nl <= 30) && cls(cellAt(tCol, c + ivec2(-4, -4))) < 23,
isBelt(nr) + isF(nr) < 0.5 && !(nr >= 19 && nr <= 30) && cls(cellAt(tCol, c + ivec2(4, -4))) < 23);
int caps = int(cellAt(tTree, c).a * 255.0 + 0.5);
return beltSD(lb, (caps & 1) != 0, (caps & 2) != 0);
}
// 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.
@@ -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); }
${SM_SLOPE_HELPERS}
void main() {
float zp = uView.z, px = 1.0 / zp;
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));
}
${SM_SLOPE_DRAW}
// ---- 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));
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
if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb;
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]);
}
}
prepareSlopeBelts(vx0, vy0, vx0 + bw, vy0 + bh);
const useFog = SET.fog;
const OUT = sm ? 0x04000000 : 0xff000000; // unexplored and off the map: black
const lighting = SET.lighting !== 'off' && !R.lens;
@@ -224,7 +225,9 @@ function drawWorld() {
}
}
// field parts drawn over contents
const sb = slopeBeltAt(i, x, y, sm);
let p = part[i];
if (p === P_FILTER && (sb >= 0 || isSlopeBelt(blockP[slopeSealOwner(i)]))) p = 0;
const db = diagonalTubeBlock(x, y, sm);
let diagonal = false;
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]; }
}
}
const sb = slopeBeltAt(i, x, y), slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
if (slopeArt) {
const slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
if (slopeArt && !sm) {
const color = slopeBeltColor(sb, x, y, anim, br, bgc, bb);
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
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;
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 (m !== EMPTY) {
const ph = PH[m];
@@ -324,12 +330,14 @@ function drawWorld() {
const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]];
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];
cl = 23 + (code - BP_BELT_UP) * 2 + dir;
const f = isFilter(code) && filterCfg.get(sb), fm = f && f.m[0];
tpx[row + bx] = ((sb !== (y >> 2) * BW + (x >> 2) ? 0x80 : 0x40) << 24) |
(fm ? (COLB[fm] << 16) | (COLG[fm] << 8) | COLR[fm] : 0);
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;
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;
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;
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; }
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;
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 = 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 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);
+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"/>',
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"/>',
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"/>',
filter: '<path d="M3 17L19 5l3 4L6 21zM7 15l3 3M11 12l3 3M15 9l3 3M7 5v5M5 8l2 2 2-2"/>',
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: '<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"/>',
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"/>',
+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();
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();
if (beltGrade(code) < 0) {
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);
} else {
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);
for (let yy = -4; yy < 8; yy += step) for (let xx = -4; xx < 8; xx += step) {
const wx = ox + xx, wy = oy + yy, ix = Math.floor(wx), iy = Math.floor(wy);
if (bx >= 0 && (ix < ox || ix >= ox + BS || iy < oy || iy >= oy + BS)) {
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);
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);');
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'));
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/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=()=>{};");