diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index b7d2028..04f6956 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -388,3 +388,9 @@ above, including:
- **Upgrade ideas for the drill**: an amplifier, a tuning fork, harmonics. Also a Grav pick
upgrade tool and mining charges. See `docs/systems/dig-tools.md`.
- The owner's ideas for later are tasks tagged `idea` in the Backlog of the task board.
+
+- **Sloped belts and filters (t40, 2026-10-25).** The owner asked for belts, then filters, to work
+ diagonally like tubes and walls. A Belt or Filter drag at about 45° lays slope-shaped blocks
+ whose face is a running belt, carrying grains and bars up or down in the drag's direction at
+ flat-belt speed; the face grips its load, so nothing slides back. A sloped filter drops what it
+ passes out underneath and carries the rest. New block codes (19-22), so saves still load.
diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md
index 4223306..4fc3457 100644
--- a/docs/PROJECT_MAP.md
+++ b/docs/PROJECT_MAP.md
@@ -205,10 +205,10 @@ Material definitions: ids, properties and metal families. ids, and drill/tank ti
Material tables: typed properties, casting groups, flags, parts and tool tiers. Typed property tables
- variables: PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, P_NONE, P_TU, P_TD, P_TL, P_TR, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, P_TUR, P_TDR, P_TDL, P_TUL, TUBE_PARTS, TUBE_DIRS, IS_TUBE, TDX, TDY, BIT_TIERS, TANK_TIERS, REACH
-### src/sim/pieces.js (156 lines)
+### src/sim/pieces.js (176 lines)
Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost.
-- functions: isSlope, isWall, slopeCell, slopeCode, isPipeCode, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost
-- variables: BS, BP_WALL, BP_BELT, BP_TUBE, BP_SHAFT, BP_GRATE, BP_GATE, BP_FILTER, BP_PIPE, BP_PUMP, BP_OUTLET, BP_GRIND, BP_SAW, BP_PRESS, BP_RACK, BP_SLOPE_BL, BP_SLOPE_BR, BP_SLOPE_TL, BP_SLOPE_TR, filterCfg, WALL_OF, WALL_LIST, BAR_OF, GRAINS_PER_BAR, POWER_FREE, CASH, PIECES, PIECE_BY_ID, PIECE_BY_CODE, BELT_POWER, TUBE_POWER, FAN_POWER, WHEEL_K, ENGINE_K, MOLDS
+- functions: isSlopeBelt, isFilter, isBelt, beltGrade, slopeShape, beltFaceY, beltDirection, isSlope, isWall, slopeCell, slopeCode, isPipeCode, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost
+- variables: BS, BP_WALL, BP_BELT, BP_TUBE, BP_SHAFT, BP_GRATE, BP_GATE, BP_FILTER, BP_PIPE, BP_PUMP, BP_OUTLET, BP_GRIND, BP_SAW, BP_PRESS, BP_RACK, BP_SLOPE_BL, BP_SLOPE_BR, BP_SLOPE_TL, BP_SLOPE_TR, BP_BELT_UP, BP_BELT_DOWN, BP_FILTER_UP, BP_FILTER_DOWN, filterCfg, WALL_OF, WALL_LIST, BAR_OF, GRAINS_PER_BAR, POWER_FREE, CASH, PIECES, PIECE_BY_ID, PIECE_BY_CODE, BELT_POWER, TUBE_POWER, FAN_POWER, WHEEL_K, ENGINE_K, MOLDS
### src/sim/economy.js (152 lines)
Money, prices, market saturation, the trade dock, buyer and builder orders.
@@ -260,27 +260,27 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
-### src/sim/machine-tick.js (168 lines)
+### src/sim/machine-tick.js (154 lines)
Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick).
- functions: engineIntake, fanBlow, machinesTick
-### src/sim/conveyors.js (147 lines)
+### src/sim/conveyors.js (201 lines)
How belts, filters and screw tubes move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full.
-- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeSpread, tubeIntake, diagonalTubeNext
+- functions: conveyorStep, carryColumn, holdSlopeColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, tubeSpread, tubeIntake, diagonalTubeNext
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
-### src/sim/blocks.js (208 lines)
+### src/sim/blocks.js (219 lines)
Building blocks: placing, integrity, removing, lines and rectangles of blocks.
-- functions: blockOf, blockCellList, pieceCost, slopeSeal, slopeSealOwner, wallCells, canPlaceBlock, placeBlock, blockIntegrity, removeWallCell, removeBlock, diagonalDrag, blockLine, blockRect, blockInReach, blockIs, placeBlocks
+- functions: blockOf, blockCellList, pieceCost, setSlopeSeal, slopeSeal, slopeSealOwner, wallCells, canPlaceBlock, placeBlock, blockIntegrity, removeWallCell, removeBlock, diagonalDrag, blockLine, blockRect, blockInReach, blockIs, placeBlocks
### src/sim/molds.js (317 lines)
Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal.
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
- variables: DROSS, grains, barWord
-### src/sim/items.js (256 lines)
+### src/sim/items.js (288 lines)
Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
-- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entTubeAt, entTubeFree, entTubeMove, entTubeLane, entTubeEdge, entTubeEnter, entTubeTick, entDiagonalTubeTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
+- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entTubeAt, entTubeFree, entTubeMove, entTubeLane, entTubeEdge, entTubeEnter, entTubeTick, entDiagonalTubeTick, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
- variables: BAR_W, BAR_H, entCurM, droppedLast
### src/sim/tools.js (52 lines)
@@ -356,7 +356,11 @@ 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/world.js (410 lines)
+### 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/world.js (422 lines)
Scale colour without changing the smooth-look class.
- functions: fogColor, diagonalTubeJoins, diagonalTubeShape, diagonalTubeBlock, drawWorld
- variables: TUBE_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
@@ -374,7 +378,7 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes
-### src/render/smooth-shaders.js (366 lines)
+### src/render/smooth-shaders.js (393 lines)
Smooth shaders: vertex and fragment programs for the rounded world look.
- variables: SM_VS, SM_FS
@@ -430,16 +434,16 @@ Notebook blueprints: starter designs, saved library and code sharing.
The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, piece
-### src/ui/build-menu.js (109 lines)
+### src/ui/build-menu.js (111 lines)
The build menu along the bottom: categories, piece tiles and their icons.
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
-### src/ui/selection.js (112 lines)
+### src/ui/selection.js (119 lines)
Tool selection: cycling, drill tuning, piece variants and costs.
- functions: pieceSize, pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterBlockAt, chipCost, pieceCostNow, pieceCode, turnSlope
-### src/ui/selection-pickers.js (59 lines)
+### src/ui/selection-pickers.js (61 lines)
Selection pickers: filter rules, filter settings and wall materials. What a filter can hold: loose things and bars you have found, plus anything it already holds
- functions: filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker
@@ -457,7 +461,7 @@ Keyboard and mouse listeners.
- functions: isTyping
- variables: canvas
-### src/ui/actions.js (240 lines)
+### src/ui/actions.js (242 lines)
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
- variables: ovShown
@@ -467,7 +471,7 @@ Gamepad input.
- functions: padPressed, pollPad
- variables: PAD
-### src/ui/previews.js (216 lines)
+### src/ui/previews.js (227 lines)
Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
@@ -518,6 +522,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_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.
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
@@ -549,3 +554,4 @@ The frame loop and boot.
- `tools/scenes/t35-diagonal.js`: Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt.
- `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
- `tools/scenes/t36-funnel.js`: A twelve-block funnel, one block open at the bottom, draining 400 grains of sand.
+- `tools/scenes/t40-sloped-belt.js`: Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below.
diff --git a/docs/agent-log.md b/docs/agent-log.md
index d70829f..b8c72a9 100644
--- a/docs/agent-log.md
+++ b/docs/agent-log.md
@@ -4,6 +4,16 @@ 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: 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
+ belts and filters whose 45° face runs, laid by a diagonal Belt or Filter drag. `conveyorStep`,
+ `holdSlopeColumn` and a shared front-first order for belts and filters in `src/sim/conveyors.js`;
+ bars in `items.js`; saves, blueprints, undo, hover, previews; `src/render/slope-belts.js`.
+- 90 grains/s flat, up and down for belts and filters; a sloped filter sorted 64/64 with no loss.
+ Seeds 777, 77 and 4242 generate identically.
+- `test/t_slope_belts.js` (32 checks); `run_all.py --browser` ALL PASSED. Art pass follows.
+
## 2026-10-25: t35 follow-up: diagonal tube elbows (Claude Code, Codex)
- Diagonal tubes are as thick as straight ones, and an end block joining a straight tube, belt or
diff --git a/src/manifest.json b/src/manifest.json
index 532c951..4a33a0e 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -41,6 +41,7 @@
"client": [
"render/setup.js",
"render/light.js",
+ "render/slope-belts.js",
"render/world.js",
"render/machines.js",
"render/overlays.js",
diff --git a/src/render/setup.js b/src/render/setup.js
index bdeb7de..e60da43 100644
--- a/src/render/setup.js
+++ b/src/render/setup.js
@@ -117,7 +117,7 @@ function thinPipeColor(x, y, anim) {
PC[0] = r; PC[1] = g; PC[2] = bl;
}
// a block that is a belt or a filter: they join up into one belt
-function beltLike(b) { if (b < 0 || b >= NB) return false; const c = blockP[b]; return c === BP_BELT || c === BP_FILTER; }
+function beltLike(b) { if (b < 0 || b >= NB) return false; const c = blockP[b]; return isBelt(c) || isFilter(c); }
// standing trees: the trunk shows its dark bark, so it reads apart from the pale cut wood it drops
const TREE_R = new Uint8Array(NMAT), TREE_G = new Uint8Array(NMAT), TREE_B = new Uint8Array(NMAT);
for (let m = 0; m < NMAT; m++) { TREE_R[m] = COLR[m]; TREE_G[m] = COLG[m]; TREE_B[m] = COLB[m]; }
diff --git a/src/render/slope-belts.js b/src/render/slope-belts.js
new file mode 100644
index 0000000..7e3f9db
--- /dev/null
+++ b/src/render/slope-belts.js
@@ -0,0 +1,28 @@
+// 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;
+}
+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; }
+ }
+ } 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);
+}
diff --git a/src/render/smooth-shaders.js b/src/render/smooth-shaders.js
index ec56109..bf5ae7b 100644
--- a/src/render/smooth-shaders.js
+++ b/src/render/smooth-shaders.js
@@ -51,8 +51,8 @@ float beltSD(vec2 lb, bool endL, bool endR) {
// 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 <= 22),
- isBelt(nr) + isF(nr) < 0.5 && !(nr >= 19 && nr <= 22));
+ 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);
}
// 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.
@@ -352,6 +352,33 @@ 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) {
diff --git a/src/render/world.js b/src/render/world.js
index 66e6024..cc28eb3 100644
--- a/src/render/world.js
+++ b/src/render/world.js
@@ -281,6 +281,11 @@ 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 color = slopeBeltColor(sb, x, y, anim, br, bgc, bb);
+ r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255;
+ }
// incandescence
if (T > 450 && m !== EMPTY && incand) {
const gi = glowIndex(PH[m] === PH_SOLID ? T + nz * 0.9 : m === MAGMA ? T + Math.sin(x * 0.19 + y * 0.11 + anim * 0.03 + nz * 0.04) * 55 - 30 : T), w = GLOW_W[gi];
@@ -296,9 +301,9 @@ function drawWorld() {
// what kind of thing this grain is drawn as: 0 open, 1 terrain, 2 loose grain, 3 liquid,
// 4 built piece (drawn crisp), 5 smoke and flame, 6 built wall block (drawn as a tile)
let cl = 0;
- const pt = p === P_PIPE && !pipeBodyCell(x, y) ? 0 : p; // the open parts of a pipe block show what is in them
+ const pt = (p === P_PIPE && !pipeBodyCell(x, y)) || (p === P_FILTER && sb >= 0) ? 0 : p; // the open parts of a pipe block show what is in them
if (IS_TUBE[pt]) cl = TUBE_CLASS[pt];
- else if (p === P_FILTER) {
+ else if (p === P_FILTER && sb < 0) {
// 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;
@@ -319,6 +324,13 @@ function drawWorld() {
const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]];
r = COLR[wm]; g = COLG[wm]; b = COLB[wm];
}
+ if (slopeArt) {
+ 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);
+ }
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);
}
diff --git a/src/sim/blocks.js b/src/sim/blocks.js
index 5c3d661..0b2f81f 100644
--- a/src/sim/blocks.js
+++ b/src/sim/blocks.js
@@ -3,33 +3,42 @@
function blockOf(x, y) { return { bx: Math.floor(x / BS), by: Math.floor(y / BS) }; }
function blockCellList(bx, by) { const out = []; for (let y = by * BS; y < by * BS + BS; y++) for (let x = bx * BS; x < bx * BS + BS; x++) out.push(y * W + x); return out; }
function pieceCost(code, v) { return isWall(code) ? wallCost(v, isSlope(code) ? 8 : BS * BS) : PIECE_BY_CODE[code].cost; }
+function setSlopeSeal(i, code, v) {
+ if (isFilter(code)) { part[i] = P_FILTER; wakeIdx(i); }
+ else {
+ setCell(i, isBelt(code) ? (v ? BELTR : BELTL) : WALL_OF[v], AMB[(i / W) | 0], 0);
+ if (isBelt(code)) markPow(i, true);
+ }
+}
// One extra cell on the solid side seals diagonal crossings between successive triangles.
function slopeSeal(code, bx, by) {
+ code = slopeShape(code);
const x = bx * BS + (code === BP_SLOPE_BR || code === BP_SLOPE_TR ? 0 : 3);
const y = by * BS + (code >= BP_SLOPE_TL ? -1 : 4);
return y < 0 || y >= H ? -1 : y * W + x;
}
function slopeSealOwner(i) {
const x = i % W, y = (i / W) | 0, bx = x >> 2;
- for (const by of [(y >> 2) - 1, (y >> 2) + 1]) {
+ if (((x & 3) !== 0 && (x & 3) !== 3) || ((y & 3) !== 0 && (y & 3) !== 3)) return -1;
+ for (let by = (y >> 2) - 1; by <= (y >> 2) + 1; by += 2) {
if (by < 0 || by >= BH) continue;
const b = by * BW + bx, c = blockP[b];
- if (isSlope(c) && slopeSeal(c, bx, by) === i) return b;
+ if ((isSlope(c) || isSlopeBelt(c)) && slopeSeal(c, bx, by) === i) return b;
}
return -1;
}
function wallCells(code, bx, by) {
- return blockCellList(bx, by).filter(i => !isSlope(code) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3));
+ return blockCellList(bx, by).filter(i => !(isSlope(code) || isSlopeBelt(code)) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3));
}
function canPlaceBlock(bx, by, code = BP_WALL) {
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return 'Out of bounds';
if (blockP[by * BW + bx]) return 'Something is already built there';
for (const i of blockCellList(bx, by)) {
- if (mach[i] || part[i]) return 'Something is already built there';
+ if (mach[i] || (part[i] && slopeSealOwner(i) < 0)) return 'Something is already built there';
if (PH[mat[i]] === PH_SOLID && ((flags[i] & F_NAT) || slopeSealOwner(i) < 0)) return 'Needs open space; clear the rock first.';
if (occ[i]) return 'A bar is in the way';
}
- if (isSlope(code)) {
+ if (isSlope(code) || isSlopeBelt(code)) {
const i = slopeSeal(code, bx, by);
if (i >= 0 && PH[mat[i]] !== PH_SOLID && (mach[i] || part[i] || occ[i])) return 'The slope corner is blocked.';
}
@@ -41,25 +50,25 @@ function placeBlock(code, v, bx, by, free, extra) {
if (isWall(code) && !WALL_OF[v]) return 'Pick a wall material with R';
const cost = pieceCost(code, v);
if (!free) { if (!canAfford(cost, 1)) return needText(cost, 1); payCost(cost, 1); }
- const cells = isSlope(code) ? wallCells(code, bx, by) : blockCellList(bx, by);
+ const cells = isSlope(code) || isSlopeBelt(code) ? wallCells(code, bx, by) : blockCellList(bx, by);
if (code !== BP_PIPE) evictCells(cells, by * BS); // a pipe is thin: what is there stays and flows past it
for (const i of cells) {
const y = (i / W) | 0, x = i % W;
if (isWall(code)) setCell(i, WALL_OF[v], AMB[y], 0);
- else if (code === BP_BELT) { setCell(i, v === 0 ? BELTL : BELTR, AMB[y], 0); markPow(i, true); }
+ else if (isBelt(code)) { setCell(i, v === 0 ? BELTL : BELTR, AMB[y], 0); markPow(i, true); }
else if (code === BP_SHAFT) { setCell(i, SHAFT, AMB[y], 0); markPow(i, true); }
else if (code === BP_RACK) setCell(i, RACKB, AMB[y], 0);
else if (isProcCode(code)) setCell(i, code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB, AMB[y], 0);
else if (code === BP_GATE) setCell(i, GATE, AMB[y], 0);
else if (code === BP_GRATE) { part[i] = P_GRATE; wakeXY(x, y); }
else if (code === BP_TUBE) { part[i] = TUBE_PARTS[v & 7]; markPow(i, true); wakeXY(x, y); }
- else if (code === BP_FILTER) { part[i] = P_FILTER; wakeXY(x, y); }
+ else if (isFilter(code)) { part[i] = P_FILTER; wakeXY(x, y); }
else if (code === BP_PIPE) { part[i] = P_PIPE; wakeXY(x, y); }
else if (isPipeCode(code)) setCell(i, PIPE, AMB[y], 0);
}
if (isPipeCode(code)) pipesDirty = true;
blockP[by * BW + bx] = code; blockV[by * BW + bx] = v;
- if (code === BP_FILTER) { blockV[by * BW + bx] = v & 128; filterCfg.set(by * BW + bx, filterRec(extra)); filtersDirty = true; }
+ if (isFilter(code)) { blockV[by * BW + bx] = v & 128; filterCfg.set(by * BW + bx, filterRec(extra)); filtersDirty = true; }
if (isProcCode(code)) procDirty = true;
if (code === BP_RACK) {
const b = by * BW + bx, s = newRack();
@@ -67,9 +76,9 @@ function placeBlock(code, v, bx, by, free, extra) {
rackState.set(b, s);
for (let q = rackTop(b); q <= rackBottom(b); q += BW) rackState.get(q).open = s.open;
}
- if (isSlope(code)) {
+ if (isSlope(code) || isSlopeBelt(code)) {
const i = slopeSeal(code, bx, by);
- if (i >= 0 && PH[mat[i]] !== PH_SOLID) { evictCells([i], by * BS); setCell(i, WALL_OF[v], AMB[(i / W) | 0], 0); }
+ if (i >= 0 && PH[mat[i]] !== PH_SOLID && !part[i]) { evictCells([i], by * BS); setSlopeSeal(i, code, v); }
}
stats.placed++;
undoNote({ t: 'block+', bx, by, code });
@@ -84,14 +93,14 @@ function blockIntegrity(b) {
for (const i of blockCellList(bx, by)) {
const m = mat[i], p = part[i];
if (isWall(code) && (!isSlope(code) || slopeCell(code, (i % W) & 3, ((i / W) | 0) & 3))) { if (PH[m] === PH_SOLID && !(flags[i] & F_NAT) && !PARTMAT[m]) n++; }
- else if (code === BP_BELT) { if (m === BELTL || m === BELTR) n++; }
+ else if (isBelt(code)) { if (m === BELTL || m === BELTR) n++; }
else if (code === BP_SHAFT) { if (m === SHAFT) n++; }
else if (code === BP_RACK) { if (m === RACKB) n++; }
else if (isProcCode(code)) { if (m === (code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB)) n++; }
else if (code === BP_GATE) { if (m === GATE || p === P_GOPEN) n++; }
else if (code === BP_GRATE) { if (p === P_GRATE) n++; }
else if (code === BP_TUBE) { if (IS_TUBE[p]) n++; }
- else if (code === BP_FILTER) { if (p === P_FILTER) n++; }
+ else if (isFilter(code)) { if (p === P_FILTER) n++; }
else if (isPipeCode(code)) { if (m === PIPE || p === P_PIPE) n++; }
}
void v;
@@ -109,7 +118,7 @@ function removeWallCell(i, wall) {
function removeBlock(bx, by) {
const b = by * BW + bx, code = blockP[b], v = blockV[b];
if (!code) return 0;
- undoNote({ t: 'block-', bx, by, code, v, extra: code === BP_FILTER ? filterInfo(b) : undefined });
+ undoNote({ t: 'block-', bx, by, code, v, extra: isFilter(code) ? filterInfo(b) : undefined });
// a pipe taken out spills its share of what the line holds
const spill = isPipeCode(code) ? pipeTakeShare(b) : null;
let n = 0, metal = 0;
@@ -125,7 +134,7 @@ function removeBlock(bx, by) {
} else if (isPipeCode(code)) {
if (p === P_PIPE) { part[i] = 0; wakeIdx(i); n++; }
else if (m === PIPE) { setCell(i, EMPTY, temp[i], 0); n++; }
- } else if (code === BP_BELT || code === BP_SHAFT || code === BP_GATE || code === BP_RACK || isProcCode(code)) {
+ } else if (isBelt(code) || code === BP_SHAFT || code === BP_GATE || code === BP_RACK || isProcCode(code)) {
if (m === BELTL || m === BELTR || m === SHAFT || m === GATE || m === GRINDER || m === SAWB || m === PRESSB || m === RACKB) { setCell(i, EMPTY, temp[i], 0); markPow(i, false); n++; }
if (p === P_GOPEN) { part[i] = 0; n++; wakeIdx(i); }
} else if (p) { part[i] = 0; markPow(i, false); wakeIdx(i); n++; }
@@ -137,7 +146,7 @@ function removeBlock(bx, by) {
}
if (code === BP_WALL && metal) giveOrDrop(BAR_OF[WALL_OF[v]] || BARCU, Math.floor(metal / GRAINS_PER_BAR), bx * BS + 2, by * BS + 2);
else if (!isWall(code) && n >= 8) refundAt(PIECE_BY_CODE[code].cost, 1, bx * BS + 2, by * BS + 2);
- if (code === BP_FILTER) { filtersDirty = true; filterCfg.delete(b); }
+ if (isFilter(code)) { filtersDirty = true; filterCfg.delete(b); }
if (isProcCode(code)) { procDirty = true; procState.delete(b); }
if (code === BP_RACK) {
const s = rackState.get(b);
@@ -146,17 +155,19 @@ function removeBlock(bx, by) {
}
blockP[b] = 0; blockV[b] = 0;
const cleared = blockCellList(bx, by);
- if (isSlope(code)) {
+ if (isSlope(code) || isSlopeBelt(code)) {
const i = slopeSeal(code, bx, by);
if (i >= 0) {
const nb = (((i / W) | 0) >> 2) * BW + ((i % W) >> 2), nc = blockP[nb];
- if ((!nc || (isSlope(nc) && !slopeCell(nc, (i % W) & 3, ((i / W) | 0) & 3))) && mat[i] === WALL_OF[v] && !(flags[i] & F_NAT)) setCell(i, EMPTY, temp[i], 0);
+ if ((!nc || (isSlope(nc) && !slopeCell(nc, (i % W) & 3, ((i / W) | 0) & 3))) && (mat[i] === (isBelt(code) ? (v ? BELTR : BELTL) : WALL_OF[v]) || (isFilter(code) && part[i] === P_FILTER)) && !(flags[i] & F_NAT)) { if (isFilter(code)) { part[i] = 0; wakeIdx(i); } else { setCell(i, EMPTY, temp[i], 0); if (isBelt(code)) markPow(i, false); } }
cleared.push(i);
}
}
for (const i of cleared) {
const owner = slopeSealOwner(i);
- if (owner >= 0 && mat[i] === EMPTY) setCell(i, WALL_OF[blockV[owner]], AMB[(i / W) | 0], 0);
+ if (owner >= 0 && mat[i] === EMPTY && !part[i]) {
+ setSlopeSeal(i, blockP[owner], blockV[owner]);
+ }
}
if (isPipeCode(code)) {
pipesDirty = true;
@@ -186,7 +197,7 @@ function blockInReach(bx, by) { return inReach(bx * BS + BS / 2, by * BS + BS /
// Is block b already exactly this piece (same kind, variant and, for a filter, settings)?
function blockIs(b, code, v, extra) {
if (blockP[b] !== code) return false;
- if (code === BP_FILTER) return (blockV[b] & 128) === (v & 128) && sameFilter(filterInfo(b), cleanFilterCfg(extra));
+ if (isFilter(code)) return (blockV[b] & 128) === (v & 128) && sameFilter(filterInfo(b), cleanFilterCfg(extra));
return blockV[b] === v;
}
// replace: a block already built in the way is taken down (and refunded) to make room
@@ -197,7 +208,7 @@ function placeBlocks(code, v, list, extra, replace) {
const b = by * BW + bx;
if (replace && blockP[b]) {
if (blockIs(b, code, v, extra)) continue;
- if (blockP[b] === BP_FILTER && code === BP_FILTER) { undoNote({ t: 'filter', bx, by, v: blockV[b], extra: filterInfo(b) }); blockV[b] = v & 128; setFilter(b, extra); filtersDirty = true; n++; continue; } // just re-set it
+ if (blockP[b] === code && isFilter(code)) { undoNote({ t: 'filter', bx, by, v: blockV[b], extra: filterInfo(b) }); blockV[b] = v & 128; setFilter(b, extra); filtersDirty = true; n++; continue; } // just re-set it
if (!canAfford(pieceCost(code, v), 1)) { last = needText(pieceCost(code, v), 1); break; }
removeBlock(bx, by);
}
diff --git a/src/sim/blueprints.js b/src/sim/blueprints.js
index c3fce38..ef1f061 100644
--- a/src/sim/blueprints.js
+++ b/src/sim/blueprints.js
@@ -7,7 +7,7 @@ function copyRegion(x0, y0, x1, y1) {
const blocks = [], ms = [], ols = [];
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
const b = by * BW + bx;
- if (blockP[b] && blockIntegrity(b) >= 8) blocks.push(blockP[b] === BP_FILTER ? [bx - bx0, by - by0, blockP[b], blockV[b], filterInfo(b)] : [bx - bx0, by - by0, blockP[b], blockV[b]]);
+ if (blockP[b] && blockIntegrity(b) >= 8) blocks.push(isFilter(blockP[b]) ? [bx - bx0, by - by0, blockP[b], blockV[b], filterInfo(b)] : [bx - bx0, by - by0, blockP[b], blockV[b]]);
}
for (const mm of machines) {
if (!mm || PIECE_BY_ID[mm.type].fixed) continue;
@@ -23,11 +23,11 @@ function copyRegion(x0, y0, x1, y1) {
}
function flipBlueprint(bp) {
const bw = bp.bw;
- const flipV = (code, v) => code === BP_BELT ? 1 - v : code === BP_FILTER ? v ^ 128 : code === BP_TUBE ? [0, 3, 2, 1, 7, 6, 5, 4][v & 7]
+ const flipV = (code, v) => isBelt(code) ? 1 - v : isFilter(code) ? v ^ 128 : code === BP_TUBE ? [0, 3, 2, 1, 7, 6, 5, 4][v & 7]
: code === BP_PUMP || code === BP_OUTLET ? (((v & 3) === 1 ? 3 : (v & 3) === 3 ? 1 : v & 3) | (v & 4)) : v;
return {
v: 2, bw, bh: bp.bh, name: bp.name,
- blocks: bp.blocks.map(([x, y, c, v, e]) => e ? [bw - 1 - x, y, isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v), e] : [bw - 1 - x, y, isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v)]),
+ blocks: bp.blocks.map(([x, y, c, v, e]) => e ? [bw - 1 - x, y, isSlopeBelt(c) ? c + (c & 1 ? 1 : -1) : isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v), e] : [bw - 1 - x, y, isSlopeBelt(c) ? c + (c & 1 ? 1 : -1) : isSlope(c) ? BP_SLOPE_BL + ((c - BP_SLOPE_BL) ^ 1) : c, flipV(c, v)]),
machines: bp.machines.map(([t, x, y, d]) => {
const P = PIECE_BY_ID[t];
let nd = d;
diff --git a/src/sim/conveyors.js b/src/sim/conveyors.js
index 4f05671..637e4b9 100644
--- a/src/sim/conveyors.js
+++ b/src/sim/conveyors.js
@@ -4,7 +4,24 @@
// clear). If a powder grain is blocked, the load above it stands still and the belt slips
// underneath. A filter's own material drops through it instead, unless it is already full.
const CARRY_DEPTH = 96;
+// Find the next carrying face at the same height or one grain above/below it.
+function conveyorStep(x, y, d) {
+ const nx = x + d;
+ if (nx < 0 || nx >= W) return 0;
+ if ((x >> 2) === (nx >> 2)) return beltGrade(blockP[(y >> 2) * BW + (x >> 2)]) * d;
+ for (let dy = -1; dy <= 1; dy++) {
+ const yy = y + dy;
+ if (yy < 0 || yy >= H) continue;
+ const b = (yy >> 2) * BW + (nx >> 2), c = blockP[b], i = yy * W + nx;
+ if ((isBelt(c) || isFilter(c)) && beltFaceY(c, nx >> 2, yy >> 2, nx) === yy &&
+ (mat[i] === BELTL || mat[i] === BELTR || part[i] === P_FILTER)) return dy;
+ }
+ const b = (y >> 2) * BW + (x >> 2);
+ return beltGrade(blockP[b]) * d;
+}
function carryColumn(a, d, pass) {
+ if (a < 0 || mat[a] === EMPTY) return;
+ const sy = ((a / W) | 0) + 1, step = conveyorStep(a % W, sy, d);
let blocked = false;
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
if (a < 0) return;
@@ -20,20 +37,30 @@ function carryColumn(a, d, pass) {
if (blocked) continue; // filters still sift above a stopped load
const x = a % W, nx = x + d;
if (nx < 0 || nx >= W) return;
- const j = a + d, tj = mat[j];
+ const j = a + d + step * W;
+ if (j < 0 || j >= N) return;
+ const tj = mat[j];
// grains on a wet belt push through the water rather than stopping at it
const wet = PH[tj] === PH_LIQUID && !occ[j] && stamp[j] !== tickStamp && (!part[j] || part[j] === P_FILTER || part[j] === P_GOPEN);
- if (!wet && !enter(m, PH_POWDER, j, d, 0)) {
+ if (!wet && !enter(m, PH_POWDER, j, d, step)) {
if (!pass) return;
blocked = true; continue;
}
const y = (a / W) | 0, bj = j + W;
// carried off the end with nothing under it: it leaves at belt speed and falls
- if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y, d * 0.5, 0);
- else move(a, j, x, y, nx, y);
+ if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y + step, d * 0.5, step * 0.5);
+ else move(a, j, x, y, nx, y + step);
stats.moved++;
}
}
+// The running face grips its whole supported load, including on the ticks between steps.
+function holdSlopeColumn(a, pass) {
+ for (let depth = 0; depth < CARRY_DEPTH && a >= 0; depth++, a -= W) {
+ const m = mat[a];
+ if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return;
+ if (PH[m] === PH_POWDER && !occ[a] && !(pass && pass[m])) stamp[a] = tickStamp;
+ }
+}
// Belts are perforated: liquid resting on one drips through to whatever open space is under it.
function beltDrain(b) {
const a = b - W, x = b % W;
@@ -53,23 +80,48 @@ function launchGrain(i, x, y, vx, vy) {
let filterR = [], filterL = [], filtersDirty = true;
function rebuildFilters() {
filtersDirty = false; filterR = []; filterL = [];
- for (let b = 0; b < NB; b++) if (blockP[b] === BP_FILTER) (blockV[b] & 128 ? filterR : filterL).push(b);
+ for (let b = 0; b < NB; b++) if (isFilter(blockP[b])) (blockV[b] & 128 ? filterR : filterL).push(b);
// front of the line first, so a full run moves as one
filterR.sort((a, c) => (c % BW) - (a % BW)); filterL.sort((a, c) => (a % BW) - (c % BW));
}
-function filterCarry(b, d) {
- if (blockP[b] !== BP_FILTER) return;
- const bx = b % BW, y = ((b / BW) | 0) * BS;
+function filterCarry(b, d, carrying = true) {
+ if (!isFilter(blockP[b])) return;
+ const bx = b % BW, by = (b / BW) | 0, code = blockP[b], y = by * BS;
if (y < 1) return;
+ if (!carrying && !isSlopeBelt(code)) return;
const c = filterCfg.get(b), pass = c ? c.pass : null;
for (let k = 0; k < BS; k++) {
- const x = d > 0 ? bx * BS + BS - 1 - k : bx * BS + k, top = y * W + x;
- if (part[top] === P_FILTER) carryColumn(top - W, d, pass);
+ const x = d > 0 ? bx * BS + BS - 1 - k : bx * BS + k, top = beltFaceY(code, bx, by, x) * W + x;
+ if (part[top] === P_FILTER) {
+ if (carrying) carryColumn(top - W, d, pass);
+ if (isSlopeBelt(code)) holdSlopeColumn(top - W, pass);
+ }
+ }
+}
+// Belts and filters share one front-first order, including their flat/slope joins.
+function carryBeltsAndFilters(belts, filters, d, bm, carrying) {
+ let k = 0, f = 0;
+ while (k < belts.length || f < filters.length) {
+ const fx = f < filters.length ? (filters[f] % BW) * BS + (d > 0 ? BS - 1 : 0) : 0;
+ if (f < filters.length && (k >= belts.length || (fx - belts[k] % W) * d > 0)) { filterCarry(filters[f++], d, carrying); continue; }
+ const b = belts[k++];
+ if (mat[b] !== bm || b < W) continue;
+ const x = b % W, y = (b / W) | 0, code = blockP[(y >> 2) * BW + (x >> 2)];
+ if (!carrying && !isSlopeBelt(code)) continue;
+ if (isSlopeBelt(code) && beltFaceY(code, x >> 2, y >> 2, x) !== y) continue;
+ if (slopeSealOwner(b) >= 0) continue;
+ const above = mat[b - W];
+ if (above === BELTL || above === BELTR || netSat(b) <= 0) continue;
+ if (carrying) {
+ if (PH[above] === PH_LIQUID) beltDrain(b);
+ carryColumn(b - W, d, null);
+ }
+ if (isSlopeBelt(code)) holdSlopeColumn(b - W, null);
}
}
// Change what a filter does; grains inside it that no longer pass are pushed out on top.
function setFilter(b, cfg) {
- if (blockP[b] !== BP_FILTER) return false;
+ if (!isFilter(blockP[b])) return false;
const rec = filterRec(cfg);
filterCfg.set(b, rec);
const bx = b % BW, by = (b / BW) | 0;
@@ -79,11 +131,13 @@ function setFilter(b, cfg) {
}
// The run of filters in a row that touch block b and do the same thing it does
function filterRun(b) {
- if (blockP[b] !== BP_FILTER) return [];
+ if (!isFilter(blockP[b])) return [];
const f = filterInfo(b), dir = blockV[b] & 128, by = (b / BW) | 0, out = [b];
for (const s of [-1, 1]) for (let bx = (b % BW) + s; bx >= 0 && bx < BW; bx += s) {
- const c = by * BW + bx;
- if (blockP[c] !== BP_FILTER || (blockV[c] & 128) !== dir || !sameFilter(filterInfo(c), f)) break;
+ const yy = by + beltGrade(blockP[b]) * (bx - (b % BW));
+ if (yy < 0 || yy >= BH) break;
+ const c = yy * BW + bx;
+ if (blockP[c] !== blockP[b] || (blockV[c] & 128) !== dir || !sameFilter(filterInfo(c), f)) break;
out.push(c);
}
return out.sort((a, c) => a - c);
diff --git a/src/sim/items.js b/src/sim/items.js
index c3ea375..116f20e 100644
--- a/src/sim/items.js
+++ b/src/sim/items.js
@@ -59,9 +59,10 @@ function entTubeFree(x, y, tubeOnly = false) {
function entTubeMove(e, x, y, tubeOnly = false, mouth = false) {
if (!entTubeFree(x, y, tubeOnly)) return false;
if (mouth) {
- // Lift clear of the receiving belt before sliding over it; check both legs before moving.
- for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++) if (!entTubeFree(e.ix, yy)) return false;
- for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, y)) return false;
+ // Go around the receiving corner, checking both legs before moving.
+ const acrossY = y > e.iy ? e.iy : y, downX = y > e.iy ? x : e.ix;
+ for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++) if (!entTubeFree(downX, yy)) return false;
+ for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, acrossY)) return false;
} else for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++)
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, yy)) return false;
e.x = e.ix = x; e.y = e.iy = y; e.vx = e.vy = 0; return true;
@@ -71,7 +72,7 @@ function entTubeEdge(e, dx, dy, k) {
const x = e.ix + (dx > 0 ? BAR_W : dx < 0 ? -1 : k), y = e.iy + (dy > 0 ? BAR_H : dy < 0 ? -1 : k);
return x < 0 || y < 0 || x >= W || y >= H ? -1 : y * W + x;
}
-function entTubeEnter(e, dx, dy) {
+function entTubeEnter(e, dx, dy, mouth = false) {
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const i = entTubeEdge(e, dx, dy, k);
if (i < 0) continue;
@@ -79,7 +80,7 @@ function entTubeEnter(e, dx, dy) {
const p = part[i]; if (!IS_TUBE[p] || (!dx && TDX[p]) || dx * TDX[p] + dy * TDY[p] < 0) continue;
// Vertical screws require the full width; horizontal screws use the nearest two-row lane.
const x = (xx & ~3), y = entTubeLane(yy, e.iy);
- if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true)) return true;
+ if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true, mouth)) return true;
}
return false;
}
@@ -129,6 +130,37 @@ function entDiagonalTubeTick(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 (entTubeMove(e, x, y, false, dy < 0)) { e.vx = dx * 0.5; e.vy = dy * 0.6; stats.moved++; }
}
+// Bars stay level; their highest contact follows the face one grain per belt beat.
+function entSlopeBeltTick(e) {
+ const bottom = e.iy + BAR_H;
+ if (bottom >= H) return false;
+ let d = 0, slope = false;
+ for (let x = e.ix; x < e.ix + BAR_W; x++) {
+ const i = bottom * W + x, b = (bottom >> 2) * BW + (x >> 2), c = blockP[b];
+ if ((isBelt(c) || isFilter(c)) && entSolidCell(i)) {
+ d = isFilter(c) ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
+ if (isSlopeBelt(c)) slope = true;
+ }
+ }
+ if (!d) return false;
+ let face = H;
+ for (let x = e.ix + d; x < e.ix + d + BAR_W; x++) for (let dy = -1; dy <= 1; dy++) {
+ const y = bottom + dy;
+ if (x < 0 || x >= W || y < 0 || y >= H) continue;
+ const b = (y >> 2) * BW + (x >> 2), c = blockP[b];
+ if (!(isBelt(c) || isFilter(c)) || (isFilter(c) && filterPassAt(e.m, y * W + x))) continue;
+ if (beltFaceY(c, x >> 2, y >> 2, x) !== y) continue;
+ if (isSlopeBelt(c)) slope = true;
+ face = Math.min(face, y);
+ }
+ if (!slope) return false;
+ e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
+ if (tick & 1) return true;
+ const y = face === H ? e.iy : face - BAR_H, x = e.ix + d;
+ if (entFreeAt(x, y)) { e.vx = d * 0.5; e.vy = (y - e.iy) * 0.5; e.x = e.ix = x; e.y = e.iy = y; stats.moved++; }
+ else if (!entTubeEnter(e, d, 0)) entTubeEnter(e, d, Math.sign(y - e.iy), true);
+ return true;
+}
function entTick() {
for (let k = ents.length - 1; k >= 0; k--) {
const e = ents[k];
@@ -137,7 +169,7 @@ function entTick() {
let ix = e.ix; const iy = e.iy;
const tube = entTubeAt(ix, iy);
if (tube) entTubeTick(e, tube);
- else {
+ else if (!entSlopeBeltTick(e)) {
// what is underneath?
let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H;
if (!support) {
diff --git a/src/sim/machine-tick.js b/src/sim/machine-tick.js
index aeb03fb..b600b61 100644
--- a/src/sim/machine-tick.js
+++ b/src/sim/machine-tick.js
@@ -87,24 +87,10 @@ function machinesTick() {
mm.hot = Math.round(hotT);
}
}
- // belts and filters move what rests on them every other tick: half a grain per tick
- if ((tick & 1) === 0) {
- for (let pass = 0; pass < 2; pass++) {
- const list = pass ? beltR : beltL, d = pass ? 1 : -1, bm = pass ? BELTR : BELTL;
- for (let k = 0; k < list.length; k++) {
- const b = list[k];
- if (mat[b] !== bm || b < W) continue;
- const above = mat[b - W];
- if (above === BELTL || above === BELTR) continue; // only the top surface carries
- if (netSat(b) <= 0) continue;
- if (PH[above] === PH_LIQUID) beltDrain(b);
- carryColumn(b - W, d, null);
- }
- }
- if (filtersDirty) rebuildFilters();
- for (const b of filterR) filterCarry(b, 1);
- for (const b of filterL) filterCarry(b, -1);
- }
+ // Half a grain per tick; sloped faces grip their load on the intervening ticks too.
+ if (filtersDirty) rebuildFilters();
+ carryBeltsAndFilters(beltL, filterL, -1, BELTL, (tick & 1) === 0);
+ carryBeltsAndFilters(beltR, filterR, 1, BELTR, (tick & 1) === 0);
// tubes: every loose grain inside steps one cell along, front of the line first, on the same
// beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt
if ((tick & 1) === 0) for (let p = 1; p < IS_TUBE.length; p++) {
diff --git a/src/sim/machines.js b/src/sim/machines.js
index c9f2885..5fd1594 100644
--- a/src/sim/machines.js
+++ b/src/sim/machines.js
@@ -44,7 +44,7 @@ function rebuildPower() {
if (!net) continue;
if (mm.type === 'fan') net.fans.push(mm); else if (mm.type === 'wheel' || mm.type === 'engine') net.prods.push(mm);
}
- beltL.sort((a, b) => a - b); beltR.sort((a, b) => b - a);
+ beltL.sort((a, b) => (a % W) - (b % W) || a - b); beltR.sort((a, b) => (b % W) - (a % W) || b - a);
for (const p of TUBE_PARTS) {
tubeLists[p].sort((a, b) => {
if (TDX[p] && TDY[p]) return ((b % W) - (a % W)) * TDX[p];
diff --git a/src/sim/pieces.js b/src/sim/pieces.js
index 14b04f7..f4d98d3 100644
--- a/src/sim/pieces.js
+++ b/src/sim/pieces.js
@@ -6,9 +6,22 @@
const BS = 4;
const BP_WALL = 1, BP_BELT = 2, BP_TUBE = 3, BP_SHAFT = 4, BP_GRATE = 5, BP_GATE = 6, BP_FILTER = 7, BP_PIPE = 8, BP_PUMP = 9, BP_OUTLET = 10, BP_GRIND = 11, BP_SAW = 12, BP_PRESS = 13, BP_RACK = 14;
const BP_SLOPE_BL = 15, BP_SLOPE_BR = 16, BP_SLOPE_TL = 17, BP_SLOPE_TR = 18;
+// The shape is independent of the direction: reversing a belt never flips its face.
+const BP_BELT_UP = 19, BP_BELT_DOWN = 20, BP_FILTER_UP = 21, BP_FILTER_DOWN = 22;
+function isSlopeBelt(c) { return c >= BP_BELT_UP && c <= BP_FILTER_DOWN; }
+function isFilter(c) { return c === BP_FILTER || c === BP_FILTER_UP || c === BP_FILTER_DOWN; }
+function isBelt(c) { return c === BP_BELT || c === BP_BELT_UP || c === BP_BELT_DOWN; }
+function beltGrade(c) { return c === BP_BELT_UP || c === BP_FILTER_UP ? -1 : c === BP_BELT_DOWN || c === BP_FILTER_DOWN ? 1 : 0; }
+function slopeShape(c) { return isSlopeBelt(c) ? (beltGrade(c) < 0 ? BP_SLOPE_BR : BP_SLOPE_BL) : c; }
+function beltFaceY(c, bx, by, x) { return by * BS + (beltGrade(c) < 0 ? 3 - (x - bx * BS) : beltGrade(c) > 0 ? x - bx * BS : 0); }
+function beltDirection(c, v) {
+ const d = isFilter(c) ? (v & 128 ? 1 : -1) : (v ? 1 : -1), g = beltGrade(c);
+ return g ? (g * d < 0 ? 'up-' : 'down-') + (d > 0 ? 'right' : 'left') : d > 0 ? 'right' : 'left';
+}
function isSlope(c) { return c >= BP_SLOPE_BL && c <= BP_SLOPE_TR; }
function isWall(c) { return c === BP_WALL || isSlope(c); }
function slopeCell(c, x, y) {
+ c = slopeShape(c);
if (c === BP_SLOPE_BR || c === BP_SLOPE_TR) x = 3 - x;
if (c === BP_SLOPE_TL || c === BP_SLOPE_TR) y = 3 - y;
return y >= x;
@@ -30,7 +43,12 @@ function filterRec(c) {
for (let m = 1; m < NMAT; m++) if (filterable(m)) pass[m] = (k.m.includes(m) ? 1 : 0) ^ k.inv;
return { m: k.m, inv: k.inv, pass };
}
-function filterPassAt(m, j) { const c = filterCfg.get((((j / W) | 0) >> 2) * BW + ((j % W) >> 2)); return c ? c.pass[m] : 0; }
+function filterPassAt(m, j) {
+ let b = (((j / W) | 0) >> 2) * BW + ((j % W) >> 2);
+ if (!isFilter(blockP[b])) { const owner = slopeSealOwner(j); if (owner >= 0) b = owner; }
+ const c = filterCfg.get(b);
+ return c ? c.pass[m] : 0;
+}
function filterInfo(b) { const c = filterCfg.get(b); return c ? { m: c.m.slice(), inv: c.inv } : { m: [], inv: 0 }; }
function sameFilter(a, b) { return a.inv === b.inv && a.m.length === b.m.length && a.m.every((m, k) => m === b.m[k]); }
// loose material -> the solid it becomes in a wall block
@@ -52,11 +70,11 @@ const PIECES = [
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall',
desc: 'A solid block. Diagonal drags make a sloped face; T flips it. For one block, T cycles square and four slope corners. R picks the material: a square costs 16 grains or 2 bars; a slope costs 8 grains or 1 bar.' },
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
- desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it.' },
+ desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it. Drag diagonally to make a sloped belt that carries up or down.' },
{ id: 'tube', code: BP_TUBE, name: 'Screw tube', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left', 'up-right', 'down-right', 'down-left', 'up-left'],
desc: 'A turning screw that draws in loose grains and whole bars and pushes them along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Drag a line to set its direction, including diagonals. R turns a single tube. Join either end to tubes or belts. Copper softens around 1000°C.' },
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
- desc: 'A stretch of belt with a mesh in it. Put it in a line of belts and it joins them as one belt. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through.' },
+ desc: 'A stretch of belt with a mesh in it. Put it in a line of belts and it joins them as one belt. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through. Drag diagonally to make a sloped filter that carries up or down.' },
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,
desc: 'Carries power between machines. Wooden, so keep it away from fire.' },
{ id: 'grate', code: BP_GRATE, name: 'Grate', kind: 'block', cost: [[LCLAY, 8]],
@@ -97,6 +115,8 @@ const PIECES = [
const PIECE_BY_ID = {}, PIECE_BY_CODE = [];
PIECES.forEach((p, i) => { p.index = i; PIECE_BY_ID[p.id] = p; if (p.code) PIECE_BY_CODE[p.code] = p; if (p.kind === 'machine') { p.w = p.bw * BS; p.h = p.bh * BS; } });
for (let c = BP_SLOPE_BL; c <= BP_SLOPE_TR; c++) PIECE_BY_CODE[c] = PIECE_BY_ID.wall;
+for (const c of [BP_BELT_UP, BP_BELT_DOWN]) PIECE_BY_CODE[c] = PIECE_BY_ID.belt;
+for (const c of [BP_FILTER_UP, BP_FILTER_DOWN]) PIECE_BY_CODE[c] = PIECE_BY_ID.filter;
// power per grain of belt / tube (a block is 16 grains)
const BELT_POWER = 0.03, TUBE_POWER = 0.125, FAN_POWER = 6;
const WHEEL_K = 0.32, ENGINE_K = 40;
diff --git a/src/sim/save.js b/src/sim/save.js
index b1567d7..b0427a8 100644
--- a/src/sim/save.js
+++ b/src/sim/save.js
@@ -55,8 +55,8 @@ function deserializeWorld(meta, buffers) {
}
// pipes used to be solid copper blocks; now they are thin and let things past
for (let b = 0; b < NB; b++) if (blockP[b] === BP_PIPE) for (const i of blockCellList(b % BW, (b / BW) | 0)) if (mat[i] === PIPE) { mat[i] = EMPTY; part[i] = P_PIPE; }
- for (const [b, m, inv] of meta.filters || []) if (blockP[b] === BP_FILTER) filterCfg.set(b, filterRec({ m, inv }));
- for (let b = 0; b < NB; b++) if (blockP[b] === BP_FILTER) {
+ for (const [b, m, inv] of meta.filters || []) if (isFilter(blockP[b])) filterCfg.set(b, filterRec({ m, inv }));
+ for (let b = 0; b < NB; b++) if (isFilter(blockP[b])) {
// the first filters kept one material in their block variant
if (!filterCfg.has(b)) filterCfg.set(b, filterRec({ m: [blockV[b] & 127], inv: 0 }));
blockV[b] &= 128;
diff --git a/src/sim/undo.js b/src/sim/undo.js
index cdc12df..b62f818 100644
--- a/src/sim/undo.js
+++ b/src/sim/undo.js
@@ -22,8 +22,8 @@ function undoLast() {
for (let k = rec.ops.length - 1; k >= 0; k--) {
const o = rec.ops[k];
if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); }
- else if (o.t === 'block-') { const r = placeBlock(o.code, o.code === BP_FILTER ? o.v & 128 : o.v, o.bx, o.by, false, o.extra); if (r) fail(r); }
- else if (o.t === 'filter') { const b = o.by * BW + o.bx; if (blockP[b] === BP_FILTER) { blockV[b] = o.v & 128; setFilter(b, o.extra); filtersDirty = true; } }
+ else if (o.t === 'block-') { const r = placeBlock(o.code, isFilter(o.code) ? o.v & 128 : o.v, o.bx, o.by, false, o.extra); if (r) fail(r); }
+ else if (o.t === 'filter') { const b = o.by * BW + o.bx; if (isFilter(blockP[b])) { blockV[b] = o.v & 128; setFilter(b, o.extra); filtersDirty = true; } }
else if (o.t === 'mach+') { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm && siloFull(mm)) fail('empty the silo first'); else if (mm) removeMachine(mm.id, true, false); }
else if (o.t === 'mach-') { const r = placeMachineAt(o.type, o.dir, o.bx, o.by); if (r) fail(r); else { const mm = machines.find(m => m && m.type === o.type && m.x === o.bx * BS && m.y === o.by * BS); if (mm) mm.on = o.on !== false; } }
else if (o.t === 'mold+') { const mo = molds.find(m => m.kind === o.kind && m.x === o.bx * BS && m.y === o.by * BS); if (mo) removeMold(mo); }
@@ -56,8 +56,8 @@ function ghostTick0() {
if (nx.done) { ghostIter = ghostBlocks.entries(); nx = ghostIter.next(); if (nx.done) break; }
const [b, [code, v, e]] = nx.value;
const bx = b % BW, by = (b / BW) | 0;
- if (blockP[b] === code && blockV[b] === (code === BP_FILTER ? v & 128 : v)) {
- if (code === BP_FILTER && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
+ if (blockP[b] === code && blockV[b] === (isFilter(code) ? v & 128 : v)) {
+ if (isFilter(code) && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
}
if (!blockInReach(bx, by)) continue;
diff --git a/src/ui/actions.js b/src/ui/actions.js
index 762478c..4f3535e 100644
--- a/src/ui/actions.js
+++ b/src/ui/actions.js
@@ -27,16 +27,16 @@ function doInteract() {
if (fb >= 0) {
if (!inReach(ax, ay)) { setStatus('Out of reach.'); playSfx('error'); return; }
const run = filterRun(fb);
- openFilterPicker(Object.assign(filterInfo(fb), { dir: blockV[fb] & 128 ? 1 : 0 }), f => {
+ openFilterPicker(Object.assign(filterInfo(fb), { dir: blockV[fb] & 128 ? 1 : 0, code: blockP[fb] }), f => {
applyFilterRun(run, f);
GAME.filterCfg = { m: f.m.slice(), inv: f.inv }; GAME.pieceDir.filter = f.dir;
- setStatus(`${run.length} ${run.length === 1 ? 'filter' : 'filters'} updated, carrying ${f.dir ? 'right' : 'left'}.`, 'info');
+ setStatus(`${run.length} ${run.length === 1 ? 'filter' : 'filters'} updated, carrying ${beltDirection(blockP[fb], f.dir ? 128 : 0)}.`, 'info');
}, run.length > 1 ? `Filter (${run.length} in this row)` : 'Filter');
GAME.editRun = run;
return;
}
if (toolId() === 'build' && pieceUsesWall(piece())) { openWallPicker(); return; }
- if (toolId() === 'build' && piece().filter) { openFilterPicker(Object.assign({}, GAME.filterCfg, { dir: GAME.pieceDir.filter || 0 }), f => { GAME.filterCfg = { m: f.m, inv: f.inv }; GAME.pieceDir.filter = f.dir; }, 'New filters'); return; }
+ if (toolId() === 'build' && piece().filter) { openFilterPicker(Object.assign({}, GAME.filterCfg, { dir: GAME.pieceDir.filter || 0, code: pieceCode(piece()) }), f => { GAME.filterCfg = { m: f.m, inv: f.inv }; GAME.pieceDir.filter = f.dir; }, 'New filters'); return; }
let msg = '';
outer: for (let r = 0; r <= 2; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
if (!inReach(ax + dx, ay + dy)) continue;
@@ -59,6 +59,7 @@ function pickUnderCursor() {
else if (blockP[bi] || ghost) {
const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1];
const P = PIECE_BY_CODE[code]; pid = P.id;
+ if (isBelt(code) || isFilter(code)) { GAME.pieceShape = GAME.pieceShape || {}; GAME.pieceShape[P.id] = code; }
if (isWall(code)) { GAME.wallMat = v; GAME.pieceDir.wall = isSlope(code) ? code - BP_SLOPE_BL + 1 : 0; }
else if (P.filter) { GAME.filterCfg = blockP[bi] ? filterInfo(bi) : cleanFilterCfg(ghost[2]); GAME.pieceDir[P.id] = v >> 7; }
else if (P.dirs) GAME.pieceDir[P.id] = v % P.dirs.length;
@@ -99,6 +100,7 @@ function dragDirFor(P, a, b) {
if (!a || !P.dirs || !(P.id === 'belt' || P.id === 'tube' || P.filter)) return -1;
const dx = b.bx - a.bx, dy = b.by - a.by;
if (!dx && !dy) return -1;
+ if ((P.id === 'belt' || P.filter) && diagonalDrag(dx, dy)) return dx > 0 ? 1 : 0;
if (P.id === 'tube' && diagonalDrag(dx, dy)) return dx > 0 ? (dy < 0 ? 4 : 5) : (dy > 0 ? 6 : 7);
if (P.id === 'tube') return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0);
return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 0) : -1;
@@ -163,7 +165,7 @@ function primaryRelease(shift) {
}
if (t === 'build' && GAME.drag) {
const P = piece(), b = aimBlock(), code = pieceCode(P, b);
- const list = shift ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by, isWall(code) || code === BP_TUBE);
+ const list = shift ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by, isWall(code) || isSlopeBelt(code) || code === BP_TUBE);
const dd = dragDirFor(P, GAME.drag, b);
if (dd >= 0) GAME.pieceDir[P.id] = dd;
const v = pieceV(P), ex = pieceExtra(P);
diff --git a/src/ui/build-menu.js b/src/ui/build-menu.js
index e836be5..380104e 100644
--- a/src/ui/build-menu.js
+++ b/src/ui/build-menu.js
@@ -12,8 +12,8 @@ const PIECE_ICONS = {
wall: '
Find something loose first.
') + ``; dialog(title || 'Filter', body, [{ label: 'Clear', cls: 'ghost', fn: () => { sel.clear(); sync(); return true; } }, { label: 'Cancel' }, { label: 'Set filter', cls: 'primary', fn: () => { onDone({ m: [...sel].sort((a, b) => a - b), inv, dir }); hudDirty = true; } }]); diff --git a/src/ui/selection.js b/src/ui/selection.js index 17d8eb8..b8703ba 100644 --- a/src/ui/selection.js +++ b/src/ui/selection.js @@ -87,7 +87,7 @@ function pieceV(P) { return GAME.pieceDir[P.id] || 0; } function pieceExtra(P) { return P.filter ? { m: GAME.filterCfg.m.slice(), inv: GAME.filterCfg.inv } : undefined; } -function filterBlockAt(x, y) { if (x < 0 || y < 0 || x >= W || y >= H) return -1; const b = ((y / BS) | 0) * BW + ((x / BS) | 0); return blockP[b] === BP_FILTER ? b : -1; } +function filterBlockAt(x, y) { if (x < 0 || y < 0 || x >= W || y >= H) return -1; const b = ((y / BS) | 0) * BW + ((x / BS) | 0); return isFilter(blockP[b]) ? b : -1; } function chipCost(cost) { const ok = canAfford(cost, 1); return `${esc(costText(cost, 1))}`; } function pieceCostNow(P, m = GAME.wallMat) { if (P.kind === 'mold') return wallCost(m, MOLDS[P.mold].walls); @@ -96,6 +96,13 @@ function pieceCostNow(P, m = GAME.wallMat) { } function pieceCode(P, end = aimBlock()) { + if (P.id === 'belt' || P.filter) { + const a = GAME.drag || end; + if (GAME.drag && diagonalDrag(end.bx - a.bx, end.by - a.by) && !(typeof modShift === 'function' && modShift())) + return (P.filter ? BP_FILTER_UP : BP_BELT_UP) + ((end.bx - a.bx) * (end.by - a.by) > 0 ? 1 : 0); + if (end.bx === a.bx && end.by === a.by) return GAME.pieceShape && GAME.pieceShape[P.id] || P.code; + return P.code; + } if (P.code !== BP_WALL) return P.code; const a = GAME.drag || end, dx = end.bx - a.bx, dy = end.by - a.by; if (GAME.drag && (dx || dy)) { diff --git a/test/t_slope_belts.js b/test/t_slope_belts.js new file mode 100644 index 0000000..3e12340 --- /dev/null +++ b/test/t_slope_belts.js @@ -0,0 +1,173 @@ +// Sloped belts: transport, joins, sorting, persistence and the actual drag controls. +const { load, suite, flatWorld, rect, ticks, count } = require('./harness'); +const fs = require('fs'); +const { run } = load(), { check, done } = suite(); +function fresh() { + flatWorld(run, 256, 224, 208); + run('RS=7;nextPowerBuild=0;money=10000;AMB.fill(20);temp.fill(20);hot.fill(0);'); +} +fresh(); +check('slope belts use new codes, without renumbering old slopes', run("typeof BP_BELT_UP !== 'undefined' && BP_BELT_UP > BP_SLOPE_TR")); +function line(grade, filter = false, dir = 1) { + fresh(); + const by = grade < 0 ? 32 : 16, inlet = by + (grade < 0 ? 1 : 0), outlet = by + 9 * grade + (grade > 0 ? 1 : 0); + run(`for(let k=0;k<10;k++) placeBlock(${filter ? 'BP_FILTER_UP' : 'BP_BELT_UP'}+${grade>0?1:0},${filter?dir*128:dir},16+k,${by}+k*${grade},true,{m:[SAND],inv:0}); + for(let x=10;x<16;x++)placeBlock(BP_BELT,${dir},x,${inlet},true); + for(let x=26;x<32;x++)placeBlock(BP_BELT,${dir},x,${outlet},true);`); + return { by, inlet, outlet, grade }; +} +function collect(s, steps, feed = false, sink = 128) { + return run(`(()=>{let out=0,added=0;for(let k=0;k<${steps};k++){ + ${feed ? `for(let l=1;l<=4;l++){const i=(${s.inlet}*4-l)*W+44;if(mat[i]===EMPTY){setCell(i,SAND,20,0);added++;}}` : ''} + simTick(); + for(let i=0;i