Sloped belts and filters, laid by a 45° drag (t40)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
+23
-17
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"client": [
|
||||
"render/setup.js",
|
||||
"render/light.js",
|
||||
"render/slope-belts.js",
|
||||
"render/world.js",
|
||||
"render/machines.js",
|
||||
"render/overlays.js",
|
||||
|
||||
+1
-1
@@ -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]; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
+14
-2
@@ -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);
|
||||
}
|
||||
|
||||
+32
-21
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+68
-14
@@ -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);
|
||||
|
||||
+38
-6
@@ -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) {
|
||||
|
||||
+4
-18
@@ -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++) {
|
||||
|
||||
+1
-1
@@ -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];
|
||||
|
||||
+23
-3
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+4
-4
@@ -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;
|
||||
|
||||
+6
-4
@@ -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);
|
||||
|
||||
@@ -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: '<rect x="2" y="9" width="20" height="8" rx="2"/><circle cx="7" cy="13" r="2"/><circle cx="17" cy="13" r="2"/><path d="M9 5h7M14 3l2 2-2 2"/>',
|
||||
filter: '<rect x="2" y="10" width="20" height="8" rx="2"/><path d="M7 10v8M12 10v8M17 10v8M2 14h20"/><path d="M12 2v5M10 5l2 2 2-2"/>',
|
||||
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"/>',
|
||||
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"/>',
|
||||
@@ -52,7 +52,9 @@ function buildVariantChips(P) {
|
||||
// while dragging a line of belts, filters or tubes, the direction is the drag's
|
||||
const dragging = GAME.drag && GAME.dragDir >= 0 && P.dirs;
|
||||
const glyph = d => ({ left: '←', right: '→', up: '↑', down: '↓', 'up-right': '↗', 'down-right': '↘', 'down-left': '↙', 'up-left': '↖' })[d] || '';
|
||||
const dirChip = !P.dirs ? '' : dragging ? `<span class="chip ok">${glyph(P.dirs[GAME.dragDir])} ${esc(P.dirs[GAME.dragDir])} · from your drag</span>` : `<span class="chip hot">${glyph(dir)} ${esc(dir)} · R</span>`;
|
||||
const code = pieceCode(P), direction = dragging ? GAME.dragDir : GAME.pieceDir[P.id] || 0;
|
||||
const slopeDir = isSlopeBelt(code) ? beltDirection(code, P.filter ? direction << 7 : direction) : '';
|
||||
const dirChip = !P.dirs ? '' : dragging ? `<span class="chip ok">${glyph(slopeDir || P.dirs[GAME.dragDir])} ${esc(slopeDir || P.dirs[GAME.dragDir])} · from your drag</span>` : `<span class="chip hot">${glyph(slopeDir || dir)} ${esc(slopeDir || dir)} · R</span>`;
|
||||
const fc = GAME.filterCfg;
|
||||
const sws = ms => ms.slice(0, 6).map(m => `<span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:2px;background:${rgbOf(m)}"></span>`).join('') + (ms.length > 6 ? `+${ms.length - 6}` : '');
|
||||
return pieceUsesWall(P) ? `<button class="chip hot" data-wall-picker><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R · F</button>${P.code === BP_WALL ? `<span class="chip hot">${['square', '◣ lower left', '◢ lower right', '◤ upper left', '◥ upper right'][isSlope(pieceCode(P)) ? pieceCode(P) - BP_SLOPE_BL + 1 : 0]} · T · turn / flip</span>` : ''}`
|
||||
|
||||
+2
-2
@@ -109,7 +109,7 @@ function hoverInfo(ax, ay) {
|
||||
t1 = press ? 'Brick press' : saw ? 'Saw' : 'Grinder';
|
||||
t2.push(s && s.blocked ? saw || press ? 'stopped: no room underneath; a belt there carries them away' : 'stopped: clear the space under it' : s && tick - s.busyT < 30 ? press ? 'pressing' : saw ? 'cutting' : 'grinding' : press ? 'idle: drop clay lumps on top' : saw ? 'idle: drop stone rubble or wood chunks on top' : 'idle: drop rubble, gravel, sand or dirt on top', press ? `${stats.pressed || 0} green bricks pressed` : saw ? `${(stats.bricks || 0).toLocaleString('en-US')} stone blocks, ${(stats.planks || 0).toLocaleString('en-US')} planks cut` : `${(stats.refined || 0).toLocaleString('en-US')} ground, ${(stats.refinedOre || 0).toLocaleString('en-US')} ore found`);
|
||||
} else if (p === P_FILTER) {
|
||||
const fb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pr = filterParts(filterInfo(fb)), ar = blockV[fb] & 128 ? '→' : '←';
|
||||
const owner = slopeSealOwner(i), fb = owner >= 0 ? owner : ((ay / BS) | 0) * BW + ((ax / BS) | 0), pr = filterParts(filterInfo(fb)), ar = beltDirection(blockP[fb], blockV[fb]);
|
||||
t1 = 'Filter ' + ar; t2.push(`drops ${pr.drops}`, `carries ${pr.rides} ${ar}`, 'F to change · I shows all');
|
||||
} else if (p === P_PIPE) {
|
||||
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb);
|
||||
@@ -117,7 +117,7 @@ function hoverInfo(ax, ay) {
|
||||
t2.push(pipeContentsText(pn));
|
||||
} else if (IS_TUBE[p]) t1 = 'Screw tube ' + PIECE_BY_ID.tube.dirs[TUBE_PARTS.indexOf(p)] + (m !== EMPTY ? ` · ${MD[m].name}` : '');
|
||||
else if (p) t1 = ({ 5: 'Grate', 6: 'Gate (open)', 7: 'Water wheel', 8: 'Engine intake' })[p] + (m !== EMPTY ? ` · ${MD[m].name}` : '');
|
||||
else if (m === BELTL || m === BELTR) t1 = 'Belt ' + (m === BELTL ? '←' : '→');
|
||||
else if (m === BELTL || m === BELTR) { const owner = slopeSealOwner(i), fb = owner >= 0 ? owner : (ay >> 2) * BW + (ax >> 2); t1 = 'Belt ' + beltDirection(blockP[fb], blockV[fb]); }
|
||||
else if (m === PIPE && isPipeCode(b)) {
|
||||
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb), v = blockV[pb], arrows = ['↑', '→', '↓', '←'];
|
||||
t1 = b === BP_PUMP ? `Pump ${arrows[v & 3]}` : b === BP_OUTLET ? `Outlet ${['↓', '←', '↑', '→'][v & 3]}` : 'Pipe';
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ function controlsHtml() {
|
||||
const tools = [
|
||||
['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'],
|
||||
['2 Pneumatic nozzle', "Left drag picks up the selection, or what you click if nothing is selected. Right click blows the selection. Click a bag name or Q/E selects one; Ctrl+click mixes. Hold Ctrl to see everything found. Clear removes the selection. Bars blow alone. Blowing out the last of a material deselects it. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."],
|
||||
['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws, Brick presses and Bar racks, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. A diagonal wall drag makes a sealed sloped face, solid below; T flips it. For one wall block, T cycles square and four slope corners. Right click takes a piece back. Your bot flies through anything you build.'],
|
||||
['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws, Brick presses and Bar racks, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. A diagonal wall drag makes a sealed sloped face, solid below; T flips it. For one wall block, T cycles square and four slope corners. Diagonal belt and filter drags make sloped runs that carry up or down, following the drag. Their faces always point up. Right click takes a piece back. Your bot flies through anything you build.'],
|
||||
['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'],
|
||||
['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'],
|
||||
];
|
||||
|
||||
+14
-3
@@ -15,7 +15,8 @@ function drawGrid(cbx, cby, radius) {
|
||||
}
|
||||
function previewColor(code, v) {
|
||||
if (isWall(code)) { const m = WALL_OF[v]; return `${COLR[m]},${COLG[m]},${COLB[m]}`; }
|
||||
if (code === BP_FILTER) return '96,100,110';
|
||||
if (isFilter(code)) return '96,100,110';
|
||||
if (isBelt(code)) return '110,114,124';
|
||||
return { [BP_BELT]: '110,114,124', [BP_TUBE]: '120,132,150', [BP_SHAFT]: '160,118,70', [BP_GRATE]: '170,90,66', [BP_GATE]: '160,84,64', [BP_PIPE]: '176,112,72', [BP_PUMP]: '104,122,142', [BP_OUTLET]: '170,106,66' }[code] || '120,200,190';
|
||||
}
|
||||
// Air swirls into the pickup ring or spreads from the nozzle toward the cursor.
|
||||
@@ -107,7 +108,7 @@ function drawPreviews() {
|
||||
if (P.kind === 'block') {
|
||||
const v = pieceV(P);
|
||||
col = previewColor(code, v);
|
||||
cells = GAME.drag ? (modShift() ? 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)) : [[b.bx, b.by]];
|
||||
cells = GAME.drag ? (modShift() ? 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)) : [[b.bx, b.by]];
|
||||
const dd = GAME.drag ? dragDirFor(P, GAME.drag, b) : -1;
|
||||
if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; }
|
||||
const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0);
|
||||
@@ -126,7 +127,7 @@ function drawPreviews() {
|
||||
previewBlock(code, x, y, w, h, true, P.id === 'tube' ? dIdx : -1, bx, by, planned); ctx.setLineDash([]);
|
||||
if (P.dirs && (P.id === 'belt' || P.id === 'tube' || P.filter || P.id === 'pump' || P.id === 'outlet')) {
|
||||
// a chevron on every block, pointing the way it will carry (or pump, or pour)
|
||||
const [dx, dy] = P.id === 'tube' ? TUBE_DIRS[dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx];
|
||||
const [dx, dy] = isSlopeBelt(code) ? [dIdx ? 1 : -1, beltGrade(code) * (dIdx ? 1 : -1)] : P.id === 'tube' ? TUBE_DIRS[dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx];
|
||||
const cx = x + w / 2, cy = y + h / 2, k = w * 0.22, px_ = -dy, py_ = dx;
|
||||
ctx.strokeStyle = 'rgba(20,16,12,0.55)'; ctx.lineWidth = Math.max(2, zp * 0.9); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||
const chev = () => { ctx.beginPath(); ctx.moveTo(cx - dx * k * 0.6 + px_ * k, cy - dy * k * 0.6 + py_ * k); ctx.lineTo(cx + dx * k * 0.7, cy + dy * k * 0.7); ctx.lineTo(cx - dx * k * 0.6 - px_ * k, cy - dy * k * 0.6 - py_ * k); ctx.stroke(); };
|
||||
@@ -206,6 +207,16 @@ 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)) {
|
||||
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);
|
||||
}
|
||||
ctx.closePath(); if (stroke) ctx.stroke(); 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; }
|
||||
const right = code === BP_SLOPE_BR || code === BP_SLOPE_TR, above = code >= BP_SLOPE_TL;
|
||||
ctx.beginPath();
|
||||
|
||||
@@ -13,7 +13,9 @@ function filterParts(f) {
|
||||
function filterSentence(f) { const p = filterParts(f); return `drop ${p.drops} and carry ${p.rides}`; }
|
||||
// Change the whole run of matching filters at once
|
||||
function applyFilterRun(run, f) {
|
||||
for (const b of run) { blockV[b] = f.dir ? 128 : 0; setFilter(b, f); }
|
||||
undoRecord('filter settings', () => {
|
||||
for (const b of run) { undoNote({ t: 'filter', bx: b % BW, by: (b / BW) | 0, v: blockV[b], extra: filterInfo(b) }); blockV[b] = f.dir ? 128 : 0; setFilter(b, f); }
|
||||
});
|
||||
filtersDirty = true;
|
||||
}
|
||||
function openFilterPicker(cur, onDone, title) {
|
||||
@@ -24,7 +26,7 @@ function openFilterPicker(cur, onDone, title) {
|
||||
<button class="fm" role="radio" data-inv="0"><b>↓ Drop these</b><span>They fall through; the rest rides along</span></button>
|
||||
<button class="fm" role="radio" data-inv="1"><b>→ Carry these</b><span>They ride along; the rest falls through</span></button>
|
||||
</div>
|
||||
<div class="fdir" role="radiogroup" aria-label="Rollers turn"><span class="label">Rollers turn</span><button class="fd" role="radio" data-dir="0">← Left</button><button class="fd" role="radio" data-dir="1">Right →</button></div>`
|
||||
<div class="fdir" role="radiogroup" aria-label="Rollers turn"><span class="label">Rollers turn</span><button class="fd" role="radio" data-dir="0">${cur.code ? esc(beltDirection(cur.code, 0)) : '← Left'}</button><button class="fd" role="radio" data-dir="1">${cur.code ? esc(beltDirection(cur.code, 128)) : 'Right →'}</button></div>`
|
||||
+ (list.length ? `<div class="fpick">${list.map(m => `<button class="fp" role="checkbox" data-m="${m}"><span class="ck" aria-hidden="true"></span><span class="sw" style="background:${rgbOf(m)}"></span><span>${esc(MD[m].name)}</span></button>`).join('')}</div>` : '<p class="note">Find something loose first.</p>')
|
||||
+ `<p class="note fp-sum" id="fp-sum"></p>`;
|
||||
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; } }]);
|
||||
|
||||
+8
-1
@@ -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 `<span class="chip ${ok ? 'ok' : 'bad'}">${esc(costText(cost, 1))}</span>`; }
|
||||
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)) {
|
||||
|
||||
@@ -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<N;i++)if(mat[i]===SAND && i%W>=${sink}){out++;setCell(i,EMPTY,20,0);}
|
||||
for(let j=flyers.length-1;j>=0;j--)if(flyers[j].m===SAND && flyers[j].x>=${sink}){out++;flyers.splice(j,1);}
|
||||
}return {out,added,left:mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length};})()`);
|
||||
}
|
||||
const rates=[];
|
||||
for(const grade of [0,-1,1]) {
|
||||
let s;
|
||||
if(grade) s=line(grade);
|
||||
else {fresh();run('for(let bx=10;bx<32;bx++)placeBlock(BP_BELT,1,bx,24,true);');s={inlet:24};}
|
||||
collect(s,220,true);
|
||||
const r=collect(s,600,true);rates.push(r.out/10);
|
||||
check(`grade ${grade} moves a four-grain load at flat belt speed`,r.out>0 && (grade===0 || Math.abs(r.out/10-rates[0])<=rates[0]*0.05),JSON.stringify(r));
|
||||
const end=collect(s,600);
|
||||
check(`grade ${grade} leaves no load stuck`,end.left===0,JSON.stringify(end));
|
||||
}
|
||||
console.log('Throughput grains/s flat/up/down: '+rates.join('/'));
|
||||
for(const grade of [-1,1]) {
|
||||
const s=line(grade);
|
||||
rect(run,40,s.inlet*4-8,55,s.inlet*4-1,run('SAND'));
|
||||
rect(run,39,20,39,s.inlet*4-1,run('STONE'));
|
||||
const r=collect(s,1000);
|
||||
check(`grade ${grade} conserves and delivers every grain across both flat joins`,r.out===128 && r.left===0,JSON.stringify(r));
|
||||
line(grade);
|
||||
run(`spawnBar(BARCU,44,${s.inlet*4-2},0,0);`);
|
||||
ticks(run,500);
|
||||
check(`grade ${grade} carries a bar through both flat joins`,run('ents.length===1 && ents[0].ix>=128'),run('ents.map(e=>[e.ix,e.iy])'));
|
||||
}
|
||||
for(const grade of [-1,1]) {
|
||||
const s=line(grade);
|
||||
rect(run,68,s.by*4-12,71,s.by*4-5,run('WATER'));
|
||||
const before=count(run,run('WATER'));
|
||||
ticks(run,600);
|
||||
check(`water drains through grade ${grade} and is conserved`,count(run,run('WATER'))===before && count(run,run('WATER'),0,200,255,207)===before);
|
||||
// A completely full face held at its head must not unload backwards or through corners.
|
||||
line(grade);
|
||||
run(`for(let x=64;x<104;x++){const by=${s.by}+((x>>2)-16)*${grade},c=blockP[by*BW+(x>>2)],y=beltFaceY(c,x>>2,by,x);for(let l=1;l<=4;l++)setCell((y-l)*W+x,SAND,20,0);}`);
|
||||
rect(run,104,0,104,s.outlet*4-1,run('STONE'));
|
||||
ticks(run,300);
|
||||
const held=run('mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length');
|
||||
check(`blocked grade ${grade} holds every grain without slipping below its face`,held===160 && count(run,run('SAND'),0,0,63,207)===0 && count(run,run('SAND'),0,160,255,207)===0,held);
|
||||
rect(run,104,0,104,s.outlet*4-1,run('EMPTY'));
|
||||
const released=collect(s,800);
|
||||
check(`grade ${grade} resumes after the head clears`,released.out===160 && released.left===0,JSON.stringify(released));
|
||||
line(grade);
|
||||
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});setCell((${s.by+9*grade}*4+${grade<0?0:3}-1)*W+103,SAND,20,0);tick=2;tickStamp++;machinesTick();`);
|
||||
check(`grade ${grade} launches off an open end along its face`,run(`flyers.length===1 && flyers[0].vx>0 && Math.sign(flyers[0].vy)===${grade}`));
|
||||
run(`spawnBar(BARCU,44,${s.inlet*4-2},0,0);`);
|
||||
const barLaunch=run('(()=>{for(let k=0;k<400;k++){simTick();if(ents[0].ix>=104)return ents[0].vx>0;}return false;})()');
|
||||
check(`grade ${grade} keeps a bar moving forward off an open end`,barLaunch);
|
||||
for(const diagonal of [false,true]) {
|
||||
line(grade);
|
||||
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});for(let k=0;k<3;k++)placeBlock(BP_TUBE,${diagonal?(grade<0?4:5):1},26+k,${s.by+9*grade+(grade<0?-1:0)}+k*${diagonal?grade:0},true);`);
|
||||
rect(run,40,s.inlet*4-8,55,s.inlet*4-1,run('SAND'));
|
||||
rect(run,39,20,39,s.inlet*4-1,run('STONE'));
|
||||
const r=collect(s,1000,false,116);
|
||||
check(`grade ${grade} unloads grains into a ${diagonal?'diagonal':'straight'} tube`,r.out===128 && r.left===0,JSON.stringify(r));
|
||||
run(`spawnBar(BARCU,44,${s.inlet*4-2},0,0);`);
|
||||
ticks(run,600);
|
||||
check(`grade ${grade} unloads a bar into a ${diagonal?'diagonal':'straight'} tube`,run('ents.length===1 && ents[0].ix>=116 && !mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)'),run('ents.map(e=>[e.ix,e.iy])'));
|
||||
|
||||
}
|
||||
}
|
||||
for(const grade of [-1,1]) {
|
||||
const s=line(grade,true);
|
||||
run('for(const b of filterCfg.keys())setFilter(b,{m:[SAND],inv:1});');
|
||||
collect(s,220,true);
|
||||
const r=collect(s,600,true);
|
||||
console.log(`Filter throughput grade ${grade}, grains/s: ${r.out/10}`);
|
||||
check(`carrying filter grade ${grade} matches the belt feed`,r.out/10>=rates[0]*0.95,JSON.stringify(r));
|
||||
}
|
||||
for(const grade of [-1,1]) for(const inv of [0,1]) {
|
||||
const s=line(grade,true);
|
||||
run(`for(const b of filterCfg.keys())setFilter(b,{m:[SAND],inv:${inv}});`);
|
||||
rect(run,39,20,39,s.inlet*4-1,run('STONE'));
|
||||
run(`for(let y=${s.inlet*4-8};y<${s.inlet*4};y++)for(let x=40;x<56;x++)setCell(y*W+x,(x+y)&1?SAND:GRAVEL,20,0);`);
|
||||
const result=run(`(()=>{const out=[0,0],below=[0,0];for(let k=0;k<1200;k++){
|
||||
simTick();
|
||||
for(let i=0;i<N;i++)if(mat[i]===SAND || mat[i]===GRAVEL){const m=mat[i]===SAND?0:1,x=i%W,y=(i/W)|0;
|
||||
if(x>=128){out[m]++;setCell(i,EMPTY,20,0);}else if(y>=180){below[m]++;setCell(i,EMPTY,20,0);}}
|
||||
for(let f=flyers.length-1;f>=0;f--){const p=flyers[f];if((p.m===SAND || p.m===GRAVEL) && p.x>=128){out[p.m===SAND?0:1]++;flyers.splice(f,1);}}
|
||||
}return {out,below,left:mat.reduce((n,m)=>n+(m===SAND || m===GRAVEL),0)+flyers.filter(f=>f.m===SAND || f.m===GRAVEL).length};})()`);
|
||||
console.log(`Sorting grade ${grade} inverse ${inv}: `+JSON.stringify(result));
|
||||
check(`grade ${grade} ${inv?'carries selected':'drops selected'} and conserves the full mix`,result.out[inv?0:1]===64 && result.below[inv?1:0]===64 && result.left===0 && result.out[inv?1:0]===0 && result.below[inv?0:1]===0,JSON.stringify(result));
|
||||
}
|
||||
for(const c of ['BP_BELT_UP','BP_BELT_DOWN','BP_FILTER_UP','BP_FILTER_DOWN'])for(const d of [0,1]) {
|
||||
fresh();
|
||||
run(`placeBlock(${c},${c.includes('FILTER')?d*128:d},20,20,true,{m:[SAND,BARCU],inv:1});globalThis.saved=serializeWorld();deserializeWorld(saved.meta,saved.buffers);`);
|
||||
check(`${c} ${d} saves its shape, direction and settings`,run(`blockP[20*BW+20]===${c} && blockV[20*BW+20]===${c.includes('FILTER')?d*128:d} && blockIntegrity(20*BW+20)===10 ${c.includes('FILTER')?'&& sameFilter(filterInfo(20*BW+20),{m:[SAND,BARCU].sort((a,b)=>a-b),inv:1})':''}`));
|
||||
run(`globalThis.clip=copyRegion(80,80,83,83);bot.x=96;bot.y=80;undoRecord('paste',()=>pasteGhost(clip,24,20));ghostTick();`);
|
||||
check(`${c} ${d} copy/paste keeps all settings`,run('blockP[20*BW+24]===blockP[20*BW+20] && blockV[20*BW+24]===blockV[20*BW+20] && sameFilter(filterInfo(20*BW+20),filterInfo(20*BW+24))'));
|
||||
run('undoLast();');
|
||||
check(`${c} ${d} undo clears the paste including its seal`,run(`blockP[20*BW+24]===0 && blockCellList(24,20).every(i=>mat[i]===EMPTY && !part[i]) && mat[slopeSeal(${c},24,20)]===EMPTY && !part[slopeSeal(${c},24,20)]`));
|
||||
run("undoRecord('cut',()=>cutRegion(80,80,83,83));undoLast();");
|
||||
check(`${c} ${d} undo restores a cut`,run(`blockP[20*BW+20]===${c} && blockIntegrity(20*BW+20)===10`));
|
||||
check(`${c} ${d} mirrors its shape and reverses horizontally`,run(`(()=>{const b=flipBlueprint(clip).blocks[0];return b[2]===${c}+${c.endsWith('UP')?1:-1} && b[3]===${c.includes('FILTER')?(1-d)*128:1-d};})()`));
|
||||
}
|
||||
for(const grade of [-1,1]) {
|
||||
const s=line(grade,true);
|
||||
run(`spawnBar(BARCU,44,${s.inlet*4-2},0,0);`);ticks(run,500);
|
||||
check(`sloped filter grade ${grade} carries a bar it does not pass`,run('ents.length===1 && ents[0].ix>=128'));
|
||||
line(grade,true);
|
||||
run(`for(const b of filterCfg.keys())setFilter(b,{m:[BARCU],inv:0});spawnBar(BARCU,44,${s.inlet*4-2},0,0);`);ticks(run,500);
|
||||
check(`sloped filter grade ${grade} drops a selected bar through its open frame`,run('ents.length===1 && ents[0].ix<104 && ents[0].iy===206 && !mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)'));
|
||||
}
|
||||
// Reverse the motor without flipping the upward-facing triangle.
|
||||
for(const grade of [-1,1]) {
|
||||
const s=line(grade,false,0);
|
||||
rect(run,112,s.outlet*4-8,127,s.outlet*4-1,run('SAND'));
|
||||
rect(run,128,20,128,s.outlet*4-1,run('STONE'));
|
||||
const r=run(`(()=>{let out=0;for(let k=0;k<1000;k++){simTick();for(let i=0;i<N;i++)if(mat[i]===SAND && i%W<40){out++;setCell(i,EMPTY,20,0);}for(let f=flyers.length-1;f>=0;f--)if(flyers[f].m===SAND && flyers[f].x<40){out++;flyers.splice(f,1);}}return {out,left:mat.reduce((n,m)=>n+(m===SAND),0)};})()`);
|
||||
check(`reversed grade ${grade} delivers every grain to the left`,r.out===128 && r.left===0,JSON.stringify(r));
|
||||
line(grade,false,0);run(`spawnBar(BARCU,120,${s.outlet*4-2},0,0);`);ticks(run,500);
|
||||
check(`reversed grade ${grade} carries bars left across both joins`,run('ents.length===1 && ents[0].ix<40'),run('ents.map(e=>[e.ix,e.iy])'));
|
||||
}
|
||||
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);})()'));
|
||||
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=()=>{};");
|
||||
for(const id of ['belt','filter'])for(const [dx,dy] of [[5,-4],[4,5],[-5,4],[-4,-5]]) {
|
||||
run('GAME.drag={bx:20,by:20};');
|
||||
const c=(id==='belt'?19:21)+(dx*dy>0?1:0);
|
||||
check(`${id} drag ${dx},${dy} picks an upward face and runs along the drag`,run(`pieceCode(PIECE_BY_ID.${id},{bx:${20+dx},by:${20+dy}})===${c} && dragDirFor(PIECE_BY_ID.${id},GAME.drag,{bx:${20+dx},by:${20+dy}})===${dx>0?1:0}`));
|
||||
}
|
||||
run("globalThis.setStatus=()=>{};globalThis.catOf=()=>0;globalThis.selectTool=()=>{};globalThis.aimBlock=()=>blockOf(GAME.aim.x,GAME.aim.y);");
|
||||
for(const id of ['belt','filter'])for(const [dx,dy] of [[3,-3],[3,3],[-3,3],[-3,-3]]) {
|
||||
fresh();
|
||||
run(`globalThis.piece=()=>PIECE_BY_ID.${id};GAME.drag={bx:20,by:20};GAME.aim={x:${(20+dx)*4},y:${(20+dy)*4}};bot.x=80;bot.y=80;primaryRelease(false);`);
|
||||
const c=(id==='belt'?19:21)+(dx*dy>0?1:0), v=dx>0?(id==='filter'?128:1):0;
|
||||
check(`${id} release really builds the diagonal in direction ${dx},${dy}`,run(`Array.from({length:4},(_,k)=>{const b=(20+k*${Math.sign(dy)})*BW+20+k*${Math.sign(dx)};return blockP[b]===${c} && blockV[b]===${v};}).every(Boolean)`));
|
||||
run('pickUnderCursor();rotate();');
|
||||
check(`${id} pick and R reverse the motor while preserving a single slope shape`,run(`pieceCode(PIECE_BY_ID.${id})===${c} && pieceV(PIECE_BY_ID.${id})===${id==='filter'?128-v:1-v}`));
|
||||
run('GAME.drag=aimBlock();GAME.replace=true;primaryRelease(false);GAME.replace=false;');
|
||||
check(`${id} a single replacement reverses the picked slope`,run(`blockP[${20+dy}*BW+${20+dx}]===${c} && blockV[${20+dy}*BW+${20+dx}]===${id==='filter'?128-v:1-v}`));
|
||||
|
||||
}
|
||||
run(fs.readFileSync('src/ui/selection-pickers.js','utf8'));
|
||||
const filterLine=line(-1,true);
|
||||
run("applyFilterRun(filterRun(32*BW+16),{m:[GRAVEL],inv:1,dir:0});");
|
||||
check('the settings UI updates the matching diagonal filter run',run('filterCfg.size===10 && [...filterCfg.keys()].every(b=>sameFilter(filterInfo(b),{m:[GRAVEL],inv:1}) && blockV[b]===0)'));
|
||||
run('undoLast();');
|
||||
check('undo restores diagonal filter settings and direction',run('filterCfg.size===10 && [...filterCfg.keys()].every(b=>sameFilter(filterInfo(b),{m:[SAND],inv:0}) && blockV[b]===128)'));
|
||||
// An actual v2 fixture, before either diagonal belt or filter existed.
|
||||
const legacy=JSON.parse(require('zlib').gunzipSync(fs.readFileSync(__dirname+'/fixtures/slopes-v2.json.gz')));
|
||||
globalThis.legacyBeltSave=legacy;
|
||||
run('deserializeWorld(legacyBeltSave.meta,legacyBeltSave.buffers)');
|
||||
check('old v2 saves keep all wall slope ids and materials',run('Array.from({length:4},(_,k)=>{const b=6*BW+2+k*3;return blockP[b]===15+k && blockV[b]===RSTONE && blockIntegrity(b)===10;}).every(Boolean)'));
|
||||
done();
|
||||
@@ -0,0 +1,31 @@
|
||||
// Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below.
|
||||
const left = Math.max(8, ((SX - 176) >> 2) * 4), base = ((SY - 20) >> 2), bx0 = left >> 2;
|
||||
const ground = (base + 22) * 4;
|
||||
for (let y = (base - 16) * 4; y < ground + 8; y++) for (let x = left; x < left + 188; x++) {
|
||||
const i = y * W + x;
|
||||
mat[i] = y >= ground ? STONE : EMPTY; part[i] = 0; mach[i] = 0; bgm[i] = 0; flags[i] = y >= ground ? F_NAT : 0; temp[i] = 20; wakeXY(x, y);
|
||||
}
|
||||
for (let x = left; x < left + 188; x++) { surfY[x] = ground; dirtY[x] = ground + 8; }
|
||||
AMB.fill(20); money = 50000; weather.next = 1e15;
|
||||
for (let k = 4; k < 10; k++) placeBlock(BP_BELT, 1, bx0 + k, base + 1, true);
|
||||
for (let k = 0; k < 10; k++) placeBlock(BP_BELT_UP, 1, bx0 + 10 + k, base - k, true);
|
||||
for (let k = 20; k < 26; k++) placeBlock(BP_BELT, 1, bx0 + k, base - 9, true);
|
||||
for (let k = 0; k < 10; k++) placeBlock(BP_BELT_DOWN, 1, bx0 + 26 + k, base - 9 + k, true);
|
||||
for (let k = 36; k < 43; k++) placeBlock(BP_BELT, 1, bx0 + k, base + 1, true);
|
||||
const low = base + 16;
|
||||
for (let k = 4; k < 10; k++) placeBlock(BP_BELT, 1, bx0 + k, low + 1, true);
|
||||
for (let k = 0; k < 10; k++) placeBlock(BP_FILTER_UP, 128, bx0 + 10 + k, low - k, true, {m:[SAND],inv:0});
|
||||
for (let k = 20; k < 30; k++) placeBlock(BP_BELT, 1, bx0 + k, low - 9, true);
|
||||
for (let k = 0; k < 380; k++) {
|
||||
for (let l = 1; l <= 4; l++) {
|
||||
let i = ((base + 1) * 4 - l) * W + left + 20;
|
||||
if (mat[i] === EMPTY) setCell(i, SAND, 20, 0);
|
||||
i = ((low + 1) * 4 - l) * W + left + 20;
|
||||
if (mat[i] === EMPTY) setCell(i, (k + l) & 1 ? SAND : GRAVEL, 20, 0);
|
||||
}
|
||||
simTick();
|
||||
}
|
||||
spawnBar(BARCU, left + 92, (base - 9) * 4 - BAR_H, 0, 0, 20);
|
||||
window.CAM = [left + 92, (base + 6) * 4];
|
||||
window.SLOPE_CAM = [left + 64, (base - 4) * 4];
|
||||
updateRoof();
|
||||
Reference in New Issue
Block a user