// 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) { let joins = 0; for (let end = 0; end < 2; end++) { const join = endJoin(bx, by, end, planned); if (join < 0) continue; const nb = join >> 1, next = planned && planned.get(nb); const slope = isSlopeBelt(next ? next[0] : blockP[nb]) && ((nb / BW) | 0) - by === beltGrade(code) * (nb % BW - bx); // A same-row feed into a low end drops onto its face: drawn as two capped ends, still joined. if (!slope && joinIsDrop(bx, by, end, join, planned)) continue; joins |= (slope ? 4 : 1) << end; } return joins; } function flatBeltCaps(b, planned = null) { const bx = b % BW, by = (b / BW) | 0; let caps = 0; for (let end = 0; end < 2; end++) { const join = endJoin(bx, by, end, planned); if (join < 0 || joinIsDrop(bx, by, end, join, planned)) 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) | (isFilter(c) && netSat(face) > 0 && screenLoaded(b) && (tick & 2) ? 64 : 0); for (let yy = Math.max(0, by - 2); 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; if (isFilter(code) && (joins & 64)) y -= 0.45; 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 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 = !mesh && joins & 16 ? anim : 0; let r = 38, g = 41, blue = 46; if (-sd < (mesh ? 1.1 : 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; } } return (r | 0) | ((g | 0) << 8) | ((blue | 0) << 16); } function screenLoaded(b) { const bx = b % BW, by = (b / BW) | 0, c = blockP[b]; for (let x = bx * BS; x < bx * BS + BS; x++) { const i = (beltFaceY(c, bx, by, x) - 1) * W + x; if (i >= 0 && (PH[mat[i]] === PH_POWDER || occ[i])) return true; } return false; } // Filled far wall, in belt coordinates. Project onto the face so ends stop at the slope ends. let slopeWallOwner = -1; function slopeWallShape(x, y, b, code = blockP[b]) { const g = beltGrade(code), ox = b % BW * BS, oy = ((b / BW) | 0) * BS; const sy = g < 0 ? BS : 0, lx = x - ox, ly = y - oy - sy; const foot = (lx + g * ly) / 2, depth = (g * lx - ly) * Math.SQRT1_2; return Math.max(-foot * Math.SQRT2, (foot - BS) * Math.SQRT2, -depth, depth - CLEAT_HEIGHT); } function slopeWallAt(x, y, smooth = false) { slopeWallOwner = -1; let best = smooth ? 0.8 : 0; for (let by = Math.max(0, (y >> 2) - 1); by <= Math.min(BH - 1, (y >> 2) + 3); by++) for (let bx = Math.max(0, (x >> 2) - 2); bx <= Math.min(BW - 1, (x >> 2) + 2); bx++) { const b = by * BW + bx; if (!isBelt(blockP[b]) || !isSlopeBelt(blockP[b]) || !slopeUphill(blockP[b], blockV[b])) continue; // downhill: a plain belt const sd = slopeWallShape(x + 0.5, y + 0.5, b); if (sd <= best) { best = sd; slopeWallOwner = b; } } return slopeWallOwner; } // The load occludes the far wall; ribs use exactly the simulation barrier raster. function slopeDetailAt(i, x, y, smooth = false) { slopeWallOwner = -1; if (!slopeBeltHalo[(y >> 2) * BW + (x >> 2)] && !cleatMask[i]) return 0; if (occ[i] || mach[i] || part[i]) return 0; if (cleatMask[i] && PH[mat[i]] !== PH_SOLID) { slopeWallAt(x, y, smooth); return 1; } if (mat[i] !== EMPTY && PH[mat[i]] !== PH_GAS) return 0; return slopeWallAt(x, y, smooth) >= 0 ? 2 : 0; } function slopePreviewCleat(x, y, b, code) { const bx = b % BW, by = (b / BW) | 0, g = beltGrade(code); for (let h = 0; h <= CLEAT_REACH; h++) { const foot = x - g * h; if (foot >= bx * BS && foot < (bx + 1) * BS && foot % CLEAT_SPACING === 0 && y + h + 1 === beltFaceY(code, bx, by, foot)) return true; } return false; }