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:
2026-10-25 23:17:55 -04:00
co-authored by Claude Opus 5.5
parent 24eb9559f8
commit e19cb784d2
26 changed files with 531 additions and 110 deletions
+6
View File
@@ -388,3 +388,9 @@ above, including:
- **Upgrade ideas for the drill**: an amplifier, a tuning fork, harmonics. Also a Grav pick - **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`. 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. - 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
View File
@@ -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 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 - 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. 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 - 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, 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 - 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) ### src/sim/economy.js (152 lines)
Money, prices, market saturation, the trade dock, buyer and builder orders. 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 - 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 - 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). Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick).
- functions: engineIntake, fanBlow, 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. 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 - 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. 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) ### src/sim/molds.js (317 lines)
Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal. 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 - functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
- variables: DROSS, grains, barWord - 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. 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 - variables: BAR_W, BAR_H, entCurM, droppedLast
### src/sim/tools.js (52 lines) ### 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. Propagated coloured light from glowing and burning cells.
- functions: builtForLight, looseForLight, computeLight - 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. Scale colour without changing the smooth-look class.
- functions: fogColor, diagonalTubeJoins, diagonalTubeShape, diagonalTubeBlock, drawWorld - functions: fogColor, diagonalTubeJoins, diagonalTubeShape, diagonalTubeBlock, drawWorld
- variables: TUBE_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap - variables: TUBE_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
@@ -374,7 +378,7 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace - functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
- variables: SM, smBytes - variables: SM, smBytes
### src/render/smooth-shaders.js (366 lines) ### src/render/smooth-shaders.js (393 lines)
Smooth shaders: vertex and fragment programs for the rounded world look. Smooth shaders: vertex and fragment programs for the rounded world look.
- variables: SM_VS, SM_FS - variables: SM_VS, SM_FS
@@ -430,16 +434,16 @@ Notebook blueprints: starter designs, saved library and code sharing.
The tool hotbar and what is selected. The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, piece - 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. The build menu along the bottom: categories, piece tiles and their icons.
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu - 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. 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 - 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 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 - functions: filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker
@@ -457,7 +461,7 @@ Keyboard and mouse listeners.
- functions: isTyping - functions: isTyping
- variables: canvas - 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 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 - functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
- variables: ovShown - variables: ovShown
@@ -467,7 +471,7 @@ Gamepad input.
- functions: padPressed, pollPad - functions: padPressed, pollPad
- variables: PAD - 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. Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock - functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
@@ -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_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation. - `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
- `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly. - `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly.
- `test/t_slope_belts.js`: Sloped belts: transport, joins, sorting, persistence and the actual drag controls.
- `test/t_slopes.js`: Sloped walls: flow, sealed joints, building, refunds and persistence. - `test/t_slopes.js`: Sloped walls: flow, sealed joints, building, refunds and persistence.
- `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works. - `test/t_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders. - `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-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/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/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.
+10
View File
@@ -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 note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-25: t40: 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) ## 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 - Diagonal tubes are as thick as straight ones, and an end block joining a straight tube, belt or
+1
View File
@@ -41,6 +41,7 @@
"client": [ "client": [
"render/setup.js", "render/setup.js",
"render/light.js", "render/light.js",
"render/slope-belts.js",
"render/world.js", "render/world.js",
"render/machines.js", "render/machines.js",
"render/overlays.js", "render/overlays.js",
+1 -1
View File
@@ -117,7 +117,7 @@ function thinPipeColor(x, y, anim) {
PC[0] = r; PC[1] = g; PC[2] = bl; PC[0] = r; PC[1] = g; PC[2] = bl;
} }
// a block that is a belt or a filter: they join up into one belt // 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 // 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); 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]; } for (let m = 0; m < NMAT; m++) { TREE_R[m] = COLR[m]; TREE_G[m] = COLG[m]; TREE_B[m] = COLB[m]; }
+28
View File
@@ -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);
}
+29 -2
View File
@@ -51,8 +51,8 @@ float beltSD(vec2 lb, bool endL, bool endR) {
// the line only rounds off where the whole run ends. // the line only rounds off where the whole run ends.
float beltLineSD(ivec2 c, vec2 lb) { float beltLineSD(ivec2 c, vec2 lb) {
int nr = cls(cellAt(tCol, c + ivec2(4, 0))), nl = cls(cellAt(tCol, c - ivec2(4, 0))); int 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), 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 <= 22)); isBelt(nr) + isF(nr) < 0.5 && !(nr >= 19 && nr <= 30) && cls(cellAt(tCol, c + ivec2(4, -4))) < 23);
} }
// The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and // The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and
// back along the bottom. // back along the bottom.
@@ -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 // ---- light, then the glow of hot things on top
if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb; if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb;
if (uGlowK > 0.0) { if (uGlowK > 0.0) {
+14 -2
View File
@@ -281,6 +281,11 @@ function drawWorld() {
if (pipeBodyCell(x, y)) { thinPipeColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2]; } if (pipeBodyCell(x, y)) { thinPipeColor(x, y, anim); r = PC[0]; g = PC[1]; b = PC[2]; }
} }
} }
const sb = slopeBeltAt(i, x, y), slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
if (slopeArt) {
const color = slopeBeltColor(sb, x, y, anim, br, bgc, bb);
r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255;
}
// incandescence // incandescence
if (T > 450 && m !== EMPTY && incand) { 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]; 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, // 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) // 4 built piece (drawn crisp), 5 smoke and flame, 6 built wall block (drawn as a tile)
let cl = 0; 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]; 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 // filter: 13 carries left, 14 right; the tree buffer carries its colour tag (first material) and mode
const fb = (y >> 2) * BW + (x >> 2), fc = filterCfg.get(fb), fm = fc && fc.m.length ? fc.m[0] : 0; const fb = (y >> 2) * BW + (x >> 2), fc = filterCfg.get(fb), fm = fc && fc.m.length ? fc.m[0] : 0;
cl = blockV[fb] & 128 ? 14 : 13; cl = blockV[fb] & 128 ? 14 : 13;
@@ -319,6 +324,13 @@ function drawWorld() {
const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]]; const wm = WALL_OF[blockV[(y >> 2) * BW + (x >> 2)]];
r = COLR[wm]; g = COLG[wm]; b = COLB[wm]; r = COLR[wm]; g = COLG[wm]; b = COLB[wm];
} }
if (slopeArt) {
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; top = cl << 24;
fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 && !builtForLight(i, x, y) ? 0xff : 0); fpx[row + bx] = (tb ? 0xff000000 : 0) | (cl === 5 ? 0xff0000 : 0) | (cl === 3 ? 0xff00 : 0) | (cl === 1 && !builtForLight(i, x, y) ? 0xff : 0);
} }
+32 -21
View File
@@ -3,33 +3,42 @@
function blockOf(x, y) { return { bx: Math.floor(x / BS), by: Math.floor(y / BS) }; } 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 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 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. // One extra cell on the solid side seals diagonal crossings between successive triangles.
function slopeSeal(code, bx, by) { function slopeSeal(code, bx, by) {
code = slopeShape(code);
const x = bx * BS + (code === BP_SLOPE_BR || code === BP_SLOPE_TR ? 0 : 3); const x = bx * BS + (code === BP_SLOPE_BR || code === BP_SLOPE_TR ? 0 : 3);
const y = by * BS + (code >= BP_SLOPE_TL ? -1 : 4); const y = by * BS + (code >= BP_SLOPE_TL ? -1 : 4);
return y < 0 || y >= H ? -1 : y * W + x; return y < 0 || y >= H ? -1 : y * W + x;
} }
function slopeSealOwner(i) { function slopeSealOwner(i) {
const x = i % W, y = (i / W) | 0, bx = x >> 2; 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; if (by < 0 || by >= BH) continue;
const b = by * BW + bx, c = blockP[b]; 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; return -1;
} }
function wallCells(code, bx, by) { 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) { function canPlaceBlock(bx, by, code = BP_WALL) {
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return 'Out of bounds'; if (bx < 0 || by < 0 || bx >= BW || by >= BH) return 'Out of bounds';
if (blockP[by * BW + bx]) return 'Something is already built there'; if (blockP[by * BW + bx]) return 'Something is already built there';
for (const i of blockCellList(bx, by)) { 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 (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 (occ[i]) return 'A bar is in the way';
} }
if (isSlope(code)) { if (isSlope(code) || isSlopeBelt(code)) {
const i = slopeSeal(code, bx, by); 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.'; 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'; if (isWall(code) && !WALL_OF[v]) return 'Pick a wall material with R';
const cost = pieceCost(code, v); const cost = pieceCost(code, v);
if (!free) { if (!canAfford(cost, 1)) return needText(cost, 1); payCost(cost, 1); } 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 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) { for (const i of cells) {
const y = (i / W) | 0, x = i % W; const y = (i / W) | 0, x = i % W;
if (isWall(code)) setCell(i, WALL_OF[v], AMB[y], 0); 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_SHAFT) { setCell(i, SHAFT, AMB[y], 0); markPow(i, true); }
else if (code === BP_RACK) setCell(i, RACKB, AMB[y], 0); 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 (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_GATE) setCell(i, GATE, AMB[y], 0);
else if (code === BP_GRATE) { part[i] = P_GRATE; wakeXY(x, y); } 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_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 (code === BP_PIPE) { part[i] = P_PIPE; wakeXY(x, y); }
else if (isPipeCode(code)) setCell(i, PIPE, AMB[y], 0); else if (isPipeCode(code)) setCell(i, PIPE, AMB[y], 0);
} }
if (isPipeCode(code)) pipesDirty = true; if (isPipeCode(code)) pipesDirty = true;
blockP[by * BW + bx] = code; blockV[by * BW + bx] = v; 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 (isProcCode(code)) procDirty = true;
if (code === BP_RACK) { if (code === BP_RACK) {
const b = by * BW + bx, s = newRack(); const b = by * BW + bx, s = newRack();
@@ -67,9 +76,9 @@ function placeBlock(code, v, bx, by, free, extra) {
rackState.set(b, s); rackState.set(b, s);
for (let q = rackTop(b); q <= rackBottom(b); q += BW) rackState.get(q).open = s.open; 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); 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++; stats.placed++;
undoNote({ t: 'block+', bx, by, code }); undoNote({ t: 'block+', bx, by, code });
@@ -84,14 +93,14 @@ function blockIntegrity(b) {
for (const i of blockCellList(bx, by)) { for (const i of blockCellList(bx, by)) {
const m = mat[i], p = part[i]; 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++; } 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_SHAFT) { if (m === SHAFT) n++; }
else if (code === BP_RACK) { if (m === RACKB) 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 (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_GATE) { if (m === GATE || p === P_GOPEN) n++; }
else if (code === BP_GRATE) { if (p === P_GRATE) 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_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++; } else if (isPipeCode(code)) { if (m === PIPE || p === P_PIPE) n++; }
} }
void v; void v;
@@ -109,7 +118,7 @@ function removeWallCell(i, wall) {
function removeBlock(bx, by) { function removeBlock(bx, by) {
const b = by * BW + bx, code = blockP[b], v = blockV[b]; const b = by * BW + bx, code = blockP[b], v = blockV[b];
if (!code) return 0; 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 // a pipe taken out spills its share of what the line holds
const spill = isPipeCode(code) ? pipeTakeShare(b) : null; const spill = isPipeCode(code) ? pipeTakeShare(b) : null;
let n = 0, metal = 0; let n = 0, metal = 0;
@@ -125,7 +134,7 @@ function removeBlock(bx, by) {
} else if (isPipeCode(code)) { } else if (isPipeCode(code)) {
if (p === P_PIPE) { part[i] = 0; wakeIdx(i); n++; } if (p === P_PIPE) { part[i] = 0; wakeIdx(i); n++; }
else if (m === PIPE) { setCell(i, EMPTY, temp[i], 0); 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 (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); } if (p === P_GOPEN) { part[i] = 0; n++; wakeIdx(i); }
} else if (p) { part[i] = 0; markPow(i, false); wakeIdx(i); n++; } } 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); 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); 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 (isProcCode(code)) { procDirty = true; procState.delete(b); }
if (code === BP_RACK) { if (code === BP_RACK) {
const s = rackState.get(b); const s = rackState.get(b);
@@ -146,17 +155,19 @@ function removeBlock(bx, by) {
} }
blockP[b] = 0; blockV[b] = 0; blockP[b] = 0; blockV[b] = 0;
const cleared = blockCellList(bx, by); const cleared = blockCellList(bx, by);
if (isSlope(code)) { if (isSlope(code) || isSlopeBelt(code)) {
const i = slopeSeal(code, bx, by); const i = slopeSeal(code, bx, by);
if (i >= 0) { if (i >= 0) {
const nb = (((i / W) | 0) >> 2) * BW + ((i % W) >> 2), nc = blockP[nb]; 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); cleared.push(i);
} }
} }
for (const i of cleared) { for (const i of cleared) {
const owner = slopeSealOwner(i); 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)) { if (isPipeCode(code)) {
pipesDirty = true; 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)? // Is block b already exactly this piece (same kind, variant and, for a filter, settings)?
function blockIs(b, code, v, extra) { function blockIs(b, code, v, extra) {
if (blockP[b] !== code) return false; 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; return blockV[b] === v;
} }
// replace: a block already built in the way is taken down (and refunded) to make room // 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; const b = by * BW + bx;
if (replace && blockP[b]) { if (replace && blockP[b]) {
if (blockIs(b, code, v, extra)) continue; 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; } if (!canAfford(pieceCost(code, v), 1)) { last = needText(pieceCost(code, v), 1); break; }
removeBlock(bx, by); removeBlock(bx, by);
} }
+3 -3
View File
@@ -7,7 +7,7 @@ function copyRegion(x0, y0, x1, y1) {
const blocks = [], ms = [], ols = []; const blocks = [], ms = [], ols = [];
for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) { for (let by = by0; by <= by1; by++) for (let bx = bx0; bx <= bx1; bx++) {
const b = by * BW + 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) { for (const mm of machines) {
if (!mm || PIECE_BY_ID[mm.type].fixed) continue; if (!mm || PIECE_BY_ID[mm.type].fixed) continue;
@@ -23,11 +23,11 @@ function copyRegion(x0, y0, x1, y1) {
} }
function flipBlueprint(bp) { function flipBlueprint(bp) {
const bw = bp.bw; 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; : code === BP_PUMP || code === BP_OUTLET ? (((v & 3) === 1 ? 3 : (v & 3) === 3 ? 1 : v & 3) | (v & 4)) : v;
return { return {
v: 2, bw, bh: bp.bh, name: bp.name, 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]) => { machines: bp.machines.map(([t, x, y, d]) => {
const P = PIECE_BY_ID[t]; const P = PIECE_BY_ID[t];
let nd = d; let nd = d;
+68 -14
View File
@@ -4,7 +4,24 @@
// clear). If a powder grain is blocked, the load above it stands still and the belt slips // 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. // underneath. A filter's own material drops through it instead, unless it is already full.
const CARRY_DEPTH = 96; 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) { 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; let blocked = false;
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) { for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
if (a < 0) return; if (a < 0) return;
@@ -20,20 +37,30 @@ function carryColumn(a, d, pass) {
if (blocked) continue; // filters still sift above a stopped load if (blocked) continue; // filters still sift above a stopped load
const x = a % W, nx = x + d; const x = a % W, nx = x + d;
if (nx < 0 || nx >= W) return; 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 // 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); 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; if (!pass) return;
blocked = true; continue; blocked = true; continue;
} }
const y = (a / W) | 0, bj = j + W; const y = (a / W) | 0, bj = j + W;
// carried off the end with nothing under it: it leaves at belt speed and falls // 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); 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); else move(a, j, x, y, nx, y + step);
stats.moved++; 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. // Belts are perforated: liquid resting on one drips through to whatever open space is under it.
function beltDrain(b) { function beltDrain(b) {
const a = b - W, x = b % W; const a = b - W, x = b % W;
@@ -53,23 +80,48 @@ function launchGrain(i, x, y, vx, vy) {
let filterR = [], filterL = [], filtersDirty = true; let filterR = [], filterL = [], filtersDirty = true;
function rebuildFilters() { function rebuildFilters() {
filtersDirty = false; filterR = []; filterL = []; 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 // 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)); filterR.sort((a, c) => (c % BW) - (a % BW)); filterL.sort((a, c) => (a % BW) - (c % BW));
} }
function filterCarry(b, d) { function filterCarry(b, d, carrying = true) {
if (blockP[b] !== BP_FILTER) return; if (!isFilter(blockP[b])) return;
const bx = b % BW, y = ((b / BW) | 0) * BS; const bx = b % BW, by = (b / BW) | 0, code = blockP[b], y = by * BS;
if (y < 1) return; if (y < 1) return;
if (!carrying && !isSlopeBelt(code)) return;
const c = filterCfg.get(b), pass = c ? c.pass : null; const c = filterCfg.get(b), pass = c ? c.pass : null;
for (let k = 0; k < BS; k++) { for (let k = 0; k < BS; k++) {
const x = d > 0 ? bx * BS + BS - 1 - k : bx * BS + k, top = y * W + x; 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) carryColumn(top - W, d, pass); 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. // Change what a filter does; grains inside it that no longer pass are pushed out on top.
function setFilter(b, cfg) { function setFilter(b, cfg) {
if (blockP[b] !== BP_FILTER) return false; if (!isFilter(blockP[b])) return false;
const rec = filterRec(cfg); const rec = filterRec(cfg);
filterCfg.set(b, rec); filterCfg.set(b, rec);
const bx = b % BW, by = (b / BW) | 0; 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 // The run of filters in a row that touch block b and do the same thing it does
function filterRun(b) { 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]; 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) { for (const s of [-1, 1]) for (let bx = (b % BW) + s; bx >= 0 && bx < BW; bx += s) {
const c = by * BW + bx; const yy = by + beltGrade(blockP[b]) * (bx - (b % BW));
if (blockP[c] !== BP_FILTER || (blockV[c] & 128) !== dir || !sameFilter(filterInfo(c), f)) break; 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); out.push(c);
} }
return out.sort((a, c) => a - c); return out.sort((a, c) => a - c);
+38 -6
View File
@@ -59,9 +59,10 @@ function entTubeFree(x, y, tubeOnly = false) {
function entTubeMove(e, x, y, tubeOnly = false, mouth = false) { function entTubeMove(e, x, y, tubeOnly = false, mouth = false) {
if (!entTubeFree(x, y, tubeOnly)) return false; if (!entTubeFree(x, y, tubeOnly)) return false;
if (mouth) { if (mouth) {
// Lift clear of the receiving belt before sliding over it; check both legs before moving. // Go around the receiving corner, checking 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; const acrossY = y > e.iy ? e.iy : y, downX = y > e.iy ? x : e.ix;
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, y)) return false; 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++) } 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; 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; 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); 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; 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++) { for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
const i = entTubeEdge(e, dx, dy, k); const i = entTubeEdge(e, dx, dy, k);
if (i < 0) continue; 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; 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. // Vertical screws require the full width; horizontal screws use the nearest two-row lane.
const x = (xx & ~3), y = entTubeLane(yy, e.iy); 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; 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; 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++; } 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() { function entTick() {
for (let k = ents.length - 1; k >= 0; k--) { for (let k = ents.length - 1; k >= 0; k--) {
const e = ents[k]; const e = ents[k];
@@ -137,7 +169,7 @@ function entTick() {
let ix = e.ix; const iy = e.iy; let ix = e.ix; const iy = e.iy;
const tube = entTubeAt(ix, iy); const tube = entTubeAt(ix, iy);
if (tube) entTubeTick(e, tube); if (tube) entTubeTick(e, tube);
else { else if (!entSlopeBeltTick(e)) {
// what is underneath? // what is underneath?
let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H; let belt = 0, bsat = 0, transport = false, support = iy + BAR_H >= H;
if (!support) { if (!support) {
+4 -18
View File
@@ -87,24 +87,10 @@ function machinesTick() {
mm.hot = Math.round(hotT); mm.hot = Math.round(hotT);
} }
} }
// belts and filters move what rests on them every other tick: half a grain per tick // Half a grain per tick; sloped faces grip their load on the intervening ticks too.
if ((tick & 1) === 0) { if (filtersDirty) rebuildFilters();
for (let pass = 0; pass < 2; pass++) { carryBeltsAndFilters(beltL, filterL, -1, BELTL, (tick & 1) === 0);
const list = pass ? beltR : beltL, d = pass ? 1 : -1, bm = pass ? BELTR : BELTL; carryBeltsAndFilters(beltR, filterR, 1, BELTR, (tick & 1) === 0);
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);
}
// tubes: every loose grain inside steps one cell along, front of the line first, on the same // 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 // 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++) { if ((tick & 1) === 0) for (let p = 1; p < IS_TUBE.length; p++) {
+1 -1
View File
@@ -44,7 +44,7 @@ function rebuildPower() {
if (!net) continue; if (!net) continue;
if (mm.type === 'fan') net.fans.push(mm); else if (mm.type === 'wheel' || mm.type === 'engine') net.prods.push(mm); 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) { for (const p of TUBE_PARTS) {
tubeLists[p].sort((a, b) => { tubeLists[p].sort((a, b) => {
if (TDX[p] && TDY[p]) return ((b % W) - (a % W)) * TDX[p]; if (TDX[p] && TDY[p]) return ((b % W) - (a % W)) * TDX[p];
+23 -3
View File
@@ -6,9 +6,22 @@
const BS = 4; 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_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; 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 isSlope(c) { return c >= BP_SLOPE_BL && c <= BP_SLOPE_TR; }
function isWall(c) { return c === BP_WALL || isSlope(c); } function isWall(c) { return c === BP_WALL || isSlope(c); }
function slopeCell(c, x, y) { function slopeCell(c, x, y) {
c = slopeShape(c);
if (c === BP_SLOPE_BR || c === BP_SLOPE_TR) x = 3 - x; if (c === BP_SLOPE_BR || c === BP_SLOPE_TR) x = 3 - x;
if (c === BP_SLOPE_TL || c === BP_SLOPE_TR) y = 3 - y; if (c === BP_SLOPE_TL || c === BP_SLOPE_TR) y = 3 - y;
return y >= x; 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; 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 }; 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 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]); } 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 // 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', { 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.' }, 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'], { 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'], { 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.' }, 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, { 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, { 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.' }, desc: 'Carries power between machines. Wooden, so keep it away from fire.' },
{ id: 'grate', code: BP_GRATE, name: 'Grate', kind: 'block', cost: [[LCLAY, 8]], { 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 = []; 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; } }); 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 (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) // power per grain of belt / tube (a block is 16 grains)
const BELT_POWER = 0.03, TUBE_POWER = 0.125, FAN_POWER = 6; const BELT_POWER = 0.03, TUBE_POWER = 0.125, FAN_POWER = 6;
const WHEEL_K = 0.32, ENGINE_K = 40; const WHEEL_K = 0.32, ENGINE_K = 40;
+2 -2
View File
@@ -55,8 +55,8 @@ function deserializeWorld(meta, buffers) {
} }
// pipes used to be solid copper blocks; now they are thin and let things past // 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 (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 (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 (blockP[b] === BP_FILTER) { for (let b = 0; b < NB; b++) if (isFilter(blockP[b])) {
// the first filters kept one material in their block variant // the first filters kept one material in their block variant
if (!filterCfg.has(b)) filterCfg.set(b, filterRec({ m: [blockV[b] & 127], inv: 0 })); if (!filterCfg.has(b)) filterCfg.set(b, filterRec({ m: [blockV[b] & 127], inv: 0 }));
blockV[b] &= 128; blockV[b] &= 128;
+4 -4
View File
@@ -22,8 +22,8 @@ function undoLast() {
for (let k = rec.ops.length - 1; k >= 0; k--) { for (let k = rec.ops.length - 1; k >= 0; k--) {
const o = rec.ops[k]; const o = rec.ops[k];
if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); } 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 === '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 (blockP[b] === BP_FILTER) { blockV[b] = o.v & 128; setFilter(b, o.extra); filtersDirty = true; } } 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 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 === '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); } 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; } if (nx.done) { ghostIter = ghostBlocks.entries(); nx = ghostIter.next(); if (nx.done) break; }
const [b, [code, v, e]] = nx.value; const [b, [code, v, e]] = nx.value;
const bx = b % BW, by = (b / BW) | 0; const bx = b % BW, by = (b / BW) | 0;
if (blockP[b] === code && blockV[b] === (code === BP_FILTER ? v & 128 : v)) { if (blockP[b] === code && blockV[b] === (isFilter(code) ? v & 128 : v)) {
if (code === BP_FILTER && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings if (isFilter(code) && e && !sameFilter(filterInfo(b), cleanFilterCfg(e))) setFilter(b, e); // same piece, new settings
ghostBlocks.delete(b); ghostBlocked.delete(b); continue; ghostBlocks.delete(b); ghostBlocked.delete(b); continue;
} }
if (!blockInReach(bx, by)) continue; if (!blockInReach(bx, by)) continue;
+6 -4
View File
@@ -27,16 +27,16 @@ function doInteract() {
if (fb >= 0) { if (fb >= 0) {
if (!inReach(ax, ay)) { setStatus('Out of reach.'); playSfx('error'); return; } if (!inReach(ax, ay)) { setStatus('Out of reach.'); playSfx('error'); return; }
const run = filterRun(fb); 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); applyFilterRun(run, f);
GAME.filterCfg = { m: f.m.slice(), inv: f.inv }; GAME.pieceDir.filter = f.dir; 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'); }, run.length > 1 ? `Filter (${run.length} in this row)` : 'Filter');
GAME.editRun = run; GAME.editRun = run;
return; return;
} }
if (toolId() === 'build' && pieceUsesWall(piece())) { openWallPicker(); 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 = ''; let msg = '';
outer: for (let r = 0; r <= 2; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) { 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; if (!inReach(ax + dx, ay + dy)) continue;
@@ -59,6 +59,7 @@ function pickUnderCursor() {
else if (blockP[bi] || ghost) { else if (blockP[bi] || ghost) {
const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1]; const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1];
const P = PIECE_BY_CODE[code]; pid = P.id; 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; } 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.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; 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; 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; const dx = b.bx - a.bx, dy = b.by - a.by;
if (!dx && !dy) return -1; 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' && 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); 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; return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 0) : -1;
@@ -163,7 +165,7 @@ function primaryRelease(shift) {
} }
if (t === 'build' && GAME.drag) { if (t === 'build' && GAME.drag) {
const P = piece(), b = aimBlock(), code = pieceCode(P, b); 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); const dd = dragDirFor(P, GAME.drag, b);
if (dd >= 0) GAME.pieceDir[P.id] = dd; if (dd >= 0) GAME.pieceDir[P.id] = dd;
const v = pieceV(P), ex = pieceExtra(P); const v = pieceV(P), ex = pieceExtra(P);
+5 -3
View File
@@ -12,8 +12,8 @@ const PIECE_ICONS = {
wall: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 12h18M9 5v7M15 12v7"/>', wall: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 12h18M9 5v7M15 12v7"/>',
grate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M7.5 5v14M12 5v14M16.5 5v14"/>', grate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M7.5 5v14M12 5v14M16.5 5v14"/>',
gate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M9 5v14"/><path d="M12 12h6M16 9.5l2.5 2.5-2.5 2.5"/>', gate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M9 5v14"/><path d="M12 12h6M16 9.5l2.5 2.5-2.5 2.5"/>',
belt: '<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"/>', 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: '<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"/>', 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"/>', tube: '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M6.5 18l3.5-12M11.5 18l3.5-12M16.5 18l3-10"/>',
pipe: '<path d="M2 10h20M2 14h20"/><path d="M6 7.5v9M18 7.5v9"/>', pipe: '<path d="M2 10h20M2 14h20"/><path d="M6 7.5v9M18 7.5v9"/>',
pump: '<circle cx="11" cy="14" r="6"/><path d="M11 14l3.5-2.5M11 14l-1 4.2M11 14l-4-1.2"/><path d="M15.5 9.5L20 5M16 5h4v4"/>', pump: '<circle cx="11" cy="14" r="6"/><path d="M11 14l3.5-2.5M11 14l-1 4.2M11 14l-4-1.2"/><path d="M15.5 9.5L20 5M16 5h4v4"/>',
@@ -52,7 +52,9 @@ function buildVariantChips(P) {
// while dragging a line of belts, filters or tubes, the direction is the drag's // 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 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 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 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}` : ''); 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>` : ''}` 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
View File
@@ -109,7 +109,7 @@ function hoverInfo(ax, ay) {
t1 = press ? 'Brick press' : saw ? 'Saw' : 'Grinder'; 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`); 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) { } 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'); t1 = 'Filter ' + ar; t2.push(`drops ${pr.drops}`, `carries ${pr.rides} ${ar}`, 'F to change · I shows all');
} else if (p === P_PIPE) { } else if (p === P_PIPE) {
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb); const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb);
@@ -117,7 +117,7 @@ function hoverInfo(ax, ay) {
t2.push(pipeContentsText(pn)); 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 (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 (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)) { else if (m === PIPE && isPipeCode(b)) {
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb), v = blockV[pb], arrows = ['↑', '→', '↓', '←']; 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'; t1 = b === BP_PUMP ? `Pump ${arrows[v & 3]}` : b === BP_OUTLET ? `Outlet ${['↓', '←', '↑', '→'][v & 3]}` : 'Pipe';
+1 -1
View File
@@ -71,7 +71,7 @@ function controlsHtml() {
const tools = [ 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.'], ['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."], ['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.'], ['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.'], ['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
View File
@@ -15,7 +15,8 @@ function drawGrid(cbx, cby, radius) {
} }
function previewColor(code, v) { function previewColor(code, v) {
if (isWall(code)) { const m = WALL_OF[v]; return `${COLR[m]},${COLG[m]},${COLB[m]}`; } 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'; 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. // Air swirls into the pickup ring or spreads from the nozzle toward the cursor.
@@ -107,7 +108,7 @@ function drawPreviews() {
if (P.kind === 'block') { if (P.kind === 'block') {
const v = pieceV(P); const v = pieceV(P);
col = previewColor(code, v); 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; const dd = GAME.drag ? dragDirFor(P, GAME.drag, b) : -1;
if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; } if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; }
const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0); 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([]); 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')) { 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) // 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; 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'; 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(); }; 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(); if (stroke) ctx.stroke(); else ctx.fill();
ctx.restore(); return; 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; } 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; const right = code === BP_SLOPE_BR || code === BP_SLOPE_TR, above = code >= BP_SLOPE_TL;
ctx.beginPath(); ctx.beginPath();
+4 -2
View File
@@ -13,7 +13,9 @@ function filterParts(f) {
function filterSentence(f) { const p = filterParts(f); return `drop ${p.drops} and carry ${p.rides}`; } function filterSentence(f) { const p = filterParts(f); return `drop ${p.drops} and carry ${p.rides}`; }
// Change the whole run of matching filters at once // Change the whole run of matching filters at once
function applyFilterRun(run, f) { 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; filtersDirty = true;
} }
function openFilterPicker(cur, onDone, title) { 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="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> <button class="fm" role="radio" data-inv="1"><b>→ Carry these</b><span>They ride along; the rest falls through</span></button>
</div> </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>') + (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>`; + `<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; } }]); 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
View File
@@ -87,7 +87,7 @@ function pieceV(P) {
return GAME.pieceDir[P.id] || 0; return GAME.pieceDir[P.id] || 0;
} }
function pieceExtra(P) { return P.filter ? { m: GAME.filterCfg.m.slice(), inv: GAME.filterCfg.inv } : undefined; } 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 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) { function pieceCostNow(P, m = GAME.wallMat) {
if (P.kind === 'mold') return wallCost(m, MOLDS[P.mold].walls); 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()) { 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; if (P.code !== BP_WALL) return P.code;
const a = GAME.drag || end, dx = end.bx - a.bx, dy = end.by - a.by; const a = GAME.drag || end, dx = end.bx - a.bx, dy = end.by - a.by;
if (GAME.drag && (dx || dy)) { if (GAME.drag && (dx || dy)) {
+173
View File
@@ -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();
+31
View File
@@ -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();