Belts carry 4 grains deep; stalled belts warn and stop animating (t45)
This commit is contained in:
+5
-2
@@ -142,7 +142,7 @@ above, including:
|
|||||||
short stubs stay for the next firing. The mold cleans itself and rakes the grate after dropping bars or clearing junk.
|
short stubs stay for the next firing. The mold cleans itself and rakes the grate after dropping bars or clearing junk.
|
||||||
- **Screw conveyors carry no more than belts:** they move every other tick. For more throughput,
|
- **Screw conveyors carry no more than belts:** they move every other tick. For more throughput,
|
||||||
build more lines.
|
build more lines.
|
||||||
- **A heap on a belt moves as one**, up to 96 grains deep, not just its bottom layer. If a grain is blocked,
|
- **A belt carries a bed four grains deep** (one block, t45), with higher grains left loose. If a grain is blocked,
|
||||||
the load above it stands still and the belt slips underneath; the top no longer slides over
|
the load above it stands still and the belt slips underneath; the top no longer slides over
|
||||||
a stuck bottom. Filters still let their chosen materials drop through.
|
a stuck bottom. Filters still let their chosen materials drop through.
|
||||||
- **Pick** (middle mouse) selects the piece under the cursor with its direction,
|
- **Pick** (middle mouse) selects the piece under the cursor with its direction,
|
||||||
@@ -447,7 +447,10 @@ above, including:
|
|||||||
2026-10-26).** An overloaded belt line jammed against a screw conveyor above it. The owner
|
2026-10-26).** An overloaded belt line jammed against a screw conveyor above it. The owner
|
||||||
agreed that's realistic and stays the player's problem, but wants a warning on the stalled
|
agreed that's realistic and stays the player's problem, but wants a warning on the stalled
|
||||||
belt and the animation stopped on everything affected, and belts limited to a realistic
|
belt and the animation stopped on everything affected, and belts limited to a realistic
|
||||||
load depth instead of carrying 96-grain stacks. Not built yet.
|
load depth instead of carrying 96-grain stacks. Built 2026-10-29: the owner chose
|
||||||
|
**4 grains (one block)**. Loaded transport warns after half a second without progress,
|
||||||
|
with a marker and advice to clear the outlet or heap. Only affected blocks stop animating;
|
||||||
|
a blocked cleated slope stops its whole run. Screens use their slower nudge cadence.
|
||||||
|
|
||||||
- **Cleats really push their pockets (t42 feedback, 2026-10-26).** The owner found the first
|
- **Cleats really push their pockets (t42 feedback, 2026-10-26).** The owner found the first
|
||||||
cleats passed through heavy loads while the material climbed as before, and wants cleats that
|
cleats passed through heavy loads while the material climbed as before, and wants cleats that
|
||||||
|
|||||||
+28
-15
@@ -167,6 +167,11 @@ flyers and blueprint ghosts. The GPU canvas sits behind the transparent overlay
|
|||||||
`diagonal-screws.js` and `slope-belts.js` share the simulation join geometry with pixel colours,
|
`diagonal-screws.js` and `slope-belts.js` share the simulation join geometry with pixel colours,
|
||||||
smooth tags, ghosts and previews.
|
smooth tags, ghosts and previews.
|
||||||
|
|
||||||
|
Transport backpressure lives in `src/sim/transport-status.js`: transient per-block load,
|
||||||
|
movement, jam age and animation clocks. Flat carry, screens, screws, cleats and bars report
|
||||||
|
progress; only the serial transport step updates it. Both looks use those clocks; smooth
|
||||||
|
receives a view-sized clock texture so geometry tags and shading fields remain intact.
|
||||||
|
|
||||||
## The browser shell
|
## The browser shell
|
||||||
|
|
||||||
- `GAME` (`src/ui/state.js`) holds the UI state: tool, selected piece, build tab, drag,
|
- `GAME` (`src/ui/state.js`) holds the UI state: tool, selected piece, build tab, drag,
|
||||||
@@ -244,7 +249,7 @@ Starter furnace designs and the milestone list shown in the notebook.
|
|||||||
- functions: smelterBlueprint
|
- functions: smelterBlueprint
|
||||||
- variables: STARTER_DESIGNS, MILESTONES
|
- variables: STARTER_DESIGNS, MILESTONES
|
||||||
|
|
||||||
### src/sim/world.js (124 lines)
|
### src/sim/world.js (125 lines)
|
||||||
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
|
World state: the per-cell typed arrays, alloc(), ambient temperature by depth, waking.
|
||||||
- functions: emit, bagCap, discover, sharedArray, alloc, depthU, depthY, meanSurface, computeAmbient, fillNoise, renderDirtyXY, renderDirtyChunk, renderDirtyIdx, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
|
- functions: emit, bagCap, discover, sharedArray, alloc, depthU, depthY, meanSurface, computeAmbient, fillNoise, renderDirtyXY, renderDirtyChunk, renderDirtyIdx, wakeXY, wakeHeatXY, wakeIdx, wakeHeatIdx
|
||||||
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, renderDirty, awake, hot, hotLvl, powderArea, roofDirty, loadWakeOrder, loadWakePending, loadWakeIndex, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, screwLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
|
- variables: W, H, N, CW, CH, NCH, mat, temp, life, flags, stamp, part, mach, digp, noiseB, airS, netId, isPow, occ, surfY, dirtY, graniteY, basaltY, magmaY, roofY, AMB, worldSeed, surfBase, underD, shallowD, lapse, BW, BH, NB, blockP, blockV, ents, bgm, flyers, ghostBlocks, ghostMolds, renderDirty, awake, hot, hotLvl, powderArea, roofDirty, loadWakeOrder, loadWakePending, loadWakeIndex, tick, tickStamp, machines, molds, nets, pcells, powerDirty, beltL, beltR, screwLists, brokenMachines, ghostMachines, ghostBlocked, EV, bot, bag, bagTotal, player, seen, ledger, stats
|
||||||
@@ -284,7 +289,7 @@ 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 (158 lines)
|
### src/sim/machine-tick.js (160 lines)
|
||||||
Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
|
Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
|
||||||
- functions: engineIntake, fanBlow, machinesTick
|
- functions: engineIntake, fanBlow, machinesTick
|
||||||
|
|
||||||
@@ -293,8 +298,8 @@ Travel-directed transport ends. Precomputed probes; world/plan queries allocate
|
|||||||
- functions: pieceEnds, joinCandidate, endJoin, pieceJoins, endAt, joinIsDrop, joinSnap, nearMiss
|
- functions: pieceEnds, joinCandidate, endJoin, pieceJoins, endAt, joinIsDrop, joinSnap, nearMiss
|
||||||
- variables: JOIN_ENDS, NO_PIECE, NEAR_MISS_VIEW
|
- variables: JOIN_ENDS, NO_PIECE, NEAR_MISS_VIEW
|
||||||
|
|
||||||
### src/sim/conveyors.js (213 lines)
|
### src/sim/conveyors.js (215 lines)
|
||||||
How belts, filters and screw conveyors 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 conveyors move what is on or in them. What rests on a belt or filter rides along: the bottom four grains 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: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, screwAccepts, straightScrewNext, diagonalScrewNext
|
- functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, screwAccepts, straightScrewNext, diagonalScrewNext
|
||||||
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
|
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
|
||||||
|
|
||||||
@@ -303,7 +308,12 @@ Throughput for the info overlay (t49): how many grains each belt, filter and scr
|
|||||||
- functions: setFlowCounting, flowNote, flowCross, flowSlopeUnder, flowTick
|
- functions: setFlowCounting, flowNote, flowCross, flowSlopeUnder, flowTick
|
||||||
- variables: flowOn, flowCount, flowRate
|
- variables: flowOn, flowCount, flowRate
|
||||||
|
|
||||||
### src/sim/slope-conveyors.js (231 lines)
|
### src/sim/transport-status.js (38 lines)
|
||||||
|
Local backpressure: only loaded transport that cannot advance warns after half a second.
|
||||||
|
- functions: transportNote, transportStalled, transportClock, transportBegin, transportEnd, transportCleat, transportBar
|
||||||
|
- variables: STALL_TICKS, transportState
|
||||||
|
|
||||||
|
### src/sim/slope-conveyors.js (237 lines)
|
||||||
Cleats are rubber ribs standing up from a sloped belt's face, one every CLEAT_SPACING face cells. Loose grains treat them as solid (cleatMask). Nothing grips the bare face: grains slide down it until a cleat stops them. Each belt step moves every cleat one face cell along the belt, and the rib pushes whatever is in its way ahead of it, so the pocket in front rides along while anything heaped above the rib is left to roll back. If a rib can't push its way, its whole run stalls. Only uphill runs have cleats: downhill, the belt carries its load like a flat one.
|
Cleats are rubber ribs standing up from a sloped belt's face, one every CLEAT_SPACING face cells. Loose grains treat them as solid (cleatMask). Nothing grips the bare face: grains slide down it until a cleat stops them. Each belt step moves every cleat one face cell along the belt, and the rib pushes whatever is in its way ahead of it, so the pocket in front rides along while anything heaped above the rib is left to roll back. If a rib can't push its way, its whole run stalls. Only uphill runs have cleats: downhill, the belt carries its load like a flat one.
|
||||||
- functions: ribMove, cleatFoot, ribCells, slopeUphill, slopeFaceAt, pushRow, scoop, pocketRoom, advanceCleat, slopeRunKey, slopeCleatsTick, rebuildCleatMask, expelFromRib, screenNudge, wakeCleatLoad
|
- functions: ribMove, cleatFoot, ribCells, slopeUphill, slopeFaceAt, pushRow, scoop, pocketRoom, advanceCleat, slopeRunKey, slopeCleatsTick, rebuildCleatMask, expelFromRib, screenNudge, wakeCleatLoad
|
||||||
- variables: CLEAT_SPACING, CLEAT_HEIGHT, CLEAT_REACH, SCREEN_NUDGE_TICKS, cleatChunks, cleatMask, cleatCells, slopePhase, ribLog
|
- variables: CLEAT_SPACING, CLEAT_HEIGHT, CLEAT_REACH, SCREEN_NUDGE_TICKS, cleatChunks, cleatMask, cleatCells, slopePhase, ribLog
|
||||||
@@ -317,7 +327,7 @@ Molds: cavities for castings; ingot troughs empty themselves below. Removal retu
|
|||||||
- 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 (319 lines)
|
### src/sim/items.js (320 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, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entBeltScrewEnter, entSlopeBeltTick, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
|
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entScrewAt, entScrewFree, entScrewMove, entScrewLane, entScrewEdge, entScrewEnter, entScrewTick, entDiagonalScrewTick, entBeltScrewEnter, 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
|
||||||
@@ -374,7 +384,7 @@ The same chunk schedule runs inline or in workers. Workers only own a pass at a
|
|||||||
- functions: threadWorld, threadBind, threadContext, resetChunkWork, inlineChunkWork, chunkEvent, chunkWake, chunkHeat, chunkSeed, runChunks, mergeChunkWork, chunkPasses, threadParams, threadSetParams, threadSlot, threadExecute, threadPass, threadWait, threadFailure, threadMessage
|
- functions: threadWorld, threadBind, threadContext, resetChunkWork, inlineChunkWork, chunkEvent, chunkWake, chunkHeat, chunkSeed, runChunks, mergeChunkWork, chunkPasses, threadParams, threadSetParams, threadSlot, threadExecute, threadPass, threadWait, threadFailure, threadMessage
|
||||||
- variables: simPool, chunkWork, inlineWork
|
- variables: simPool, chunkWork, inlineWork
|
||||||
|
|
||||||
### src/sim/tick.js (29 lines)
|
### src/sim/tick.js (31 lines)
|
||||||
simTick: one simulation step, in order.
|
simTick: one simulation step, in order.
|
||||||
- functions: simTick
|
- functions: simTick
|
||||||
|
|
||||||
@@ -412,7 +422,7 @@ Screw casing geometry shared by both looks, ghosts and placement previews. smoot
|
|||||||
- functions: diagonalScrewJoins, straightScrewJoins, diagonalScrewShape, diagonalScrewBlock
|
- functions: diagonalScrewJoins, straightScrewJoins, diagonalScrewShape, diagonalScrewBlock
|
||||||
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
|
- variables: SCREW_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
|
||||||
|
|
||||||
### src/render/cell.js (276 lines)
|
### src/render/cell.js (284 lines)
|
||||||
Rare-cell shading also treats built pieces as transparent surroundings.
|
Rare-cell shading also treats built pieces as transparent surroundings.
|
||||||
- functions: builtForShade, cellPixel
|
- functions: builtForShade, cellPixel
|
||||||
|
|
||||||
@@ -443,7 +453,7 @@ GPU sources retain the CPU roof and transmission rules; directional relaxation f
|
|||||||
Smooth slope geometry and cleats, ported from slope-belt-shader.js.
|
Smooth slope geometry and cleats, ported from slope-belt-shader.js.
|
||||||
- variables: GPU_SMOOTH_SLOPE_HELPERS, GPU_SMOOTH_SLOPE_DRAW
|
- variables: GPU_SMOOTH_SLOPE_HELPERS, GPU_SMOOTH_SLOPE_DRAW
|
||||||
|
|
||||||
### src/render/gpu/smooth-shader.js (369 lines)
|
### src/render/gpu/smooth-shader.js (371 lines)
|
||||||
Literal port of the legacy smooth shader; dpdy is reversed for WebGPU's top-left origin.
|
Literal port of the legacy smooth shader; dpdy is reversed for WebGPU's top-left origin.
|
||||||
- variables: GPU_SMOOTH
|
- variables: GPU_SMOOTH
|
||||||
|
|
||||||
@@ -451,7 +461,7 @@ Literal port of the legacy smooth shader; dpdy is reversed for WebGPU's top-left
|
|||||||
- functions: smoothSurface
|
- functions: smoothSurface
|
||||||
- variables: SM
|
- variables: SM
|
||||||
|
|
||||||
### src/render/gpu/smooth.js (48 lines)
|
### src/render/gpu/smooth.js (50 lines)
|
||||||
Field mip levels preserve the legacy trilinear AO sample at level 2.2.
|
Field mip levels preserve the legacy trilinear AO sample at level 2.2.
|
||||||
- functions: gpuSmoothInit, gpuSmoothTextures, gpuSmoothBindings, gpuSmoothUpload, gpuSmoothMips
|
- functions: gpuSmoothInit, gpuSmoothTextures, gpuSmoothBindings, gpuSmoothUpload, gpuSmoothMips
|
||||||
- variables: GPU_FIELD_MIP
|
- variables: GPU_FIELD_MIP
|
||||||
@@ -469,7 +479,7 @@ Whole-world textures retain clean chunks across camera moves.
|
|||||||
Upload full chunks so scrolling within a resident chunk needs no extra copy.
|
Upload full chunks so scrolling within a resident chunk needs no extra copy.
|
||||||
- functions: gpuUploadChunks, gpuUploadStrip, gpuCheckUploads
|
- functions: gpuUploadChunks, gpuUploadStrip, gpuCheckUploads
|
||||||
|
|
||||||
### src/render/gpu/draw.js (92 lines)
|
### src/render/gpu/draw.js (94 lines)
|
||||||
Only exceptional blocks and their geometric neighbours run the CPU colour rules.
|
Only exceptional blocks and their geometric neighbours run the CPU colour rules.
|
||||||
- functions: gpuOverrideHas, gpuOverrides, gpuDraw, gpuPresent
|
- functions: gpuOverrideHas, gpuOverrides, gpuDraw, gpuPresent
|
||||||
- variables: GPU_PATTERN
|
- variables: GPU_PATTERN
|
||||||
@@ -482,9 +492,9 @@ Scale colour without changing the smooth-look class.
|
|||||||
Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
|
Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
|
||||||
- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks
|
- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks
|
||||||
|
|
||||||
### src/render/overlays.js (370 lines)
|
### src/render/overlays.js (388 lines)
|
||||||
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
|
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
|
||||||
- functions: spawnFloater, drawRain, drawFlowArrows, drawJoinMark, drawJoinMarks, flowLoaded, drawFlowRates, pingImage, drawPing, drawHoveredVein, drawPingRecharge, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
|
- functions: spawnFloater, drawRain, drawFlowArrows, drawJoinMark, drawJoinMarks, flowLoaded, drawFlowRates, pingImage, drawPing, drawHoveredVein, drawPingRecharge, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles, drawTransportStalls
|
||||||
- variables: PING_SPEED, PING_HOLD, PING_FADE, PING_BANDS, PING_GLINTS, GLINT_MS
|
- variables: PING_SPEED, PING_HOLD, PING_FADE, PING_BANDS, PING_GLINTS, GLINT_MS
|
||||||
|
|
||||||
### src/render/screw-heat.js (29 lines)
|
### src/render/screw-heat.js (29 lines)
|
||||||
@@ -572,7 +582,7 @@ Hover work is shared by the cursor label and the vein outline.
|
|||||||
- functions: oreLike, bitNeeded, pingVisible, cursorVein, hoveredVein, cursorLabelAlpha
|
- functions: oreLike, bitNeeded, pingVisible, cursorVein, hoveredVein, cursorLabelAlpha
|
||||||
- variables: CURSOR_ORES, cursorVeinCache, cursorLabelKey, cursorLabelSince
|
- variables: CURSOR_ORES, cursorVeinCache, cursorLabelKey, cursorLabelSince
|
||||||
|
|
||||||
### src/ui/info.js (216 lines)
|
### src/ui/info.js (221 lines)
|
||||||
Discovery cards, hover info (I) and the cursor label.
|
Discovery cards, hover info (I) and the cursor label.
|
||||||
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
|
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
|
||||||
|
|
||||||
@@ -622,7 +632,7 @@ Shared world colours, minimap and full map.
|
|||||||
Browser-only clocks; simulation code stays usable without a browser or a clock.
|
Browser-only clocks; simulation code stays usable without a browser or a clock.
|
||||||
- variables: PERF_MARKS, PERF_FRAME, perfMarks, perfTimes, perfLast
|
- variables: PERF_MARKS, PERF_FRAME, perfMarks, perfTimes, perfLast
|
||||||
|
|
||||||
### src/ui/loop.js (170 lines)
|
### src/ui/loop.js (171 lines)
|
||||||
The frame loop and boot.
|
The frame loop and boot.
|
||||||
- functions: scanVisible, frame, boot
|
- functions: scanVisible, frame, boot
|
||||||
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
|
- variables: lastT, acc, lastRender, frameMs, simMs, fpsSm, ambientT
|
||||||
@@ -686,6 +696,8 @@ The frame loop and boot.
|
|||||||
- `test/t_slope_belts.js`: Sloped belts: transport, joins, sorting, persistence and the actual drag controls.
|
- `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 WebGPU pass compiles and draws, and the thermal lens still works.
|
- `test/t_smooth.py`: The smooth look: the WebGPU pass compiles and draws, and the thermal lens still works.
|
||||||
|
- `test/t_stalls.js`: t45: finite carrying bed, local sustained jams, recovery, filters, screws and bars.
|
||||||
|
- `test/t_stalls.py`: t45: real transport warnings, hover, frozen animation and recovery in both looks.
|
||||||
- `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.
|
||||||
- `test/t_threads.js`: Shared allocation, checkerboard independence, and real worker-count determinism.
|
- `test/t_threads.js`: Shared allocation, checkerboard independence, and real worker-count determinism.
|
||||||
- `test/t_vacuum_collapse.js`: Real nozzle cuts into settled heaps, inline and through the real Node worker pool.
|
- `test/t_vacuum_collapse.js`: Real nozzle cuts into settled heaps, inline and through the real Node worker pool.
|
||||||
@@ -706,6 +718,7 @@ The frame loop and boot.
|
|||||||
- `tools/test_perf.py`: Profiler checks: headers, summaries/comparison, and unchanged pixels/simulation with clocks on.
|
- `tools/test_perf.py`: Profiler checks: headers, summaries/comparison, and unchanged pixels/simulation with clocks on.
|
||||||
- `tools/use_venv.py`: Let browser scripts run with plain python3 by switching to the repo's .venv.
|
- `tools/use_venv.py`: Let browser scripts run with plain python3 by switching to the repo's .venv.
|
||||||
- `tools/where.py`: Turn a line number in dist/index.html into the source file and line it came from.
|
- `tools/where.py`: Turn a line number in dist/index.html into the source file and line it came from.
|
||||||
|
- `tools/experiments/belt-depth.js`: t45: sustained normal (one layer) and heavy (16 layers) flat-belt feeds, open sink.
|
||||||
- `tools/experiments/cast_sunk_mold.js`: Block-sized bit mold sunk into the ground with a stone furnace built over its funnel.
|
- `tools/experiments/cast_sunk_mold.js`: Block-sized bit mold sunk into the ground with a stone furnace built over its funnel.
|
||||||
- `tools/experiments/factory_cost.js`: What does a running factory cost when the player is somewhere else? Builds K self-running modules in a generated world, far from the bot, and measures simulation time per tick. Each module: a coal + copper-ore furnace burning in a stone-block box, a spring feeding a water wheel, a shaft carrying its power to a 10-block belt that hauls sand off a hopper.
|
- `tools/experiments/factory_cost.js`: What does a running factory cost when the player is somewhere else? Builds K self-running modules in a generated world, far from the bot, and measures simulation time per tick. Each module: a coal + copper-ore furnace burning in a stone-block box, a spring feeding a water wheel, a shaft carrying its power to a 10-block belt that hauls sand off a hopper.
|
||||||
- `tools/experiments/flow.js`: Throughput of belts and screw conveyors: grains delivered per second. Whatever lands past the outlet is taken away, so the output pile never backs up into the line.
|
- `tools/experiments/flow.js`: Throughput of belts and screw conveyors: grains delivered per second. Whatever lands past the outlet is taken away, so the output pile never backs up into the line.
|
||||||
|
|||||||
@@ -14,6 +14,19 @@ prints the failing checks.
|
|||||||
|
|
||||||
## Headless suites
|
## Headless suites
|
||||||
|
|
||||||
|
For t45, `node test/t_stalls.js` checks the four-grain bed, sustained local jams,
|
||||||
|
recovery, bars, cleated runs, slower screens and an overloaded closed loop.
|
||||||
|
`test/t_threads.js` includes loaded belts and slopes, and hashes stall/animation state
|
||||||
|
as well as cell bytes with inline, 1, 2 and 8 workers (including repeats).
|
||||||
|
`python3 test/t_stalls.py` checks both looks: hover advice, always-visible warnings,
|
||||||
|
fog, actual frozen belt/filter/screw pixels and clearing the blockage. It saves
|
||||||
|
`shots/t45-stalled.png` and `shots/t45-stalled-smooth.png`.
|
||||||
|
`node tools/experiments/belt-depth.js` measures sustained one- and sixteen-layer feeds
|
||||||
|
on a flat belt: seed 7, 220 warm-up ticks and 1200 measured ticks, with an open sink.
|
||||||
|
The side-fed lift comparison in `t_conveyors.js` now uses a four-grain bed in both
|
||||||
|
fixtures rather than the old sixty-grain heap and its hard-coded delivery baseline;
|
||||||
|
it retains the 90% comparison, conservation and all-lane checks.
|
||||||
|
|
||||||
For t69, `node test/t_screw_heat.js` checks full-cost repair, load preservation, undo,
|
For t69, `node test/t_screw_heat.js` checks full-cost repair, load preservation, undo,
|
||||||
all screw directions, the unchanged melt threshold, and saved damage/message state.
|
all screw directions, the unchanged melt threshold, and saved damage/message state.
|
||||||
`python3 test/t_screw_heat.py` is the focused browser check for both looks, hover,
|
`python3 test/t_screw_heat.py` is the focused browser check for both looks, hover,
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ 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-29: t45 belt depth and visible transport stalls (Codex)
|
||||||
|
|
||||||
|
- Four-grain belt bed; local half-second jam warnings, hover advice and frozen/resuming
|
||||||
|
animation in both looks, including screws, screens, bars and whole cleated runs.
|
||||||
|
- Files: sim/{conveyors,machine-tick,slope-conveyors,transport-status,items,tick,world},
|
||||||
|
render/{cell,slope-belts,overlays,gpu/{draw,smooth,smooth-shader,smooth-slope}},
|
||||||
|
ui/{info,loop}, manifest; test/{t_stalls.js,t_stalls.py,t_conveyors,t_threads,fixtures/t45-loop},
|
||||||
|
tools/experiments/belt-depth.js, docs/{DESIGN,TESTING,PROJECT_MAP}, task and this log.
|
||||||
|
- New checks fail on original code; all 42 headless suites pass. Browser t_stalls.py,
|
||||||
|
play.py, t_smooth.py and t_gpu.py pass with host GPU; sandbox startup timed out.
|
||||||
|
Fixed GPU pan/zoom clock-buffer recreation caught by t_gpu.py. Worker counts 0/1/2/8
|
||||||
|
match cells and jam/clocks. Inspected shots/t45-stalled{,-smooth}.png. Map regenerated.
|
||||||
|
- Normal feed 30.0 -> 30.0, heavy 360.0 -> 90.0 grains/s; uphill 78.8. Preserved the
|
||||||
|
separate 96-cell cleat push bound. Updated lift fixture to the chosen four-grain bed;
|
||||||
|
retained its 90% comparison, conservation and all-lane checks. Worldgen untouched.
|
||||||
|
No open blockers; full browser suite not run. Ready for owner review; no commit.
|
||||||
|
|
||||||
## 2026-10-29: t89 custom game cursors (Codex)
|
## 2026-10-29: t89 custom game cursors (Codex)
|
||||||
|
|
||||||
- Finished interrupted work: hollow build arrow at the exact mouse, open coloured crosses for
|
- Finished interrupted work: hollow build arrow at the exact mouse, open coloured crosses for
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"sim/joins.js",
|
"sim/joins.js",
|
||||||
"sim/conveyors.js",
|
"sim/conveyors.js",
|
||||||
"sim/flow.js",
|
"sim/flow.js",
|
||||||
|
"sim/transport-status.js",
|
||||||
"sim/slope-conveyors.js",
|
"sim/slope-conveyors.js",
|
||||||
"sim/blocks.js",
|
"sim/blocks.js",
|
||||||
"sim/molds.js",
|
"sim/molds.js",
|
||||||
|
|||||||
+10
-2
@@ -15,7 +15,8 @@ function cellPixel(x, y) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const useFog = SET.fog, grain = SET.grain, incand = SET.incandescence;
|
const useFog = SET.fog, grain = SET.grain, incand = SET.incandescence;
|
||||||
const anim = R.frame, lens = R.lens, tl = THERM[SET.thermalPalette] || THERM.inferno;
|
let anim = R.frame;
|
||||||
|
const lens = R.lens, tl = THERM[SET.thermalPalette] || THERM.inferno;
|
||||||
const glowOn = SET.glow !== 'off' && !lens, gpx = R.glowPx, gw = R.glowW;
|
const glowOn = SET.glow !== 'off' && !lens, gpx = R.glowPx, gw = R.glowW;
|
||||||
const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW;
|
const fy = y / FOG, fj = Math.floor(fy), ft = fy - fj, fr = fj * FW, fn = Math.min(FH - 1, fj + 1) * FW;
|
||||||
const yw = y * W;
|
const yw = y * W;
|
||||||
@@ -36,6 +37,8 @@ function cellPixel(x, y) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
const i = yw + x, m = mat[i], nz = noiseB[i] - 128;
|
||||||
|
const ownBlock = (y >> 2) * BW + (x >> 2);
|
||||||
|
if (isBelt(blockP[ownBlock]) || isFilter(blockP[ownBlock]) || blockP[ownBlock] === BP_SCREW) anim = transportClock(ownBlock);
|
||||||
const T = temp[i];
|
const T = temp[i];
|
||||||
// Also used by the thermal lens, which always draws in pixels.
|
// Also used by the thermal lens, which always draws in pixels.
|
||||||
let looseShade = 1;
|
let looseShade = 1;
|
||||||
@@ -138,6 +141,7 @@ function cellPixel(x, y) {
|
|||||||
const db = diagonalScrewBlock(x, y, sm);
|
const db = diagonalScrewBlock(x, y, sm);
|
||||||
let diagonal = false;
|
let diagonal = false;
|
||||||
if (db >= 0) {
|
if (db >= 0) {
|
||||||
|
anim = transportClock(db);
|
||||||
const v = blockV[db], joins = diagonalJoins[db];
|
const v = blockV[db], joins = diagonalJoins[db];
|
||||||
const sd = diagonalScrewShape(x + 0.5, y + 0.5, db, v, joins);
|
const sd = diagonalScrewShape(x + 0.5, y + 0.5, db, v, joins);
|
||||||
p = sm || sd <= 0 ? SCREW_PARTS[v] : 0;
|
p = sm || sd <= 0 ? SCREW_PARTS[v] : 0;
|
||||||
@@ -193,7 +197,7 @@ function cellPixel(x, y) {
|
|||||||
}
|
}
|
||||||
const slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
|
const slopeArt = sb >= 0 && (m === EMPTY || PH[m] === PH_GAS || m === BELTL || m === BELTR);
|
||||||
if (slopeArt && !sm) {
|
if (slopeArt && !sm) {
|
||||||
const color = slopeBeltColor(sb, x, y, anim, br, bgc, bb);
|
const color = slopeBeltColor(sb, x, y, transportClock(sb), br, bgc, bb);
|
||||||
r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255;
|
r = color & 255; g = color >> 8 & 255; b = color >> 16 & 255;
|
||||||
}
|
}
|
||||||
const slopeDetail = slopeDetailAt(i, x, y, sm);
|
const slopeDetail = slopeDetailAt(i, x, y, sm);
|
||||||
@@ -272,5 +276,9 @@ function cellPixel(x, y) {
|
|||||||
bpx[row + bx] = fogColor(c, k); tpx[row + bx] = fogColor(t, k);
|
bpx[row + bx] = fogColor(c, k); tpx[row + bx] = fogColor(t, k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (sm && GPU.motionData) {
|
||||||
|
const owner = slopeDetail && slopeWallOwner >= 0 ? slopeWallOwner : sb >= 0 ? sb : db >= 0 ? db : ownBlock;
|
||||||
|
GPU.motionData[row + bx] = transportClock(owner) / 60;
|
||||||
|
}
|
||||||
px[row + bx] = top | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
px[row + bx] = top | ((b > 255 ? 255 : b) << 16) | ((g > 255 ? 255 : g) << 8) | (r > 255 ? 255 : r);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ function gpuOverrides() {
|
|||||||
if (R.smoothOn) {
|
if (R.smoothOn) {
|
||||||
if (!GPU.overrideMask || GPU.overrideMask.length !== R.px.length) GPU.overrideMask = new Uint8Array(R.px.length);
|
if (!GPU.overrideMask || GPU.overrideMask.length !== R.px.length) GPU.overrideMask = new Uint8Array(R.px.length);
|
||||||
GPU.overrideMask.fill(0);
|
GPU.overrideMask.fill(0);
|
||||||
|
if (!GPU.motionData || GPU.motionData.length !== R.px.length) GPU.motionData = new Float32Array(R.px.length);
|
||||||
|
else GPU.motionData.fill(0);
|
||||||
}
|
}
|
||||||
if (!GPU.blocks || GPU.blocks.length !== NB) GPU.blocks = new Uint8Array(NB);
|
if (!GPU.blocks || GPU.blocks.length !== NB) GPU.blocks = new Uint8Array(NB);
|
||||||
const marked = GPU.blocks; marked.fill(0);
|
const marked = GPU.blocks; marked.fill(0);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ struct Params { view: vec4f, world: vec4f, style: vec4f, effects: vec4f, camera:
|
|||||||
@group(0) @binding(6) var tGlow: texture_2d<f32>;
|
@group(0) @binding(6) var tGlow: texture_2d<f32>;
|
||||||
@group(0) @binding(7) var linear: sampler;
|
@group(0) @binding(7) var linear: sampler;
|
||||||
@group(0) @binding(8) var<uniform> su: vec4f;
|
@group(0) @binding(8) var<uniform> su: vec4f;
|
||||||
|
@group(0) @binding(9) var tMotion: texture_2d<f32>;
|
||||||
fn sampleLevel(t: texture_2d<f32>, uv: vec2f, level: f32) -> vec4f { return textureSampleLevel(t,linear,uv,level); }
|
fn sampleLevel(t: texture_2d<f32>, uv: vec2f, level: f32) -> vec4f { return textureSampleLevel(t,linear,uv,level); }
|
||||||
fn modulo(x: f32,y: f32) -> f32 { return x-y*floor(x/y); }
|
fn modulo(x: f32,y: f32) -> f32 { return x-y*floor(x/y); }
|
||||||
fn h21(inputP: vec2f) -> f32 { var p=inputP; p = fract(p * vec2f(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
|
fn h21(inputP: vec2f) -> f32 { var p=inputP; p = fract(p * vec2f(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
|
||||||
@@ -51,9 +52,9 @@ fn beltLineSD(c: vec2i, lb: vec2f) -> f32 {
|
|||||||
return beltSD(lb, (caps & 1) != 0, (caps & 2) != 0);
|
return beltSD(lb, (caps & 1) != 0, (caps & 2) != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bandRidge(w: vec2f, lb: vec2f, dir: f32) -> f32 {
|
fn bandRidge(w: vec2f, lb: vec2f, dir: f32, time: f32) -> f32 {
|
||||||
var run: f32 = select( -1.0, 1.0 , lb.y < 2.0 );
|
var run: f32 = select( -1.0, 1.0 , lb.y < 2.0 );
|
||||||
return step(0.72, fract((w.x - dir * run * (su.x) * 30.0) * 0.5));
|
return step(0.72, fract((w.x - dir * run * time * 30.0) * 0.5));
|
||||||
}
|
}
|
||||||
fn bandColor(lb: vec2f, rg: f32) -> vec3f { return mix(vec3f(0.2, 0.21, 0.24), vec3f(0.34, 0.35, 0.39), rg) * (select( 0.72, 1.0 ,lb.y < 2.0 )); }
|
fn bandColor(lb: vec2f, rg: f32) -> vec3f { return mix(vec3f(0.2, 0.21, 0.24), vec3f(0.34, 0.35, 0.39), rg) * (select( 0.72, 1.0 ,lb.y < 2.0 )); }
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ ${GPU_SMOOTH_SLOPE_HELPERS}
|
|||||||
var qq: vec2f = q - 0.5; var c0: vec2i = vec2i(floor(qq)); var t: vec2f = qq - floor(qq);
|
var qq: vec2f = q - 0.5; var c0: vec2i = vec2i(floor(qq)); var t: vec2f = qq - floor(qq);
|
||||||
var k0: vec4f = cellAt(tCol, c0); var k1: vec4f = cellAt(tCol, c0 + vec2i(1, 0)); var k2: vec4f = cellAt(tCol, c0 + vec2i(0, 1)); var k3: vec4f = cellAt(tCol, c0 + vec2i(1, 1));
|
var k0: vec4f = cellAt(tCol, c0); var k1: vec4f = cellAt(tCol, c0 + vec2i(1, 0)); var k2: vec4f = cellAt(tCol, c0 + vec2i(0, 1)); var k3: vec4f = cellAt(tCol, c0 + vec2i(1, 1));
|
||||||
var n0: i32 = cls(k0); var n1: i32 = cls(k1); var n2: i32 = cls(k2); var n3: i32 = cls(k3);
|
var n0: i32 = cls(k0); var n1: i32 = cls(k1); var n2: i32 = cls(k2); var n3: i32 = cls(k3);
|
||||||
|
let motionTime = cellAt(tMotion, c).r;
|
||||||
var own: vec4f = cellAt(tCol, c); var nc: i32 = cls(own);
|
var own: vec4f = cellAt(tCol, c); var nc: i32 = cls(own);
|
||||||
var s: vec2f = smoothstep(vec2f(0.1), vec2f(0.9), t);
|
var s: vec2f = smoothstep(vec2f(0.1), vec2f(0.9), t);
|
||||||
var wq: vec4f = vec4f((1.0 - s.x) * (1.0 - s.y), s.x * (1.0 - s.y), (1.0 - s.x) * s.y, s.x * s.y);
|
var wq: vec4f = vec4f((1.0 - s.x) * (1.0 - s.y), s.x * (1.0 - s.y), (1.0 - s.x) * s.y, s.x * s.y);
|
||||||
@@ -228,14 +230,14 @@ ${GPU_SMOOTH_SLOPE_DRAW}
|
|||||||
var edge: f32 = -sd;
|
var edge: f32 = -sd;
|
||||||
var bc: vec3f;
|
var bc: vec3f;
|
||||||
if (edge < 0.8) {
|
if (edge < 0.8) {
|
||||||
bc = bandColor(lb, bandRidge(w, lb, dir));
|
bc = bandColor(lb, bandRidge(w, lb, dir, motionTime));
|
||||||
bc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
|
bc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
|
||||||
} else {
|
} else {
|
||||||
var rc: vec2f = vec2f(select( 3.0, 1.0 ,lb.x < 2.0 ), 2.0); var dv: vec2f = lb - rc;
|
var rc: vec2f = vec2f(select( 3.0, 1.0 ,lb.x < 2.0 ), 2.0); var dv: vec2f = lb - rc;
|
||||||
var rd: f32 = length(dv);
|
var rd: f32 = length(dv);
|
||||||
bc = vec3f(0.15, 0.16, 0.18);
|
bc = vec3f(0.15, 0.16, 0.18);
|
||||||
if (rd < 0.95) {
|
if (rd < 0.95) {
|
||||||
var ang: f32 = atan2(dv.y, dv.x) - dir * (su.x) * 30.0 / 0.95;
|
var ang: f32 = atan2(dv.y, dv.x) - dir * motionTime * 30.0 / 0.95;
|
||||||
var spoke: f32 = smoothstep(0.55, 0.85, cos(ang * 3.0));
|
var spoke: f32 = smoothstep(0.55, 0.85, cos(ang * 3.0));
|
||||||
bc = mix(vec3f(0.5, 0.36, 0.25), vec3f(0.72, 0.53, 0.36), 0.5 + 0.5 * dot(normalize(dv + 1e-4), vec2f(-0.6, -0.8)) * smoothstep(0.2, 0.95, rd));
|
bc = mix(vec3f(0.5, 0.36, 0.25), vec3f(0.72, 0.53, 0.36), 0.5 + 0.5 * dot(normalize(dv + 1e-4), vec2f(-0.6, -0.8)) * smoothstep(0.2, 0.95, rd));
|
||||||
bc *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
|
bc *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
|
||||||
@@ -294,7 +296,7 @@ ${GPU_SMOOTH_SLOPE_DRAW}
|
|||||||
tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a)));
|
tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a)));
|
||||||
} else {
|
} else {
|
||||||
var full: bool = own.r + own.g + own.b > 0.0;
|
var full: bool = own.r + own.g + own.b > 0.0;
|
||||||
var fl: f32 = fract((along + a * 0.9 - sp2 * (su.x) * 14.0) * 0.5);
|
var fl: f32 = fract((along + a * 0.9 - sp2 * motionTime * 14.0) * 0.5);
|
||||||
var flight: f32 = smoothstep(0.82, 0.92, fl) * (1.0 - smoothstep(0.92, 1.0, fl));
|
var flight: f32 = smoothstep(0.82, 0.92, fl) * (1.0 - smoothstep(0.92, 1.0, fl));
|
||||||
var bore: vec3f = vec3f(0.11, 0.1, 0.11) * (0.8 + 0.4 * (1.0 - abs(a - 2.0) / 1.4));
|
var bore: vec3f = vec3f(0.11, 0.1, 0.11) * (0.8 + 0.4 * (1.0 - abs(a - 2.0) / 1.4));
|
||||||
tc = select( bore, own.rgb * (0.9 + 0.1 * vn(w * 1.3)) ,full );
|
tc = select( bore, own.rgb * (0.9 + 0.1 * vn(w * 1.3)) ,full );
|
||||||
@@ -343,7 +345,7 @@ ${GPU_SMOOTH_SLOPE_DRAW}
|
|||||||
var hole: f32 = 1.0 - smoothstep(0.5 - bw2 - px, 0.5 - bw2, max(cg.x, cg.y));
|
var hole: f32 = 1.0 - smoothstep(0.5 - bw2 - px, 0.5 - bw2, max(cg.x, cg.y));
|
||||||
var fc: vec3f;
|
var fc: vec3f;
|
||||||
if (edge < 0.8) {
|
if (edge < 0.8) {
|
||||||
var rg: f32 = bandRidge(w, lb, dir);
|
var rg: f32 = bandRidge(w, lb, dir, motionTime);
|
||||||
var band: vec3f = bandColor(lb, rg);
|
var band: vec3f = bandColor(lb, rg);
|
||||||
if (tinted && (carry == (lb.y < 2.0))) { band = mix(band, tag.rgb * (select( 0.8, 1.0 ,lb.y < 2.0 )), 0.75 * rg); }
|
if (tinted && (carry == (lb.y < 2.0))) { band = mix(band, tag.rgb * (select( 0.8, 1.0 ,lb.y < 2.0 )), 0.75 * rg); }
|
||||||
fc = mix(band, behind * 0.85, hole * (1.0 - rg) * 0.75);
|
fc = mix(band, behind * 0.85, hole * (1.0 - rg) * 0.75);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const GPU_SMOOTH_SLOPE_DRAW = ` if (nc == 31 || nc == 32) {
|
|||||||
var local: vec2f = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w);
|
var local: vec2f = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w);
|
||||||
var shapePos: vec3f = slopeShape(local, w - local, grade, joins);
|
var shapePos: vec3f = slopeShape(local, w - local, grade, joins);
|
||||||
var sd: f32 = shapePos.x; var axis: f32 = shapePos.y; var depth: f32 = shapePos.z;
|
var sd: f32 = shapePos.x; var axis: f32 = shapePos.y; var depth: f32 = shapePos.z;
|
||||||
var time: f32 = select( 0.0, (su.x) , !mesh && (joins & 16) != 0 );
|
var time: f32 = select( 0.0, motionTime , !mesh && (joins & 16) != 0 );
|
||||||
var lb: vec2f = vec2f(axis, depth); var alongWorld: vec2f = vec2f(axis, depth);
|
var lb: vec2f = vec2f(axis, depth); var alongWorld: vec2f = vec2f(axis, depth);
|
||||||
var rg: f32 = step(0.72, fract((axis - dir * (select( -1.0, 1.0 ,depth < 2.0 )) * time * 30.0) * 0.5));
|
var rg: f32 = step(0.72, fract((axis - dir * (select( -1.0, 1.0 ,depth < 2.0 )) * time * 30.0) * 0.5));
|
||||||
var belt: vec3f;
|
var belt: vec3f;
|
||||||
|
|||||||
@@ -19,16 +19,17 @@ async function gpuSmoothInit() {
|
|||||||
function gpuSmoothTextures() {
|
function gpuSmoothTextures() {
|
||||||
const w=R.bw,h=R.bh;
|
const w=R.bw,h=R.bh;
|
||||||
GPU.smoothOver=Array.from({length:3},()=>gpuTexture(w,h));
|
GPU.smoothOver=Array.from({length:3},()=>gpuTexture(w,h));
|
||||||
|
GPU.motion=gpuTexture(w,h,'r32float');
|
||||||
GPU.smoothMask=gpuTexture(w,h,'r8uint');
|
GPU.smoothMask=gpuTexture(w,h,'r8uint');
|
||||||
GPU.backTex=gpuTexture(w,h,'rgba8unorm',true);
|
GPU.backTex=gpuTexture(w,h,'rgba8unorm',true);
|
||||||
GPU.treeTex=gpuTexture(w,h,'rgba8unorm',true);
|
GPU.treeTex=gpuTexture(w,h,'rgba8unorm',true);
|
||||||
GPU.fieldTex=GPU.device.createTexture({size:[w,h],format:'rgba8unorm',mipLevelCount:Math.min(4,1+Math.floor(Math.log2(Math.max(w,h)))),usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.STORAGE_BINDING|GPUTextureUsage.RENDER_ATTACHMENT});
|
GPU.fieldTex=GPU.device.createTexture({size:[w,h],format:'rgba8unorm',mipLevelCount:Math.min(4,1+Math.floor(Math.log2(Math.max(w,h)))),usage:GPUTextureUsage.TEXTURE_BINDING|GPUTextureUsage.STORAGE_BINDING|GPUTextureUsage.RENDER_ATTACHMENT});
|
||||||
GPU.viewTextures.push(...GPU.smoothOver,GPU.smoothMask,GPU.backTex,GPU.treeTex,GPU.fieldTex);
|
GPU.viewTextures.push(...GPU.smoothOver,GPU.smoothMask,GPU.motion,GPU.backTex,GPU.treeTex,GPU.fieldTex);
|
||||||
}
|
}
|
||||||
function gpuSmoothBindings(group,view,buffer) {
|
function gpuSmoothBindings(group,view,buffer) {
|
||||||
const colourResources=[buffer(GPU.uniform),buffer(GPU.lut),buffer(GPU.land),...GPU.worldTextures.slice(0,9).map(view),view(GPU.override),view(GPU.overrideGlow),view(GPU.colourTex),null,view(GPU.backTex),view(GPU.treeTex),GPU.fieldTex.createView({baseMipLevel:0,mipLevelCount:1}),...GPU.smoothOver.map(view),view(GPU.smoothMask)];
|
const colourResources=[buffer(GPU.uniform),buffer(GPU.lut),buffer(GPU.land),...GPU.worldTextures.slice(0,9).map(view),view(GPU.override),view(GPU.overrideGlow),view(GPU.colourTex),null,view(GPU.backTex),view(GPU.treeTex),GPU.fieldTex.createView({baseMipLevel:0,mipLevelCount:1}),...GPU.smoothOver.map(view),view(GPU.smoothMask)];
|
||||||
GPU.bindColourSmooth=GPU.device.createBindGroup({layout:GPU.colourSmooth.getBindGroupLayout(0),entries:colourResources.map((resource,binding)=>({binding,resource})).filter(e=>e.resource)});
|
GPU.bindColourSmooth=GPU.device.createBindGroup({layout:GPU.colourSmooth.getBindGroupLayout(0),entries:colourResources.map((resource,binding)=>({binding,resource})).filter(e=>e.resource)});
|
||||||
GPU.bindSmooth=group(GPU.smooth,[buffer(GPU.uniform),view(GPU.colourTex),view(GPU.backTex),view(GPU.treeTex),view(GPU.fieldTex),view(GPU.lightTex),view(GPU.glowTex),GPU.sampler,buffer(GPU.smoothUniform)]);
|
GPU.bindSmooth=group(GPU.smooth,[buffer(GPU.uniform),view(GPU.colourTex),view(GPU.backTex),view(GPU.treeTex),view(GPU.fieldTex),view(GPU.lightTex),view(GPU.glowTex),GPU.sampler,buffer(GPU.smoothUniform),view(GPU.motion)]);
|
||||||
GPU.fieldMips=[];
|
GPU.fieldMips=[];
|
||||||
for(let i=1;i<GPU.fieldTex.mipLevelCount;i++) GPU.fieldMips.push({
|
for(let i=1;i<GPU.fieldTex.mipLevelCount;i++) GPU.fieldMips.push({
|
||||||
bind:group(GPU.fieldMip,[GPU.fieldTex.createView({baseMipLevel:i-1,mipLevelCount:1}),GPU.sampler]),
|
bind:group(GPU.fieldMip,[GPU.fieldTex.createView({baseMipLevel:i-1,mipLevelCount:1}),GPU.sampler]),
|
||||||
@@ -36,6 +37,7 @@ function gpuSmoothBindings(group,view,buffer) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function gpuSmoothUpload() {
|
function gpuSmoothUpload() {
|
||||||
|
gpuWrite(GPU.motion,GPU.motionData,R.bw,0,0,R.bw,R.bh);
|
||||||
for(const [i,a] of [R.backPx,R.treePx,R.fieldPx].entries()) gpuWrite(GPU.smoothOver[i],a,R.bw,0,0,R.bw,R.bh);
|
for(const [i,a] of [R.backPx,R.treePx,R.fieldPx].entries()) gpuWrite(GPU.smoothOver[i],a,R.bw,0,0,R.bw,R.bh);
|
||||||
gpuWrite(GPU.smoothMask,GPU.overrideMask,R.bw,0,0,R.bw,R.bh);
|
gpuWrite(GPU.smoothMask,GPU.overrideMask,R.bw,0,0,R.bw,R.bh);
|
||||||
GPU.device.queue.writeBuffer(GPU.smoothUniform,0,new Float32Array([(performance.now()-SM.t0)/1000,smoothSurface(),SET.glow==='off'?0:SET.glow==='high'?0.9:0.55,0]));
|
GPU.device.queue.writeBuffer(GPU.smoothUniform,0,new Float32Array([(performance.now()-SM.t0)/1000,smoothSurface(),SET.glow==='off'?0:SET.glow==='high'?0.9:0.55,0]));
|
||||||
|
|||||||
@@ -368,3 +368,21 @@ function drawParticles() {
|
|||||||
ctx.fillRect(s.x, s.y, sz, sz);
|
ctx.fillRect(s.x, s.y, sz, sz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always visible, in both looks: amber warning on each stopped transport block.
|
||||||
|
function drawTransportStalls() {
|
||||||
|
const ctx = R.ctx, v = viewCells(), s = BS * R.zp;
|
||||||
|
ctx.save();
|
||||||
|
for (const [b] of transportState) {
|
||||||
|
if (!transportStalled(b)) continue;
|
||||||
|
const x = b % BW * BS, y = Math.floor(b / BW) * BS;
|
||||||
|
if (x + BS < R.camX || x > R.camX + v.w || y + BS < R.camY || y > R.camY + v.h || (SET.fog && fogSeen(x + 2, y + 2) < 64)) continue;
|
||||||
|
const p = worldToScreenPx(x, y);
|
||||||
|
ctx.fillStyle = 'rgba(255,164,48,0.25)'; ctx.fillRect(p.x, p.y, s, s);
|
||||||
|
ctx.strokeStyle = '#ffbd59'; ctx.lineWidth = Math.max(1, R.zp * 0.25); ctx.strokeRect(p.x, p.y, s, s);
|
||||||
|
ctx.fillStyle = '#ffdc85';
|
||||||
|
ctx.fillRect(p.x + s * 0.45, p.y + s * 0.15, s * 0.1, s * 0.4);
|
||||||
|
ctx.fillRect(p.x + s * 0.45, p.y + s * 0.7, s * 0.1, s * 0.1);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function prepareSlopeBelts(x0, y0, x1, y1) {
|
|||||||
if (c === BP_BELT || c === BP_FILTER) beltCaps[b] = flatBeltCaps(b);
|
if (c === BP_BELT || c === BP_FILTER) beltCaps[b] = flatBeltCaps(b);
|
||||||
if (!isSlopeBelt(c)) continue;
|
if (!isSlopeBelt(c)) continue;
|
||||||
const face = beltFaceY(c, bx, by, bx * BS) * W + bx * BS;
|
const face = beltFaceY(c, bx, by, bx * BS) * W + bx * BS;
|
||||||
slopeBeltJoins[b] = slopeBeltConnections(bx, by, c) | (netSat(face) > 0 ? 16 : 0) | (filterCfg.get(b)?.inv ? 32 : 0) | (isFilter(c) && netSat(face) > 0 && screenLoaded(b) && (tick & 2) ? 64 : 0);
|
slopeBeltJoins[b] = slopeBeltConnections(bx, by, c) | (netSat(face) > 0 ? 16 : 0) | (filterCfg.get(b)?.inv ? 32 : 0) | (isFilter(c) && netSat(face) > 0 && screenLoaded(b) && !transportStalled(b) && (tick & 2) ? 64 : 0);
|
||||||
for (let yy = Math.max(0, by - 2); yy <= Math.min(BH - 1, by + 1); yy++)
|
for (let yy = Math.max(0, by - 2); yy <= Math.min(BH - 1, by + 1); yy++)
|
||||||
for (let xx = Math.max(0, bx - 1); xx <= Math.min(BW - 1, bx + 1); xx++) slopeBeltHalo[yy * BW + xx] = 1;
|
for (let xx = Math.max(0, bx - 1); xx <= Math.min(BW - 1, bx + 1); xx++) slopeBeltHalo[yy * BW + xx] = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// How belts, filters and screw conveyors move what is on or in them.
|
// How belts, filters and screw conveyors move what is on or in them.
|
||||||
// What rests on a belt or filter rides along: the whole stack standing on the carrying surface
|
// What rests on a belt or filter rides along: the bottom four grains standing on the carrying surface
|
||||||
// steps one cell toward d together (belts run front of the line first, so the way ahead is
|
// 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
|
// 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 = 4;
|
||||||
// Find the next carrying face at the same height or one grain above/below it.
|
// Find the next carrying face at the same height or one grain above/below it.
|
||||||
function conveyorStep(x, y, d) {
|
function conveyorStep(x, y, d) {
|
||||||
const nx = x + d;
|
const nx = x + d;
|
||||||
@@ -28,11 +28,12 @@ function carryColumn(a, d, pass) {
|
|||||||
const m = mat[a];
|
const m = mat[a];
|
||||||
if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return; // the top of the heap, or a ceiling
|
if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return; // the top of the heap, or a ceiling
|
||||||
if (PH[m] !== PH_POWDER || stamp[a] === tickStamp || occ[a]) continue;
|
if (PH[m] !== PH_POWDER || stamp[a] === tickStamp || occ[a]) continue;
|
||||||
|
transportNote(carrier);
|
||||||
if (pass && pass[m]) {
|
if (pass && pass[m]) {
|
||||||
if (enter(m, PH_POWDER, a + W, 0, 1)) continue; // falls through on its own
|
if (enter(m, PH_POWDER, a + W, 0, 1)) { transportNote(carrier, true); continue; } // falls through on its own
|
||||||
// the rollers shake the load: what the filter passes works its way down through the rest
|
// the rollers shake the load: what the filter passes works its way down through the rest
|
||||||
const u = mat[a + W];
|
const u = mat[a + W];
|
||||||
if (L > 0 && !pass[u] && PH[u] === PH_POWDER && stamp[a + W] !== tickStamp && !occ[a + W] && rnd() < 0.5) { const x = a % W, y = (a / W) | 0; move(a, a + W, x, y, x, y + 1); continue; }
|
if (L > 0 && !pass[u] && PH[u] === PH_POWDER && stamp[a + W] !== tickStamp && !occ[a + W] && rnd() < 0.5) { const x = a % W, y = (a / W) | 0; move(a, a + W, x, y, x, y + 1); transportNote(carrier, true); continue; }
|
||||||
}
|
}
|
||||||
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;
|
||||||
@@ -50,6 +51,7 @@ function carryColumn(a, d, pass) {
|
|||||||
// 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 + step, d * 0.5, step * 0.5);
|
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y + step, d * 0.5, step * 0.5);
|
||||||
else move(a, j, x, y, nx, y + step);
|
else move(a, j, x, y, nx, y + step);
|
||||||
|
transportNote(carrier, true);
|
||||||
stats.moved++;
|
stats.moved++;
|
||||||
if (flowOn && (x >> 2) !== (nx >> 2)) flowNote(carrier);
|
if (flowOn && (x >> 2) !== (nx >> 2)) flowNote(carrier);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -192,7 +192,7 @@ function entSlopeBeltTick(e) {
|
|||||||
}
|
}
|
||||||
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], oldX = e.ix, oldY = e.iy;
|
||||||
entMark(e, 0);
|
entMark(e, 0);
|
||||||
entCurM = e.m;
|
entCurM = e.m;
|
||||||
let ix = e.ix; const iy = e.iy;
|
let ix = e.ix; const iy = e.iy;
|
||||||
@@ -264,6 +264,7 @@ function entTick() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!(tick & 1)) transportBar(oldX, oldY, e.ix !== oldX || e.iy !== oldY);
|
||||||
entMark(e, 1);
|
entMark(e, 1);
|
||||||
}
|
}
|
||||||
entCurM = 0;
|
entCurM = 0;
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ function machinesTick() {
|
|||||||
const m = mat[i];
|
const m = mat[i];
|
||||||
if (m === EMPTY || PH[m] !== PH_POWDER) { if (m === EMPTY || PH[m] !== PH_SOLID) screwIntake(i, p, dx, dy); continue; }
|
if (m === EMPTY || PH[m] !== PH_POWDER) { if (m === EMPTY || PH[m] !== PH_SOLID) screwIntake(i, p, dx, dy); continue; }
|
||||||
if (stamp[i] === tickStamp) continue;
|
if (stamp[i] === tickStamp) continue;
|
||||||
|
const carrier = (Math.floor(i / W) >> 2) * BW + ((i % W) >> 2);
|
||||||
|
transportNote(carrier);
|
||||||
if (temp[i] > 1000 && rnd() < 0.02) {
|
if (temp[i] > 1000 && rnd() < 0.02) {
|
||||||
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
|
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
|
||||||
if (!ledger.screwMelt) { ledger.screwMelt = true; emit({ t: 'screw_melt' }); }
|
if (!ledger.screwMelt) { ledger.screwMelt = true; emit({ t: 'screw_melt' }); }
|
||||||
@@ -113,7 +115,7 @@ function machinesTick() {
|
|||||||
if (s <= 0 || rnd() > s) continue;
|
if (s <= 0 || rnd() > s) continue;
|
||||||
const x = i % W, y = (i / W) | 0;
|
const x = i % W, y = (i / W) | 0;
|
||||||
// a sideways screw carries its load along the bottom: grains settle into empty lanes below
|
// a sideways screw carries its load along the bottom: grains settle into empty lanes below
|
||||||
if (flat && y + 1 < H && part[i + W] === p && roomFor(i + W) && !occ[i + W]) { move(i, i + W, x, y, x, y + 1); continue; }
|
if (flat && y + 1 < H && part[i + W] === p && roomFor(i + W) && !occ[i + W]) { move(i, i + W, x, y, x, y + 1); transportNote(carrier, true); continue; }
|
||||||
if (!flat && !dx) {
|
if (!flat && !dx) {
|
||||||
let own = 0, shifted = false;
|
let own = 0, shifted = false;
|
||||||
const end = dy < 0 ? (y & ~3) : (y | 3);
|
const end = dy < 0 ? (y & ~3) : (y | 3);
|
||||||
@@ -126,7 +128,7 @@ function machinesTick() {
|
|||||||
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + xx]] === PH_POWDER) load++;
|
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + xx]] === PH_POWDER) load++;
|
||||||
// Include this grain: a strict gap prevents a sideways step being undone next beat.
|
// Include this grain: a strict gap prevents a sideways step being undone next beat.
|
||||||
if (own <= load + 1) continue;
|
if (own <= load + 1) continue;
|
||||||
move(i, j, x, y, xx, y); stats.moved++; shifted = true; break;
|
move(i, j, x, y, xx, y); stats.moved++; transportNote(carrier, true); shifted = true; break;
|
||||||
}
|
}
|
||||||
if (shifted) continue;
|
if (shifted) continue;
|
||||||
}
|
}
|
||||||
@@ -145,9 +147,9 @@ function machinesTick() {
|
|||||||
if (!screw && !pj && t === EMPTY) {
|
if (!screw && !pj && t === EMPTY) {
|
||||||
// out of the mouth: the screw spits it clear, so the next grain is never blocked by the last
|
// out of the mouth: the screw spits it clear, so the next grain is never blocked by the last
|
||||||
launchGrain(i, nx, ny, dy && !dx ? (rnd() - 0.5) * (dy < 0 ? 0.9 : 0.1) : dx * (0.42 + rnd() * 0.16), dy ? dy * (0.5 + rnd() * 0.2) : rnd() * 0.08);
|
launchGrain(i, nx, ny, dy && !dx ? (rnd() - 0.5) * (dy < 0 ? 0.9 : 0.1) : dx * (0.42 + rnd() * 0.16), dy ? dy * (0.5 + rnd() * 0.2) : rnd() * 0.08);
|
||||||
stats.moved++; if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j); continue;
|
stats.moved++; transportNote(carrier, true); if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j); continue;
|
||||||
}
|
}
|
||||||
move(i, j, x, y, nx, ny); stats.moved++;
|
move(i, j, x, y, nx, ny); stats.moved++; transportNote(carrier, true);
|
||||||
if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j);
|
if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j);
|
||||||
if (screw && pj !== p) {
|
if (screw && pj !== p) {
|
||||||
// turning a corner: keep going across the new screw's width so every lane fills
|
// turning a corner: keep going across the new screw's width so every lane fills
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ function slopeFaceAt(x, y, g) {
|
|||||||
function pushRow(p, dx, dy) {
|
function pushRow(p, dx, dy) {
|
||||||
const row = [];
|
const row = [];
|
||||||
let q = p;
|
let q = p;
|
||||||
for (let k = 0; k < CARRY_DEPTH; k++) {
|
// A pushed chain can cross pockets; its length is independent of the flat carrying depth.
|
||||||
|
for (let k = 0; k < 96; k++) {
|
||||||
if (PH[mat[q]] !== PH_POWDER || occ[q] || part[q] || (k && cleatMask[q])) break; // a machine moves its own contents
|
if (PH[mat[q]] !== PH_POWDER || occ[q] || part[q] || (k && cleatMask[q])) break; // a machine moves its own contents
|
||||||
row.push(q);
|
row.push(q);
|
||||||
const x = q % W + dx, y = ((q / W) | 0) + dy;
|
const x = q % W + dx, y = ((q / W) | 0) + dy;
|
||||||
@@ -162,8 +163,11 @@ function slopeCleatsTick(advance = true) {
|
|||||||
ribLog.length = 0;
|
ribLog.length = 0;
|
||||||
for (const list of [beltR, beltL]) for (const i of list) {
|
for (const list of [beltR, beltL]) for (const i of list) {
|
||||||
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
const x = i % W, y = (i / W) | 0, b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
||||||
if ((x & 3) === 0 && isBelt(c) && isSlopeBelt(c) && beltFaceY(c, x >> 2, y >> 2, x) === y && netSat(i) > 0 && !stalled.has(slopeRunKey(b, memo)))
|
if ((x & 3) === 0 && isBelt(c) && isSlopeBelt(c) && beltFaceY(c, x >> 2, y >> 2, x) === y && netSat(i) > 0) {
|
||||||
slopePhase[b] = (slopePhase[b] + 1) % CLEAT_SPACING;
|
const jam = stalled.has(slopeRunKey(b, memo));
|
||||||
|
if (slopeUphill(c, blockV[b])) transportCleat(b, jam);
|
||||||
|
if (!jam) slopePhase[b] = (slopePhase[b] + 1) % CLEAT_SPACING;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rebuildCleatMask();
|
rebuildCleatMask();
|
||||||
@@ -216,10 +220,12 @@ function screenNudge(top, d, pass) {
|
|||||||
if (tick % SCREEN_NUDGE_TICKS) return;
|
if (tick % SCREEN_NUDGE_TICKS) return;
|
||||||
const i = top - W, m = mat[i];
|
const i = top - W, m = mat[i];
|
||||||
if (i < 0 || PH[m] !== PH_POWDER || stamp[i] === tickStamp || occ[i]) return;
|
if (i < 0 || PH[m] !== PH_POWDER || stamp[i] === tickStamp || occ[i]) return;
|
||||||
if (pass && pass[m] && enter(m, PH_POWDER, top, 0, 1)) return;
|
const carrier = ((top / W) >> 2) * BW + ((top % W) >> 2);
|
||||||
|
transportNote(carrier);
|
||||||
|
if (pass && pass[m] && enter(m, PH_POWDER, top, 0, 1)) { transportNote(carrier, true); return; }
|
||||||
const j = i + d + W;
|
const j = i + d + W;
|
||||||
if (i % W + d < 0 || i % W + d >= W || j >= N) return;
|
if (i % W + d < 0 || i % W + d >= W || j >= N) return;
|
||||||
if (enter(m, PH_POWDER, j, d, 1)) { move(i, j, i % W, (i / W) | 0, j % W, (j / W) | 0); if (flowOn) flowCross(((top / W) >> 2) * BW + ((top % W) >> 2), i, j); }
|
if (enter(m, PH_POWDER, j, d, 1)) { move(i, j, i % W, (i / W) | 0, j % W, (j / W) | 0); transportNote(carrier, true); if (flowOn) flowCross(carrier, i, j); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only loose load can react to a barrier appearing or disappearing.
|
// Only loose load can react to a barrier appearing or disappearing.
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ function simTick() {
|
|||||||
if (loadWakeOrder) loadWakeTick();
|
if (loadWakeOrder) loadWakeTick();
|
||||||
if (tick % 20 === 1) updateDirtyRoof();
|
if (tick % 20 === 1) updateDirtyRoof();
|
||||||
weatherTick();
|
weatherTick();
|
||||||
|
if (!(tick & 1)) transportBegin();
|
||||||
machinesTick();
|
machinesTick();
|
||||||
pipesTick();
|
pipesTick();
|
||||||
pulseTick();
|
pulseTick();
|
||||||
entTick();
|
entTick();
|
||||||
|
if (!(tick & 1)) transportEnd();
|
||||||
if (flyers.length) flyTick();
|
if (flyers.length) flyTick();
|
||||||
if (tick % 8 === 0) wakeBotPowder();
|
if (tick % 8 === 0) wakeBotPowder();
|
||||||
simMove();
|
simMove();
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// Local backpressure: only loaded transport that cannot advance warns after half a second.
|
||||||
|
const STALL_TICKS = 30;
|
||||||
|
const transportState = new Map();
|
||||||
|
function transportNote(b, moved = false) {
|
||||||
|
if (b < 0 || b >= NB) return;
|
||||||
|
let s = transportState.get(b);
|
||||||
|
if (!s) { s = { age: 0, clock: tick, loaded: false, moved: false, jam: false, code: blockP[b], v: blockV[b] }; transportState.set(b, s); }
|
||||||
|
s.loaded = true; s.moved ||= moved;
|
||||||
|
}
|
||||||
|
function transportStalled(b) { return (transportState.get(b)?.age || 0) >= STALL_TICKS; }
|
||||||
|
function transportClock(b) { return transportState.get(b)?.clock ?? tick; }
|
||||||
|
function transportBegin() {
|
||||||
|
for (const [b, s] of transportState) {
|
||||||
|
if (blockP[b] !== s.code || blockV[b] !== s.v || !pieceEnds(s.code, s.v).length) { transportState.delete(b); continue; }
|
||||||
|
if (isFilter(s.code) && isSlopeBelt(s.code) && tick % SCREEN_NUDGE_TICKS) continue;
|
||||||
|
s.loaded = s.moved = s.jam = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function transportEnd() {
|
||||||
|
for (const s of transportState.values()) {
|
||||||
|
const cadence = isFilter(s.code) && isSlopeBelt(s.code) ? SCREEN_NUDGE_TICKS : 2;
|
||||||
|
if (tick % cadence) continue;
|
||||||
|
s.age = s.jam || (s.loaded && !s.moved) ? Math.min(STALL_TICKS, s.age + cadence) : 0;
|
||||||
|
if (s.age < STALL_TICKS) s.clock += cadence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A whole cleated run rolls back together; friction or a bar must not hide that failure.
|
||||||
|
function transportCleat(b, jam) {
|
||||||
|
transportNote(b, !jam);
|
||||||
|
transportState.get(b).jam = jam;
|
||||||
|
}
|
||||||
|
function transportBar(x, y, moved) {
|
||||||
|
for (let yy = y; yy <= y + BAR_H && yy < H; yy++) for (let xx = x; xx < x + BAR_W && xx < W; xx++) {
|
||||||
|
if (xx < 0 || yy < 0) continue;
|
||||||
|
const b = (yy >> 2) * BW + (xx >> 2), c = blockP[b];
|
||||||
|
if (c === BP_SCREW || isBelt(c) || isFilter(c)) transportNote(b, moved);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ function sharedArray(Type, length) {
|
|||||||
}
|
}
|
||||||
function alloc(w, h) {
|
function alloc(w, h) {
|
||||||
resetDevConsole();
|
resetDevConsole();
|
||||||
|
transportState.clear();
|
||||||
botTrailHead = botTrailCount = 0; botEscape = false;
|
botTrailHead = botTrailCount = 0; botEscape = false;
|
||||||
W = w; H = h; N = w * h;
|
W = w; H = h; N = w * h;
|
||||||
fogAlloc();
|
fogAlloc();
|
||||||
|
|||||||
@@ -142,6 +142,11 @@ function hoverInfo(ax, ay) {
|
|||||||
warn = 'near heat limit'; t2.push(`${Math.round(screwLoadHeat(jb))}°C load · melts above 1000°C`);
|
warn = 'near heat limit'; t2.push(`${Math.round(screwLoadHeat(jb))}°C load · melts above 1000°C`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const stalledBlock = slopeSealOwner(i) >= 0 ? slopeSealOwner(i) : jb;
|
||||||
|
if (transportStalled(stalledBlock)) {
|
||||||
|
warn = 'stalled';
|
||||||
|
t2.push('Stalled: load cannot move. Clear the outlet or the top of the heap.');
|
||||||
|
}
|
||||||
if (GAME.infoOverlay && !id && !bar && pieceEnds(blockP[jb], blockV[jb]).length) t2.push(`moving ${(flowRate[jb] || 0).toFixed(1)} grains/s`);
|
if (GAME.infoOverlay && !id && !bar && pieceEnds(blockP[jb], blockV[jb]).length) t2.push(`moving ${(flowRate[jb] || 0).toFixed(1)} grains/s`);
|
||||||
for (let end = 0; end < 2 && !id && !bar; end++) {
|
for (let end = 0; end < 2 && !id && !bar; end++) {
|
||||||
const miss = nearMiss(jb % BW, (jb / BW) | 0, end);
|
const miss = nearMiss(jb % BW, (jb / BW) | 0, end);
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ function frame(now) {
|
|||||||
if (GAME.infoOverlay) { drawFlowArrows(); drawFlowRates(); }
|
if (GAME.infoOverlay) { drawFlowArrows(); drawFlowRates(); }
|
||||||
drawJoinMarks();
|
drawJoinMarks();
|
||||||
drawScrewHeat();
|
drawScrewHeat();
|
||||||
|
drawTransportStalls();
|
||||||
drawPing();
|
drawPing();
|
||||||
drawHoveredVein();
|
drawHoveredVein();
|
||||||
drawPingRecharge();
|
drawPingRecharge();
|
||||||
|
|||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
// Closed belt/screw loop, with a low overhead return and a deep mixed heap.
|
||||||
|
for(let bx=12;bx<28;bx++)placeBlock(BP_BELT,1,bx,32,true);
|
||||||
|
for(let by=28;by<=32;by++)placeBlock(BP_SCREW,0,28,by,true);
|
||||||
|
for(let bx=12;bx<=28;bx++)placeBlock(BP_SCREW,3,bx,27,true);
|
||||||
|
for(let by=28;by<32;by++)placeBlock(BP_SCREW,2,12,by,true);
|
||||||
|
for(let y=112;y<128;y++)for(let x=56;x<108;x++)setCell(y*W+x,y>=124?SAND:CUORE,20,0);
|
||||||
+5
-7
@@ -56,11 +56,11 @@ for (const horizontal of [false, true]) {
|
|||||||
check(horizontal ? 'a right screw takes grains from above into every row' : 'an up screw takes grains from the side into every column', filled.join() === '3,2,1,0', filled);
|
check(horizontal ? 'a right screw takes grains from above into every row' : 'an up screw takes grains from the side into every column', filled.join() === '3,2,1,0', filled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A finite heap rides into the side of a six-block lift; remove each mouth flyer once.
|
// A finite four-grain bed feeds a six-block lift; compare with the same open belt.
|
||||||
const sideSeed = run('RS');
|
const sideSeed = run('RS');
|
||||||
fresh();
|
fresh();
|
||||||
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true); for (let by = 19; by <= 24; by++) placeBlock(BP_SCREW, 0, 32, by, true);');
|
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true); for (let by = 19; by <= 24; by++) placeBlock(BP_SCREW, 0, 32, by, true);');
|
||||||
rect(run, 64, 40, 119, 99, run('SAND'));
|
rect(run, 64, 96, 119, 99, run('SAND'));
|
||||||
const sideLift = run(`(() => {
|
const sideLift = run(`(() => {
|
||||||
let out = 0; const lanes = [0, 0, 0, 0];
|
let out = 0; const lanes = [0, 0, 0, 0];
|
||||||
const grains = () => mat.reduce((n, m) => n + (m === SAND), 0) + flyers.filter(f => f.m === SAND).length;
|
const grains = () => mat.reduce((n, m) => n + (m === SAND), 0) + flyers.filter(f => f.m === SAND).length;
|
||||||
@@ -75,15 +75,12 @@ const sideLift = run(`(() => {
|
|||||||
console.log('Side-fed lift, 3000 ticks: ' + JSON.stringify(sideLift));
|
console.log('Side-fed lift, 3000 ticks: ' + JSON.stringify(sideLift));
|
||||||
check('a belt feeding a lift from the side fills all four lanes', sideLift.lanes.every(n => n > 0), sideLift.lanes);
|
check('a belt feeding a lift from the side fills all four lanes', sideLift.lanes.every(n => n > 0), sideLift.lanes);
|
||||||
check('the side-fed lift conserves its sand, including mouth flyers', sideLift.before === sideLift.after, `${sideLift.before} -> ${sideLift.after}`);
|
check('the side-fed lift conserves its sand, including mouth flyers', sideLift.before === sideLift.after, `${sideLift.before} -> ${sideLift.after}`);
|
||||||
// Before side spreading: 1459 grains left this lift in 3000 ticks (one lane, nearly full).
|
|
||||||
// The same belt and heap deliver 2243 into an open drop, the most the lift can be given.
|
|
||||||
check('a side-fed lift carries at least 90% of what its belt brings (it was 65%)', sideLift.out >= Math.floor(0.9 * 2243), sideLift.out);
|
|
||||||
|
|
||||||
// Same belt and finite heap, with a non-blocking sink at the screw side.
|
// Same belt and four-grain feed, with a non-blocking sink at the screw side.
|
||||||
run(`RS = ${sideSeed}`);
|
run(`RS = ${sideSeed}`);
|
||||||
fresh();
|
fresh();
|
||||||
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
|
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
|
||||||
rect(run, 64, 40, 119, 99, run('SAND'));
|
rect(run, 64, 96, 119, 99, run('SAND'));
|
||||||
const beltDelivery = run(`(() => {
|
const beltDelivery = run(`(() => {
|
||||||
let out = 0;
|
let out = 0;
|
||||||
for (let k = 0; k < 3000; k++) {
|
for (let k = 0; k < 3000; k++) {
|
||||||
@@ -97,6 +94,7 @@ const beltDelivery = run(`(() => {
|
|||||||
return out;
|
return out;
|
||||||
})()`);
|
})()`);
|
||||||
console.log('Belt delivery to a non-blocking sink, 3000 ticks: ' + beltDelivery);
|
console.log('Belt delivery to a non-blocking sink, 3000 ticks: ' + beltDelivery);
|
||||||
|
check('a side-fed lift carries at least 90% of what its four-grain belt brings', sideLift.out >= Math.floor(0.9 * beltDelivery), `${sideLift.out} / ${beltDelivery}`);
|
||||||
|
|
||||||
// --- throughput
|
// --- throughput
|
||||||
fresh();
|
fresh();
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// t45: finite carrying bed, local sustained jams, recovery, filters, screws and bars.
|
||||||
|
const fs=require('fs');
|
||||||
|
const {load,flatWorld,suite,ticks}=require('./harness');
|
||||||
|
const {run}=load(), {check,done}=suite();
|
||||||
|
function fresh(){flatWorld(run,256,224,208);run('RS=7;AMB.fill(20);temp.fill(20);weather.next=1e15;nextPowerBuild=0;for(const k of Object.keys(stats))stats[k]=0;');}
|
||||||
|
fresh();
|
||||||
|
run('placeBlock(BP_BELT,1,12,32,true);for(let l=1;l<=8;l++)setCell((128-l)*W+49,SAND,20,0);carryColumn(127*W+49,1,null);');
|
||||||
|
check('exactly four grains ride; the upper heap stays loose',run('Array.from({length:8},(_,k)=>mat[(127-k)*W+(k<4?50:49)]===SAND).every(Boolean)'));
|
||||||
|
check('stall tracking exists',run('typeof transportStalled==="function"'));
|
||||||
|
if(run('typeof transportStalled!=="function"')) {done();process.exit();}
|
||||||
|
for(const code of ['BP_BELT','BP_FILTER','BP_SCREW']) {
|
||||||
|
fresh();
|
||||||
|
run(`for(let bx=12;bx<20;bx++)placeBlock(${code},${code==='BP_FILTER'?128:1},bx,32,true,{m:[],inv:0});
|
||||||
|
for(let y=120;y<132;y++)for(let x=48;x<80;x++)if(mat[y*W+x]===EMPTY)setCell(y*W+x,SAND,20,0);
|
||||||
|
for(let y=110;y<136;y++)setCell(y*W+80,STONE,20,0);
|
||||||
|
placeBlock(BP_BELT,1,30,32,true);
|
||||||
|
for(let bx=19;bx<=30;bx++)placeBlock(BP_SHAFT,0,bx,34,true);
|
||||||
|
placeBlock(BP_SHAFT,0,19,33,true);placeBlock(BP_SHAFT,0,30,33,true);`);
|
||||||
|
ticks(run,10);
|
||||||
|
check(`${code}: a brief wait is not a stall`,run('!transportStalled(32*BW+19)'));
|
||||||
|
ticks(run,240);
|
||||||
|
check(`${code}: loaded blocked tail and upstream run warn`,run('transportStalled(32*BW+19)&&transportStalled(32*BW+15)'));
|
||||||
|
check(`${code}: an unrelated empty belt stays clear (same power net for belts/screws)`,run('!transportStalled(32*BW+30)') && (code==='BP_FILTER' || run('netId[128*W+60]===netId[128*W+120]')));
|
||||||
|
const clock=run('transportClock(32*BW+19)');ticks(run,20);
|
||||||
|
check(`${code}: animation clock freezes`,run('transportClock(32*BW+19)')===clock);
|
||||||
|
run('for(let y=110;y<136;y++)setCell(y*W+80,EMPTY,20,0);');ticks(run,30);
|
||||||
|
check(`${code}: clearing the blockage resumes motion`,run('!transportStalled(32*BW+19) && transportClock(32*BW+19)')>clock);
|
||||||
|
}
|
||||||
|
fresh();run('for(let bx=12;bx<40;bx++)placeBlock(BP_BELT,1,bx,32,true);spawnBar(BARCU,48,126,0,0);');
|
||||||
|
check('a steadily moving bar never reports a stall between whole-grain steps',run(`(()=>{for(let k=0;k<120;k++){simTick();if([...transportState.keys()].some(transportStalled))return false;}return ents[0].ix>90;})()`));
|
||||||
|
fresh();run('placeBlock(BP_BELT,1,12,32,true);placeBlock(BP_WALL,RSTONE,13,31,true);spawnBar(BARCU,48,126,0,0);');ticks(run,100);
|
||||||
|
check('a blocked bar also stalls its carrying belt',run('transportStalled(32*BW+12)'));
|
||||||
|
fresh();
|
||||||
|
run(`for(let k=0;k<8;k++)placeBlock(BP_BELT_UP,1,16+k,40-k,true);
|
||||||
|
for(let y=110;y<132;y++)for(let x=96;x<100;x++)setCell(y*W+x,STONE,20,0);
|
||||||
|
for(let y=110;y<166;y++)for(let x=56;x<96;x++)if(!part[y*W+x]&&mat[y*W+x]===EMPTY)setCell(y*W+x,SAND,20,0);`);
|
||||||
|
ticks(run,100);
|
||||||
|
check('a blocked cleat stops its whole slope run',run('Array.from({length:8},(_,k)=>transportStalled((40-k)*BW+16+k)).every(Boolean)'));
|
||||||
|
const phase=run('slopePhase[40*BW+16]'), slopeClock=run('transportClock(40*BW+16)');ticks(run,20);
|
||||||
|
check('stalled cleats and stripe clocks stay still together',run('slopePhase[40*BW+16]')===phase && run('transportClock(40*BW+16)')===slopeClock);
|
||||||
|
fresh();
|
||||||
|
run(`for(let k=0;k<8;k++)placeBlock(BP_FILTER_DOWN,128,16+k,24+k,true,{m:[],inv:0});
|
||||||
|
for(let y=100;y<140;y++)setCell(y*W+96,STONE,20,0);
|
||||||
|
for(let x=64;x<96;x++){const by=24+(x>>2)-16,y=beltFaceY(BP_FILTER_DOWN,x>>2,by,x);for(let l=1;l<=4;l++)setCell((y-l)*W+x,SAND,20,0);}`);
|
||||||
|
ticks(run,300);
|
||||||
|
check('a jammed downhill screen warns on its slower nudge cadence',run('transportStalled(31*BW+23)'));
|
||||||
|
fresh();run(fs.readFileSync(__dirname+'/fixtures/t45-loop.js','utf8'));ticks(run,1800);
|
||||||
|
console.log('Overloaded loop: '+run('JSON.stringify({moved:stats.moved,grains:mat.reduce((n,m)=>n+(m===SAND||m===CUORE),0),flyers:flyers.length,stalled:[...transportState].filter(([b,s])=>s.age>=STALL_TICKS).length})'));
|
||||||
|
check('overloaded loop conserves its 832 grains',run('mat.reduce((n,m)=>n+(m===SAND||m===CUORE),0)+flyers.length')===832);
|
||||||
|
done();
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""t45: real transport warnings, hover, frozen animation and recovery in both looks."""
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / 'tools'))
|
||||||
|
from use_venv import ensure_playwright; ensure_playwright()
|
||||||
|
from browser import launch, consume_gpu_option
|
||||||
|
consume_gpu_option()
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
with launch(viewport={'width': 1280, 'height': 760}, page_path=sys.argv[1] if len(sys.argv)>1 else None) as (_, page, url):
|
||||||
|
errors=[]
|
||||||
|
page.on('pageerror', lambda e: errors.append(str(e)))
|
||||||
|
page.goto(url)
|
||||||
|
page.evaluate("""() => {
|
||||||
|
const enter=enterGame;enterGame=()=>{enter();GAME.paused=true;};
|
||||||
|
SET.look='pixel';SET.workerCap=0;SET.autosave=0;startNewWorld(7,768,600);
|
||||||
|
}""")
|
||||||
|
page.wait_for_function('GAME.running', timeout=60000)
|
||||||
|
page.evaluate("""() => {
|
||||||
|
mat.fill(EMPTY);part.fill(0);blockP.fill(0);blockV.fill(0);isPow.fill(0);
|
||||||
|
flags.fill(0);temp.fill(20);AMB.fill(20);bgm.fill(0);fog.fill(255);
|
||||||
|
machines=[null];molds=[];ents=[];flyers=[];treeBases=[];saplings=[];
|
||||||
|
pcells.clear();beltL=[];beltR=[];screwLists=Array.from({length:IS_SCREW.length},()=>[]);
|
||||||
|
weather.next=1e15;weather.shower=null;surfY.fill(300);updateRoof();
|
||||||
|
bot.x=320;bot.y=210;money=500;ledger={};nextPowerBuild=0;
|
||||||
|
for(const [code,v,by] of [[BP_BELT,1,46],[BP_FILTER,128,52],[BP_SCREW,1,58]]) {
|
||||||
|
for(let bx=68;bx<86;bx++)placeBlock(code,v,bx,by,true,{m:[],inv:0});
|
||||||
|
// The return screw hangs just above the loaded belt.
|
||||||
|
if(code===BP_BELT)for(let bx=70;bx<86;bx++)placeBlock(BP_SCREW,3,bx,44,true);
|
||||||
|
for(let y=by*4-4;y<by*4+(code===BP_SCREW?4:0);y++)
|
||||||
|
for(let x=272;x<344;x++)setCell(y*W+x,SAND,20,0);
|
||||||
|
for(let y=by*4-8;y<by*4+8;y++)setCell(y*W+344,STONE,20,0);
|
||||||
|
}
|
||||||
|
for(let k=0;k<180;k++)simTick();
|
||||||
|
$('hud').hidden=true;
|
||||||
|
SET.fog=false;SET.zoom=9;SET.lighting='off';SET.glow='off';SET.minimap=false;
|
||||||
|
SET.cameraLead=false;GAME.infoOverlay=false;GAME.hidePreviews=true;
|
||||||
|
document.querySelectorAll('.card-pop,.toast').forEach(e=>e.remove());R.floaters=[];
|
||||||
|
resizeRender();snapCamera();renderDirty.fill(1);GPU.world=null;SM.surf=-1;
|
||||||
|
}""")
|
||||||
|
assert page.evaluate("hoverInfo(330,185).warn==='stalled'"), 'jam has no hover warning'
|
||||||
|
assert page.evaluate("hoverInfo(330,185).t2.some(t=>t.includes('Clear the outlet or the top of the heap'))")
|
||||||
|
assert page.evaluate("[46,52,58].every(by=>transportStalled(by*BW+82))")
|
||||||
|
for look in ['pixel','smooth']:
|
||||||
|
page.evaluate(f"SET.look='{look}'")
|
||||||
|
page.wait_for_function(f"R.smoothOn === {'true' if look=='smooth' else 'false'}")
|
||||||
|
page.wait_for_timeout(150)
|
||||||
|
assert page.evaluate("""() => {
|
||||||
|
const p=worldToScreenPx(330,185),ctx=R.ctx;
|
||||||
|
ctx.clearRect(0,0,R.canvas.width,R.canvas.height);drawTransportStalls();
|
||||||
|
const shown=ctx.getImageData(p.x,p.y,1,1).data[3];
|
||||||
|
SET.fog=true;fog.fill(0);ctx.clearRect(0,0,R.canvas.width,R.canvas.height);drawTransportStalls();
|
||||||
|
const hidden=ctx.getImageData(p.x,p.y,1,1).data[3];SET.fog=false;fog.fill(255);
|
||||||
|
return shown>0 && hidden===0;
|
||||||
|
}""")
|
||||||
|
# Compare actual built pixels while simulation time advances under the jam.
|
||||||
|
page.wait_for_timeout(60)
|
||||||
|
clips=page.evaluate("""() => [184,208,233].map(y=>{const p=worldToScreenPx(329,y);return {x:p.x,y:p.y,width:18,height:9};})""")
|
||||||
|
before=[page.screenshot(clip=c) for c in clips]
|
||||||
|
page.evaluate('for(let k=0;k<10;k++)simTick()')
|
||||||
|
page.wait_for_timeout(60)
|
||||||
|
assert [page.screenshot(clip=c) for c in clips]==before, f'{look}: stalled transport still animates'
|
||||||
|
path=ROOT / ('shots/t45-stalled.png' if look=='pixel' else 'shots/t45-stalled-smooth.png')
|
||||||
|
page.screenshot(path=str(path))
|
||||||
|
print(f'PASS {look}: marker with overlay off, fog, hover, frozen rendered belt and screenshot')
|
||||||
|
page.evaluate("""() => {
|
||||||
|
for(const by of [46,52,58])for(let y=by*4-8;y<by*4+8;y++)for(let x=344;x<360;x++)setCell(y*W+x,EMPTY,20,0);
|
||||||
|
for(let k=0;k<60;k++)simTick();
|
||||||
|
}""")
|
||||||
|
assert page.evaluate('!transportStalled(46*BW+82)'), 'belt did not recover'
|
||||||
|
assert not errors, errors
|
||||||
|
print('PASS cleared blockage resumes transport; no page errors')
|
||||||
+7
-1
@@ -26,6 +26,11 @@ function setup() {
|
|||||||
const m=materials[(random()*materials.length)|0];
|
const m=materials[(random()*materials.length)|0];
|
||||||
setCell(y*W+x,m,m===MAGMA?1450:m===MCU?1200:m===STEAM?110:20,0);
|
setCell(y*W+x,m,m===MAGMA?1450:m===MCU?1200:m===STEAM?110:20,0);
|
||||||
}
|
}
|
||||||
|
for(let y=208;y<230;y++)for(let x=164;x<220;x++)setCell(y*W+x,EMPTY,20,0);
|
||||||
|
for(let bx=42;bx<53;bx++)placeBlock(BP_BELT,1,bx,55,true);
|
||||||
|
for(let y=210;y<222;y++)for(let x=212;x<216;x++)setCell(y*W+x,STONE,20,0);
|
||||||
|
for(let y=216;y<220;y++)for(let x=168;x<212;x++)setCell(y*W+x,SAND,20,0);
|
||||||
|
for(let k=0;k<6;k++)placeBlock(BP_BELT_UP,1,70+k,48-k,true);
|
||||||
// A cohesive vertical face spans chunk rows and must collapse identically in every pool.
|
// A cohesive vertical face spans chunk rows and must collapse identically in every pool.
|
||||||
for(let y=50;y<90;y++)for(let x=260;x<280;x++)
|
for(let y=50;y<90;y++)for(let x=260;x<280;x++)
|
||||||
setCell(y*W+x,(x+y)%5===0?LCLAY:RSTONE,20,0);
|
setCell(y*W+x,(x+y)%5===0?LCLAY:RSTONE,20,0);
|
||||||
@@ -42,7 +47,7 @@ function hash() {
|
|||||||
const h = crypto.createHash('sha256');
|
const h = crypto.createHash('sha256');
|
||||||
for (const a of run('[mat,temp,life,flags,stamp,part,mach,digp,bgm,isPow,netId,awake,hot,hotLvl,powderArea,roofDirty,seen,renderDirty]'))
|
for (const a of run('[mat,temp,life,flags,stamp,part,mach,digp,bgm,isPow,netId,awake,hot,hotLvl,powderArea,roofDirty,seen,renderDirty]'))
|
||||||
h.update(Buffer.from(a.buffer,a.byteOffset,a.byteLength));
|
h.update(Buffer.from(a.buffer,a.byteOffset,a.byteLength));
|
||||||
h.update(run('JSON.stringify({RS,stats,EV,flyers,brokenMachines,pcells:[...pcells],powerDirty,weather,machines})'));
|
h.update(run('JSON.stringify({RS,stats,EV,flyers,brokenMachines,pcells:[...pcells],powerDirty,weather,machines,transport:[...transportState],slopePhase:Array.from(slopePhase)})'));
|
||||||
return h.digest('hex');
|
return h.digest('hex');
|
||||||
}
|
}
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -51,6 +56,7 @@ function hash() {
|
|||||||
setup(); const stop = n ? await enableThreads(run,n) : null;
|
setup(); const stop = n ? await enableThreads(run,n) : null;
|
||||||
try {
|
try {
|
||||||
run('for(let k=0;k<320;k++)simTick();');
|
run('for(let k=0;k<320;k++)simTick();');
|
||||||
|
assert(run('transportStalled(55*BW+48)'), 'worker fixture must include a loaded stalled belt');
|
||||||
const actual=hash();
|
const actual=hash();
|
||||||
if(!expected)expected=actual;
|
if(!expected)expected=actual;
|
||||||
assert.strictEqual(actual,expected,`world and effects differ with ${n} workers`);
|
assert.strictEqual(actual,expected,`world and effects differ with ${n} workers`);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// t45: sustained normal (one layer) and heavy (16 layers) flat-belt feeds, open sink.
|
||||||
|
const {load,flatWorld}=require('../../test/harness');
|
||||||
|
const {run}=load();
|
||||||
|
for(const depth of [1,16]) {
|
||||||
|
flatWorld(run,256,224,208);
|
||||||
|
run(`RS=7;AMB.fill(20);temp.fill(20);hot.fill(0);weather.next=1e15;
|
||||||
|
for(let bx=10;bx<32;bx++)placeBlock(BP_BELT,1,bx,24,true);`);
|
||||||
|
const start=performance.now();
|
||||||
|
const rate=run(`(()=>{let out=0;for(let k=0;k<1420;k++){
|
||||||
|
for(let l=1;l<=${depth};l++){const i=(96-l)*W+44;if(mat[i]===EMPTY)setCell(i,SAND,20,0);}
|
||||||
|
simTick();
|
||||||
|
for(let y=0;y<H;y++)for(let x=128;x<W;x++){const i=y*W+x;if(mat[i]===SAND){if(k>=220)out++;setCell(i,EMPTY,20,0);}}
|
||||||
|
for(let j=flyers.length-1;j>=0;j--)if(flyers[j].m===SAND&&flyers[j].x>=128){if(k>=220)out++;flyers.splice(j,1);}
|
||||||
|
}return out/20;})()`);
|
||||||
|
console.log(`${depth===1?'normal':'heavy'}: ${rate.toFixed(1)} grains/s; ${((performance.now()-start)/1420).toFixed(3)} ms/tick including feed/sink`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user