Diagonal screw tubes, laid by a 45° drag (t35)
- Tubes run in eight directions: a drag at about 45° lays a diagonal run, R cycles all eight. - Diagonal runs carry grains at the straight rate (120 grains/s), join straight tubes and belts, keep grains in at the corners, pass water, and carry bars level. - New part ids 11-14 (saved ids are never renumbered); tube checks use an IS_TUBE table. - New test/t_diag_tubes.js (85 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -373,6 +373,11 @@ above, including:
|
||||
would otherwise slip through where two pieces meet). Square walls placed diagonally still
|
||||
touch only at corners and leak there, like any square blocks.
|
||||
|
||||
- **Diagonal screw tubes (t35, 2026-10-25).** Tubes run in eight directions. A drag at about 45°
|
||||
lays a diagonal run pointing along the drag, and R cycles all eight for a single tube. Diagonal
|
||||
runs carry grains at the same rate as straight ones, join straight tubes and belts, and carry
|
||||
bars level, a block every eight ticks. They use new part ids (11-14) because part ids are saved.
|
||||
|
||||
## Ideas not built yet
|
||||
|
||||
- **Concrete**, as a stronger wall material from rubble.
|
||||
|
||||
+14
-12
@@ -201,9 +201,9 @@ Material definitions: ids, properties and metal families. ids, and drill/tank ti
|
||||
- functions: def, metalFamily
|
||||
- variables: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, FIRETO
|
||||
|
||||
### src/sim/material-tables.js (49 lines)
|
||||
### src/sim/material-tables.js (54 lines)
|
||||
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, 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 (158 lines)
|
||||
Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost.
|
||||
@@ -240,7 +240,7 @@ Heat: conduction, melting and freezing, reactions, fire and air, steel.
|
||||
- functions: curAir, airflowAt, alloyMix, airy, hasO2, condPair, heatPass, heatChunk, neighborDir, react, char, quench, burningNear, burn, steelCheck
|
||||
- variables: POROUS, SOIL, HOTLIQ, REACT_MIN, ALLOY
|
||||
|
||||
### src/sim/machines.js (148 lines)
|
||||
### src/sim/machines.js (157 lines)
|
||||
Machines: the power network, footprints, placing and removing machines.
|
||||
- functions: markPow, partLost, rebuildPower, netSat, evictCells, cellBuildable, machineFootprint, canPlaceMachine, addMachine, removeMachine
|
||||
- variables: nextPowerBuild
|
||||
@@ -260,13 +260,13 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
|
||||
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
|
||||
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
|
||||
|
||||
### src/sim/machine-tick.js (164 lines)
|
||||
### src/sim/machine-tick.js (168 lines)
|
||||
Per-tick machine work: steam engines, fans, belts, filters, screw tubes (machinesTick).
|
||||
- functions: engineIntake, fanBlow, machinesTick
|
||||
|
||||
### src/sim/conveyors.js (128 lines)
|
||||
### src/sim/conveyors.js (147 lines)
|
||||
How belts, filters and screw tubes move what is on or in them. What rests on a belt or filter rides along: the whole stack standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full.
|
||||
- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeSpread, tubeIntake
|
||||
- functions: carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, setFilter, filterRun, roomFor, tubeSpread, tubeIntake, diagonalTubeNext
|
||||
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
|
||||
|
||||
### src/sim/blocks.js (208 lines)
|
||||
@@ -278,9 +278,9 @@ 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
|
||||
- variables: DROSS, grains, barWord
|
||||
|
||||
### src/sim/items.js (232 lines)
|
||||
### src/sim/items.js (256 lines)
|
||||
Bars and other loose objects (ents), and the bag: adding, taking, costs and refunds.
|
||||
- functions: entSolidCell, entSupportCount, entFreeAt, entMark, spawnBar, removeEnt, entTubeAt, entTubeFree, entTubeMove, entTubeLane, entTubeEdge, entTubeEnter, entTubeTick, 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, entTick, entsInCircle, bagAdd, bagTake, giveOrDrop, refundAt, canAfford, payCost, fmtMoney, costText, needText
|
||||
- variables: BAR_W, BAR_H, entCurM, droppedLast
|
||||
|
||||
### src/sim/tools.js (52 lines)
|
||||
@@ -356,7 +356,7 @@ Drawing (src/render/): browser only. This file: the renderer state R, colour tab
|
||||
Propagated coloured light from glowing and burning cells.
|
||||
- functions: builtForLight, computeLight
|
||||
|
||||
### src/render/world.js (330 lines)
|
||||
### src/render/world.js (332 lines)
|
||||
Scale colour without changing the smooth-look class.
|
||||
- functions: fogColor, drawWorld
|
||||
- variables: TUBE_CLASS
|
||||
@@ -374,7 +374,7 @@ Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
|
||||
- functions: smCompile, smoothAvailable, smUpload, smoothDraw, smoothPace
|
||||
- variables: SM, smBytes
|
||||
|
||||
### src/render/smooth-shaders.js (337 lines)
|
||||
### src/render/smooth-shaders.js (342 lines)
|
||||
Smooth shaders: vertex and fragment programs for the rounded world look.
|
||||
- variables: SM_VS, SM_FS
|
||||
|
||||
@@ -448,7 +448,7 @@ Selection panels: current tool card and bag materials.
|
||||
- functions: renderSelCard, renderBag
|
||||
- variables: lastBagKey
|
||||
|
||||
### src/ui/info.js (188 lines)
|
||||
### src/ui/info.js (189 lines)
|
||||
Discovery cards, hover info (I) and the cursor label.
|
||||
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
|
||||
|
||||
@@ -457,7 +457,7 @@ Keyboard and mouse listeners.
|
||||
- functions: isTyping
|
||||
- variables: canvas
|
||||
|
||||
### src/ui/actions.js (239 lines)
|
||||
### src/ui/actions.js (240 lines)
|
||||
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
|
||||
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
|
||||
- variables: ovShown
|
||||
@@ -505,6 +505,7 @@ The frame loop and boot.
|
||||
- `test/t_basics.js`: Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load.
|
||||
- `test/t_build.js`: Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning.
|
||||
- `test/t_conveyors.js`: Screw tubes that never carry liquid (it flows through), tube and belt throughput, filters.
|
||||
- `test/t_diag_tubes.js`: Diagonal screws: sealed four-lane corner transfers, bars, joins and persistence.
|
||||
- `test/t_drill.js`: The resonance drill: pulses from the bot shake rock until it shatters. Widths trade area for speed, tuning shatters only one material and drains it out of the wall, built blocks are left alone, and the drill says why when nothing breaks.
|
||||
- `test/t_econ.js`: Economy: the dock (instant sale), orders, prices, the market dip, money for machines, alloys.
|
||||
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
||||
@@ -545,4 +546,5 @@ The frame loop and boot.
|
||||
- `tools/scenes/silo.js`: Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge.
|
||||
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
|
||||
- `tools/scenes/t33-ground.js`: Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt. Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket.
|
||||
- `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/t36-funnel.js`: A twelve-block funnel, one block open at the bottom, draining 400 grains of sand.
|
||||
|
||||
@@ -4,6 +4,17 @@ What agents have done, newest first. Add an entry for every piece of work: the d
|
||||
note or request it was for, what changed, the files touched, the tests run, and anything left
|
||||
open or not verified. Keep each entry short; the git history has the details.
|
||||
|
||||
## 2026-10-25: t35: diagonal screw tubes (Claude Code, Codex)
|
||||
|
||||
- Parts `P_TUR`..`P_TUL` (11-14), `TUBE_PARTS`, and an `IS_TUBE` table replacing every
|
||||
`P_TU..P_TR` range check (`src/sim/material-tables.js` and its users); tube variants 4-7;
|
||||
diagonal drags for tubes; grains in `src/sim/conveyors.js`/`cells.js`, bars in `items.js`;
|
||||
both looks, previews, hover, copy/paste, undo, blueprints, saves.
|
||||
- `test/t_diag_tubes.js` (85 checks): 120 grains/s straight, up and down over ten blocks; joins,
|
||||
no spills, water passes, bars both ways, controls, persistence. Existing suites unchanged and
|
||||
passing; seeds unchanged. `run_all.py --browser` ALL PASSED. Screenshots
|
||||
`shots/t35-diagonal-*.png`, scene `tools/scenes/t35-diagonal.js`.
|
||||
|
||||
## 2026-10-25: t36: sloped walls and diagonal wall lines (Claude Code, Codex)
|
||||
|
||||
- `BP_SLOPE_BL`..`BP_SLOPE_TR` (15-18) in `src/sim/pieces.js` (`isSlope`, `isWall`, `slopeCell`,
|
||||
|
||||
@@ -39,7 +39,7 @@ function drawFlowArrows() {
|
||||
const b = by * BW + bx, code = blockP[b];
|
||||
if (code !== BP_BELT && code !== BP_TUBE && code !== BP_PUMP && code !== BP_OUTLET) continue;
|
||||
const vv = blockV[b];
|
||||
const [dx, dy] = code === BP_BELT ? (vv ? [1, 0] : [-1, 0]) : code === BP_PUMP ? PUMP_V[vv & 3] : code === BP_OUTLET ? OUTLET_V[vv & 3] : [[0, -1], [1, 0], [0, 1], [-1, 0]][vv & 3];
|
||||
const [dx, dy] = code === BP_BELT ? (vv ? [1, 0] : [-1, 0]) : code === BP_PUMP ? PUMP_V[vv & 3] : code === BP_OUTLET ? OUTLET_V[vv & 3] : TUBE_DIRS[vv & 7];
|
||||
const c = worldToScreenPx(bx * BS + BS / 2, by * BS + BS / 2);
|
||||
ctx.fillStyle = 'rgba(14,12,11,0.55)'; ctx.beginPath(); ctx.arc(c.x, c.y, r * 1.35, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.strokeStyle = code === BP_TUBE ? '#ffc58a' : '#e7dccb'; ctx.lineWidth = Math.max(1.2, r * 0.32);
|
||||
|
||||
@@ -245,11 +245,16 @@ void main() {
|
||||
C = mix(C, bc, inside);
|
||||
}
|
||||
}
|
||||
else if (nc >= 9 && nc <= 12) {
|
||||
else if ((nc >= 9 && nc <= 12) || (nc >= 19 && nc <= 22)) {
|
||||
// screw tube: a copper pipe; what is inside shows through, and the screw's flights turn
|
||||
bool vert = nc == 9 || nc == 11;
|
||||
float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0;
|
||||
float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x;
|
||||
if (nc >= 19) {
|
||||
vec2 d = nc == 19 ? vec2(1.0, -1.0) : nc == 20 ? vec2(1.0, 1.0) : nc == 21 ? vec2(-1.0, 1.0) : vec2(-1.0, -1.0);
|
||||
a = fract(dot(w, vec2(-d.y, d.x)) * 0.25) * 4.0;
|
||||
along = dot(w, d); sp2 = 1.0;
|
||||
}
|
||||
float wall = 0.62;
|
||||
vec3 tc;
|
||||
if (a < wall || a > 4.0 - wall) {
|
||||
|
||||
+9
-7
@@ -3,7 +3,8 @@ function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 1
|
||||
// drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth look.
|
||||
// ---------- World pixels ----------
|
||||
// smooth-look class of a screw tube cell by direction: 9 up, 10 right, 11 down, 12 left
|
||||
const TUBE_CLASS = new Uint8Array(9); TUBE_CLASS[1] = 9; TUBE_CLASS[4] = 10; TUBE_CLASS[2] = 11; TUBE_CLASS[3] = 12;
|
||||
const TUBE_CLASS = new Uint8Array(15); TUBE_CLASS[1] = 9; TUBE_CLASS[4] = 10; TUBE_CLASS[2] = 11; TUBE_CLASS[3] = 12;
|
||||
for (let p = P_TUR; p <= P_TUL; p++) TUBE_CLASS[p] = 19 + p - P_TUR;
|
||||
function drawWorld() {
|
||||
const bw = R.bw, bh = R.bh, px = R.px;
|
||||
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
|
||||
@@ -155,13 +156,14 @@ function drawWorld() {
|
||||
}
|
||||
// field parts drawn over contents
|
||||
const p = part[i];
|
||||
if (p && sm && (p <= P_TR || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } }
|
||||
if (p && sm && (IS_TUBE[p] || p === P_FILTER)) { if (m === EMPTY || PH[m] === PH_GAS) { r = 0; g = 0; b = 0; } else { r = Math.max(1, r); } }
|
||||
else if (p) {
|
||||
if (p <= P_TR) {
|
||||
if (IS_TUBE[p]) {
|
||||
const on = netSat(i) > 0;
|
||||
const vert = p === P_TU || p === P_TD;
|
||||
const axis = vert ? y : x, across = vert ? x & 3 : y & 3;
|
||||
const dir = (p === P_TU || p === P_TL) ? 1 : -1;
|
||||
const vert = p === P_TU || p === P_TD, diagonal = TDX[p] && TDY[p];
|
||||
const axis = diagonal ? x * TDX[p] + y * TDY[p] : vert ? y : x;
|
||||
const across = diagonal ? ((x * -TDY[p] + y * TDX[p]) & 3) : vert ? x & 3 : y & 3;
|
||||
const dir = diagonal ? -1 : (p === P_TU || p === P_TL) ? 1 : -1;
|
||||
if (across === 0 || across === 3) { // copper casing on the two long sides; its load shows through
|
||||
if (m === EMPTY) { r = 150; g = 96; b = 64; } else { r = r * 0.72 + 44; g = g * 0.72 + 26; b = b * 0.72 + 14; }
|
||||
if ((axis & 3) === 0) { r -= 24; g -= 18; b -= 12; }
|
||||
@@ -217,7 +219,7 @@ function drawWorld() {
|
||||
// 4 built piece (drawn crisp), 5 smoke and flame, 6 built wall block (drawn as a tile)
|
||||
let cl = 0;
|
||||
const pt = p === P_PIPE && !pipeBodyCell(x, y) ? 0 : p; // the open parts of a pipe block show what is in them
|
||||
if (pt >= P_TU && pt <= P_TR) cl = TUBE_CLASS[pt];
|
||||
if (IS_TUBE[pt]) cl = TUBE_CLASS[pt];
|
||||
else if (p === P_FILTER) {
|
||||
// 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;
|
||||
|
||||
+2
-2
@@ -52,7 +52,7 @@ function placeBlock(code, v, bx, by, free, extra) {
|
||||
else if (isProcCode(code)) setCell(i, code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB, AMB[y], 0);
|
||||
else if (code === BP_GATE) setCell(i, GATE, AMB[y], 0);
|
||||
else if (code === BP_GRATE) { part[i] = P_GRATE; wakeXY(x, y); }
|
||||
else if (code === BP_TUBE) { part[i] = [P_TU, P_TR, P_TD, P_TL][v & 3]; 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 (code === BP_PIPE) { part[i] = P_PIPE; wakeXY(x, y); }
|
||||
else if (isPipeCode(code)) setCell(i, PIPE, AMB[y], 0);
|
||||
@@ -90,7 +90,7 @@ function blockIntegrity(b) {
|
||||
else if (isProcCode(code)) { if (m === (code === BP_GRIND ? GRINDER : code === BP_PRESS ? PRESSB : SAWB)) n++; }
|
||||
else if (code === BP_GATE) { if (m === GATE || p === P_GOPEN) n++; }
|
||||
else if (code === BP_GRATE) { if (p === P_GRATE) n++; }
|
||||
else if (code === BP_TUBE) { if (p >= P_TU && p <= P_TR) n++; }
|
||||
else if (code === BP_TUBE) { if (IS_TUBE[p]) n++; }
|
||||
else if (code === BP_FILTER) { if (p === P_FILTER) n++; }
|
||||
else if (isPipeCode(code)) { if (m === PIPE || p === P_PIPE) n++; }
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ function copyRegion(x0, y0, x1, y1) {
|
||||
}
|
||||
function flipBlueprint(bp) {
|
||||
const bw = bp.bw;
|
||||
const flipV = (code, v) => code === BP_BELT ? 1 - v : code === BP_FILTER ? v ^ 128 : code === BP_TUBE ? (v === 1 ? 3 : v === 3 ? 1 : v)
|
||||
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]
|
||||
: code === BP_PUMP || code === BP_OUTLET ? (((v & 3) === 1 ? 3 : (v & 3) === 3 ? 1 : v & 3) | (v & 4)) : v;
|
||||
return {
|
||||
v: 2, bw, bh: bp.bh, name: bp.name,
|
||||
|
||||
+3
-3
@@ -39,7 +39,7 @@ function enter(m, ph, j, dx, dy) {
|
||||
if (occ[j]) return 0;
|
||||
const p = part[j];
|
||||
if (p !== 0) {
|
||||
if (p <= P_TR) { if (ph === PH_POWDER && TDX[p] === -dx && TDY[p] === -dy) return 0; } // liquids and gases flow through freely
|
||||
if (IS_TUBE[p]) { if (ph === PH_POWDER && TDX[p] === -dx && TDY[p] === -dy) return 0; } // liquids and gases flow through freely
|
||||
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return 0; } // thick slag does not run through a grate
|
||||
else if (p === P_INTAKE) { if (ph !== PH_GAS) return 0; }
|
||||
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return 0; }
|
||||
@@ -92,7 +92,7 @@ function passable(m, ph, j, dx, dy) {
|
||||
if (occ[j]) return false;
|
||||
const p = part[j];
|
||||
if (p !== 0) {
|
||||
if (p <= P_TR) { if (ph === PH_POWDER && TDX[p] === -dx && TDY[p] === -dy) return false; }
|
||||
if (IS_TUBE[p]) { if (ph === PH_POWDER && TDX[p] === -dx && TDY[p] === -dy) return false; }
|
||||
else if (p === P_GRATE) { if (ph === PH_POWDER || m === MSLAG) return false; }
|
||||
else if (p === P_INTAKE) { if (ph !== PH_GAS) return false; }
|
||||
else if (p === P_FILTER) { if (ph === PH_POWDER && !filterPassAt(m, j)) return false; }
|
||||
@@ -198,7 +198,7 @@ function simMove() {
|
||||
if (stamp[i] === tickStamp) continue;
|
||||
if (ph === PH_SOLID) continue;
|
||||
const p = part[i];
|
||||
if (p >= P_TU && p <= P_TR && ph === PH_POWDER) continue; // a screw tube moves its own grains
|
||||
if (IS_TUBE[p] && ph === PH_POWDER) continue; // a screw tube moves its own grains
|
||||
if (ph === PH_POWDER) powder(i, x, y, m); else if (ph === PH_LIQUID) liquid(i, x, y, m); else gas(i, x, y, m);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-4
@@ -108,10 +108,11 @@ function tubeSpread(c, x, y, sx, sy, intake = false) {
|
||||
function tubeIntake(i, p, dx, dy) {
|
||||
if (occ[i]) return;
|
||||
const x = i % W, y = (i / W) | 0;
|
||||
const cands = [[-dx, -dy], [dy, dx], [-dy, -dx]];
|
||||
const start = (tick + i) % 3;
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const [ox, oy] = cands[(start + c) % 3];
|
||||
const k = (start + c) % 3, diagonal = dx && dy;
|
||||
const ox = diagonal ? (k === 1 ? 0 : -dx) : k === 0 ? -dx : k === 1 ? dy : -dy;
|
||||
const oy = diagonal ? (k === 0 ? 0 : -dy) : k === 0 ? -dy : k === 1 ? dx : -dx;
|
||||
const nx = x + ox, ny = y + oy;
|
||||
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
||||
const j = ny * W + nx, m = mat[j];
|
||||
@@ -119,10 +120,28 @@ function tubeIntake(i, p, dx, dy) {
|
||||
if (PH[m] !== PH_POWDER) continue; // the screw only takes loose material; liquids just flow through
|
||||
if (stamp[i] === tickStamp) return;
|
||||
const pj = part[j];
|
||||
if (pj >= P_TU && pj <= P_TR) continue; // other tube cells move their own contents
|
||||
if (IS_TUBE[pj]) continue; // other tube cells move their own contents
|
||||
if (pj === P_GRATE || pj === P_FILTER) continue;
|
||||
move(j, i, nx, ny, x, y);
|
||||
if (ox !== -dx || oy !== -dy) tubeSpread(i, x, y, -ox, -oy, true);
|
||||
if (diagonal) tubeSpread(i, x, y, 0, (y & 3) < 2 ? 1 : -1, true);
|
||||
else if (ox !== -dx || oy !== -dy) tubeSpread(i, x, y, -ox, -oy, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Four lanes cross a block in four beats; the casing carries every lane across the corner.
|
||||
// Prefer the next diagonal block, then either straight join, then the open head.
|
||||
function diagonalTubeNext(i, p) {
|
||||
const x = i % W, y = (i / W) | 0, dx = TDX[p], dy = TDY[p];
|
||||
const xx = x + dx, bx = x & ~3, by = y & ~3;
|
||||
if (xx < 0 || xx >= W) return -1;
|
||||
if ((xx >> 2) === (x >> 2)) return i + dx;
|
||||
const yy = y + dy * BS;
|
||||
if (yy >= 0 && yy < H && IS_TUBE[part[yy * W + xx]]) return yy * W + xx;
|
||||
if (IS_TUBE[part[y * W + xx]]) return y * W + xx;
|
||||
if (dy > 0 && yy < H && (mat[yy * W + xx] === BELTL || mat[yy * W + xx] === BELTR)) return ((yy & ~3) - 1) * W + xx;
|
||||
const hy = by + (dy > 0 ? BS : -1), hx = bx + (y & 3);
|
||||
if (hy >= 0 && hy < H && IS_TUBE[part[hy * W + hx]]) return hy * W + hx;
|
||||
// An open mouth discharges along the diagonal, clear of the last block.
|
||||
return yy >= 0 && yy < H ? yy * W + xx : -1;
|
||||
}
|
||||
|
||||
+31
-7
@@ -5,7 +5,7 @@
|
||||
// its cells as solid (occ).
|
||||
const BAR_W = 4, BAR_H = 2;
|
||||
let entCurM = 0; // the bar being moved, so a filter can let its own kind of bar through
|
||||
function entSolidCell(j) { const m = mat[j], p = part[j]; return occ[j] || PH[m] === PH_SOLID || PH[m] === PH_POWDER || (p !== 0 && (p <= P_GRATE || (p === P_FILTER && !filterPassAt(entCurM, j)))); }
|
||||
function entSolidCell(j) { const m = mat[j], p = part[j]; return occ[j] || PH[m] === PH_SOLID || PH[m] === PH_POWDER || (p !== 0 && (IS_TUBE[p] || p === P_GRATE || (p === P_FILTER && !filterPassAt(entCurM, j)))); }
|
||||
function entSupportCount(x, y) {
|
||||
if (y + BAR_H >= H) return BAR_W;
|
||||
let n = 0;
|
||||
@@ -42,23 +42,27 @@ function removeEnt(e) { entMark(e, 0); const k = ents.indexOf(e); if (k >= 0) en
|
||||
function entTubeAt(x, y) {
|
||||
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return 0;
|
||||
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
|
||||
const p = part[yy * W + xx]; if (p < P_TU || p > P_TR) return 0;
|
||||
const p = part[yy * W + xx]; if (!IS_TUBE[p]) return 0;
|
||||
}
|
||||
return part[(y + 1) * W + x + 1];
|
||||
}
|
||||
function entTubeFree(x, y, tubeOnly = false) {
|
||||
if (x < 0 || y < 0 || x + BAR_W > W || y + BAR_H > H) return false;
|
||||
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) {
|
||||
const j = yy * W + xx, p = part[j], tube = p >= P_TU && p <= P_TR;
|
||||
const j = yy * W + xx, p = part[j], tube = IS_TUBE[p];
|
||||
// gas and liquid don't stop a bar, in a tube or in the open
|
||||
if (occ[j] || (mat[j] !== EMPTY && PH[mat[j]] !== PH_GAS && PH[mat[j]] !== PH_LIQUID) || (tubeOnly ? !tube : !tube && entSolidCell(j))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Check the swept rectangle too: a snap must never jump through a grain or another bar.
|
||||
function entTubeMove(e, x, y, tubeOnly = false) {
|
||||
function entTubeMove(e, x, y, tubeOnly = false, mouth = false) {
|
||||
if (!entTubeFree(x, y, tubeOnly)) return false;
|
||||
for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++)
|
||||
if (mouth) {
|
||||
// Lift clear of the receiving belt before sliding over it; check both legs before moving.
|
||||
for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++) if (!entTubeFree(e.ix, yy)) return false;
|
||||
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, y)) return false;
|
||||
} else for (let yy = Math.min(e.iy, y); yy <= Math.max(e.iy, y); yy++)
|
||||
for (let xx = Math.min(e.ix, x); xx <= Math.max(e.ix, x); xx++) if (!entTubeFree(xx, yy)) return false;
|
||||
e.x = e.ix = x; e.y = e.iy = y; e.vx = e.vy = 0; return true;
|
||||
}
|
||||
@@ -72,7 +76,7 @@ function entTubeEnter(e, dx, dy) {
|
||||
const i = entTubeEdge(e, dx, dy, k);
|
||||
if (i < 0) continue;
|
||||
const xx = i % W, yy = (i / W) | 0;
|
||||
const p = part[i]; if (p < P_TU || p > P_TR || (!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.
|
||||
const x = (xx & ~3), y = entTubeLane(yy, e.iy);
|
||||
if (entTubeMove(e, x, TDX[p] || dy ? y : e.iy, true)) return true;
|
||||
@@ -84,13 +88,14 @@ function entTubeTick(e, p) {
|
||||
const ci = (e.iy + 1) * W + e.ix + 1;
|
||||
if ((tick & 1) || netSat(ci) <= 0 || rnd() > netSat(ci)) return;
|
||||
const dx = TDX[p], dy = TDY[p];
|
||||
if (dx && dy) { entDiagonalTubeTick(e, p); return; }
|
||||
let x = e.ix + dx, y = e.iy + dy, next = 0, ax = 0, ay = 0;
|
||||
for (let k = 0; k < (dx ? BAR_H : BAR_W); k++) {
|
||||
const i = entTubeEdge(e, dx, dy, k);
|
||||
if (i < 0) return;
|
||||
const xx = i % W, yy = (i / W) | 0;
|
||||
const q = part[i];
|
||||
if (q >= P_TU && q <= P_TR && q !== p) { next = q; ax = xx; ay = yy; break; }
|
||||
if (IS_TUBE[q] && q !== p) { next = q; ax = xx; ay = yy; break; }
|
||||
}
|
||||
if (next) {
|
||||
if (TDX[next] === -dx && TDY[next] === -dy) return;
|
||||
@@ -105,6 +110,25 @@ function entTubeTick(e, p) {
|
||||
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
|
||||
if (entTubeMove(e, x, y)) { e.vx = dx ? dx * 0.5 : (rnd() - 0.5) * 0.4; e.vy = dy * 0.6; stats.moved++; }
|
||||
}
|
||||
// A horizontal bar stays whole: one diagonal block per four screw beats.
|
||||
function entDiagonalTubeTick(e, p) {
|
||||
if (tick & 7) return;
|
||||
const dx = TDX[p], dy = TDY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3;
|
||||
for (const [ox, oy] of [[dx, dy], [dx, 0], [0, dy]]) {
|
||||
const x = bx + ox * BS, y = by + oy * BS + lane, q = entTubeAt(x, y);
|
||||
if (!q) continue;
|
||||
if (TDX[q] * dx + TDY[q] * dy < 0) return;
|
||||
if (entTubeMove(e, x, y, true)) stats.moved++;
|
||||
return;
|
||||
}
|
||||
const x = bx + dx * BS;
|
||||
let y = by + dy * BS + lane;
|
||||
const belt = y >= 0 && y < H ? mat[y * W + x] : EMPTY;
|
||||
if (dy > 0 && (belt === BELTL || belt === BELTR)) y = (y & ~3) - BAR_H;
|
||||
if (!entTubeFree(x, y)) 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++; }
|
||||
}
|
||||
function entTick() {
|
||||
for (let k = ents.length - 1; k >= 0; k--) {
|
||||
const e = ents[k];
|
||||
|
||||
+10
-6
@@ -107,7 +107,8 @@ function machinesTick() {
|
||||
}
|
||||
// tubes: every loose grain inside steps one cell along, front of the line first, on the same
|
||||
// beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt
|
||||
if ((tick & 1) === 0) for (let p = P_TU; p <= P_TR; p++) {
|
||||
if ((tick & 1) === 0) for (let p = 1; p < IS_TUBE.length; p++) {
|
||||
if (!IS_TUBE[p]) continue;
|
||||
const list = tubeLists[p], dx = TDX[p], dy = TDY[p], dj = dy * W + dx, flat = dy === 0;
|
||||
for (let k = 0; k < list.length; k++) {
|
||||
const i = list[k];
|
||||
@@ -124,7 +125,7 @@ function machinesTick() {
|
||||
const x = i % W, y = (i / W) | 0;
|
||||
// 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) {
|
||||
if (!flat && !dx) {
|
||||
let own = 0, shifted = false;
|
||||
const end = dy < 0 ? (y & ~3) : (y | 3);
|
||||
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + x]] === PH_POWDER) own++;
|
||||
@@ -140,18 +141,21 @@ function machinesTick() {
|
||||
}
|
||||
if (shifted) continue;
|
||||
}
|
||||
const nx = x + dx, ny = y + dy;
|
||||
if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) continue;
|
||||
const dest = dx && dy ? diagonalTubeNext(i, p) : i + dj;
|
||||
if (dest < 0) continue;
|
||||
const nx = dest % W, ny = (dest / W) | 0;
|
||||
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
||||
const j = i + dj;
|
||||
const j = dest;
|
||||
if (occ[j]) continue;
|
||||
const pj = part[j], tube = pj >= P_TU && pj <= P_TR;
|
||||
const pj = part[j], tube = IS_TUBE[pj];
|
||||
if (tube && TDX[pj] === -dx && TDY[pj] === -dy) continue;
|
||||
const t = mat[j];
|
||||
if (!roomFor(j)) continue;
|
||||
if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) continue;
|
||||
if (!tube && !pj && t === EMPTY) {
|
||||
// out of the mouth: the screw spits it clear, so the next grain is never blocked by the last
|
||||
launchGrain(i, nx, ny, dy ? (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++; continue;
|
||||
}
|
||||
move(i, j, x, y, nx, ny); stats.moved++;
|
||||
|
||||
+14
-5
@@ -10,7 +10,7 @@ let nextPowerBuild = 0;
|
||||
function rebuildPower() {
|
||||
powerDirty = false;
|
||||
for (const i of pcells) netId[i] = 0;
|
||||
nets = [null]; beltL = []; beltR = []; tubeLists = [[], [], [], [], []];
|
||||
nets = [null]; beltL = []; beltR = []; tubeLists = Array.from({ length: IS_TUBE.length }, () => []);
|
||||
const stack = [];
|
||||
for (const s of pcells) {
|
||||
if (netId[s]) continue;
|
||||
@@ -23,8 +23,13 @@ function rebuildPower() {
|
||||
const i = stack.pop();
|
||||
const m = mat[i], p = part[i];
|
||||
if (m === BELTL) { net.nb++; beltL.push(i); } else if (m === BELTR) { net.nb++; beltR.push(i); }
|
||||
if (p >= P_TU && p <= P_TR) { net.nt++; tubeLists[p].push(i); }
|
||||
const x = i % W;
|
||||
if (IS_TUBE[p]) { net.nt++; tubeLists[p].push(i); }
|
||||
const x = i % W, y = (i / W) | 0;
|
||||
// Corner-touching screws share power, without connecting unrelated diagonal machinery.
|
||||
if (IS_TUBE[p] && TDX[p] && TDY[p]) for (let d = -1; d <= 1; d += 2) {
|
||||
const xx = x + d * TDX[p], yy = y + d * TDY[p], j = yy * W + xx;
|
||||
if (xx >= 0 && xx < W && yy >= 0 && yy < H && part[j] === p && isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); }
|
||||
}
|
||||
let j;
|
||||
if (x > 0) { j = i - 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
|
||||
if (x < W - 1) { j = i + 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
|
||||
@@ -40,8 +45,12 @@ function rebuildPower() {
|
||||
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);
|
||||
tubeLists[P_TU].sort((a, b) => a - b); tubeLists[P_TD].sort((a, b) => b - a);
|
||||
tubeLists[P_TL].sort((a, b) => a - b); tubeLists[P_TR].sort((a, b) => b - a);
|
||||
for (const p of TUBE_PARTS) {
|
||||
tubeLists[p].sort((a, b) => {
|
||||
if (TDX[p] && TDY[p]) return ((b % W) - (a % W)) * TDX[p];
|
||||
return (TDY[p] || TDX[p]) < 0 ? a - b : b - a;
|
||||
});
|
||||
}
|
||||
}
|
||||
function netSat(i) { if (POWER_FREE) return 1; const n = nets[netId[i]]; return n ? n.sat : 0; }
|
||||
|
||||
|
||||
@@ -28,8 +28,13 @@ const SOLIDMETAL = new Uint8Array(NMAT); for (const m of [COPPER, IRON, STEEL, T
|
||||
|
||||
// ---------- Flags & parts ----------
|
||||
const F_NAT = 1, F_CAST = 2, F_BURN = 4;
|
||||
const P_NONE = 0, P_TU = 1, P_TD = 2, P_TL = 3, P_TR = 4, P_GRATE = 5, P_GOPEN = 6, P_WHEEL = 7, P_INTAKE = 8, P_FILTER = 9, P_PIPE = 10;
|
||||
const TDX = [0, 0, 0, -1, 1], TDY = [0, -1, 1, 0, 0];
|
||||
const P_NONE = 0, P_TU = 1, P_TD = 2, P_TL = 3, P_TR = 4, P_GRATE = 5, P_GOPEN = 6, P_WHEEL = 7, P_INTAKE = 8, P_FILTER = 9, P_PIPE = 10, P_TUR = 11, P_TDR = 12, P_TDL = 13, P_TUL = 14;
|
||||
const TUBE_PARTS = [P_TU, P_TR, P_TD, P_TL, P_TUR, P_TDR, P_TDL, P_TUL];
|
||||
const TUBE_DIRS = [[0, -1], [1, 0], [0, 1], [-1, 0], [1, -1], [1, 1], [-1, 1], [-1, -1]];
|
||||
const IS_TUBE = new Uint8Array(15), TDX = new Int8Array(15), TDY = new Int8Array(15);
|
||||
for (let v = 0; v < TUBE_PARTS.length; v++) {
|
||||
const p = TUBE_PARTS[v]; IS_TUBE[p] = 1; TDX[p] = TUBE_DIRS[v][0]; TDY[p] = TUBE_DIRS[v][1];
|
||||
}
|
||||
|
||||
// ---------- Tool tiers ----------
|
||||
// reach: how far from the bot the drill works. Harder rock takes longer, and
|
||||
|
||||
+2
-2
@@ -55,8 +55,8 @@ const PIECES = [
|
||||
desc: 'A half-block with a flat 45° face. Drag diagonally to make a funnel. R picks the material. T flips a dragged slope; without a drag, T cycles its four corners. Costs and returns 8 grains or 1 bar.' },
|
||||
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
|
||||
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it.' },
|
||||
{ id: 'tube', code: BP_TUBE, name: 'Screw tube', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', '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. Stack them pointing up to lift ore out of a mine, and turn the top one sideways to unload. Copper softens around 1000°C.' },
|
||||
{ id: 'tube', code: BP_TUBE, name: 'Screw tube', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left', 'up-right', 'down-right', 'down-left', 'up-left'],
|
||||
desc: 'A turning screw that draws in loose grains and whole bars and pushes them along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Drag a line to set its direction, including diagonals. R turns a single tube. Join either end to tubes or belts. Copper softens around 1000°C.' },
|
||||
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
|
||||
desc: 'A stretch of belt with a mesh in it. Put it in a line of belts and it joins them as one belt. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through.' },
|
||||
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ function deserializeWorld(meta, buffers) {
|
||||
const p = savedPart[i];
|
||||
if (p === P_WHEEL || p === P_INTAKE) continue;
|
||||
part[i] = p;
|
||||
if (p >= P_TU && p <= P_TR) markPow(i, true);
|
||||
if (IS_TUBE[p]) markPow(i, true);
|
||||
}
|
||||
for (let i = 0; i < N; i++) { const m = mat[i]; if (m === BELTL || m === BELTR || m === SHAFT) markPow(i, true); }
|
||||
// Old machines removed from the game are refunded.
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ function toolVacuum(cx, cy, r, pick) {
|
||||
const m = mat[i], ph = PH[m];
|
||||
if (!(ph === PH_POWDER || (ph === PH_LIQUID && player.flask))) return;
|
||||
if (!pick(m)) return;
|
||||
if (part[i] >= P_TU && part[i] <= P_TR) return;
|
||||
if (IS_TUBE[part[i]]) return;
|
||||
if (bagTotal >= bagCap()) { full = true; return false; }
|
||||
if (!first) first = m;
|
||||
bagAdd(m, 1); setCell(i, EMPTY, AMB[y], 0); got++;
|
||||
|
||||
+2
-2
@@ -17,7 +17,7 @@ let awake, hot, hotLvl, powderArea, roofDirty;
|
||||
let loadWakeOrder = null, loadWakePending = null, loadWakeIndex = 0;
|
||||
let tick = 0, tickStamp = 1;
|
||||
let machines = [null], molds = [], nets = [null], pcells = new Set(), powerDirty = true;
|
||||
let beltL = [], beltR = [], tubeLists = [[], [], [], [], []];
|
||||
let beltL = [], beltR = [], tubeLists = Array.from({ length: IS_TUBE.length }, () => []);
|
||||
let brokenMachines = [];
|
||||
let ghostMachines = [], ghostBlocked = new Set();
|
||||
const EV = [];
|
||||
@@ -55,7 +55,7 @@ function alloc(w, h) {
|
||||
roofDirty = new Uint8Array(CW).fill(1);
|
||||
powderArea = new Uint8Array(NCH).fill(1); // unknown areas get one visit before being ruled out
|
||||
machines = [null]; molds = []; nets = [null]; pcells = new Set(); powerDirty = true;
|
||||
beltL = []; beltR = []; tubeLists = [[], [], [], [], []]; brokenMachines = [];
|
||||
beltL = []; beltR = []; tubeLists = Array.from({ length: IS_TUBE.length }, () => []); brokenMachines = [];
|
||||
ghostMachines = []; ghostBlocked = new Set();
|
||||
tick = 0; tickStamp = 1; drillPulses = []; lastPulseTick = -99; undoStack = []; undoRec = null;
|
||||
pipeNets = []; pipeNetAt = null; pipesDirty = true;
|
||||
|
||||
+3
-2
@@ -61,7 +61,7 @@ function pickUnderCursor() {
|
||||
const P = PIECE_BY_CODE[code]; pid = P.id;
|
||||
if (isWall(code)) { GAME.wallMat = v; if (isSlope(code)) GAME.pieceDir.slope = code - BP_SLOPE_BL; }
|
||||
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 & 3;
|
||||
else if (P.dirs) GAME.pieceDir[P.id] = v % P.dirs.length;
|
||||
}
|
||||
if (pid) {
|
||||
const P = PIECE_BY_ID[pid];
|
||||
@@ -99,6 +99,7 @@ function dragDirFor(P, a, b) {
|
||||
if (!a || !P.dirs || !(P.id === 'belt' || P.id === 'tube' || P.filter)) return -1;
|
||||
const dx = b.bx - a.bx, dy = b.by - a.by;
|
||||
if (!dx && !dy) return -1;
|
||||
if (P.id === 'tube' && diagonalDrag(dx, dy)) return dx > 0 ? (dy < 0 ? 4 : 5) : (dy > 0 ? 6 : 7);
|
||||
if (P.id === 'tube') return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0);
|
||||
return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 0) : -1;
|
||||
}
|
||||
@@ -162,7 +163,7 @@ function primaryRelease(shift) {
|
||||
}
|
||||
if (t === 'build' && GAME.drag) {
|
||||
const P = piece(), b = aimBlock(), code = pieceCode(P, b);
|
||||
const list = shift ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by, isWall(code));
|
||||
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 dd = dragDirFor(P, GAME.drag, b);
|
||||
if (dd >= 0) GAME.pieceDir[P.id] = dd;
|
||||
const v = pieceV(P), ex = pieceExtra(P);
|
||||
|
||||
@@ -47,7 +47,7 @@ function buildVariantChips(P) {
|
||||
const dir = P.dirs ? P.dirs[GAME.pieceDir[P.id] || 0] : null;
|
||||
// while dragging a line of belts, filters or tubes, the direction is the drag's
|
||||
const dragging = GAME.drag && GAME.dragDir >= 0 && P.dirs;
|
||||
const glyph = d => ({ left: '←', right: '→', up: '↑', down: '↓' })[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 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}` : '');
|
||||
|
||||
+2
-1
@@ -115,7 +115,8 @@ function hoverInfo(ax, ay) {
|
||||
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb);
|
||||
t1 = 'Pipe' + (m !== EMPTY ? ` · ${MD[m].name} flowing past` : '');
|
||||
t2.push(pipeContentsText(pn));
|
||||
} else if (p) t1 = ({ 1: 'Screw tube ↑', 2: 'Screw tube ↓', 3: 'Screw tube ←', 4: 'Screw tube →', 5: 'Grate', 6: 'Gate (open)', 7: 'Water wheel', 8: 'Engine intake' })[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 (m === BELTL || m === BELTR) t1 = 'Belt ' + (m === BELTL ? '←' : '→');
|
||||
else if (m === PIPE && isPipeCode(b)) {
|
||||
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb), v = blockV[pb], arrows = ['↑', '→', '↓', '←'];
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ function controlsHtml() {
|
||||
3. Build (3) a chamber of stone wall blocks with a one-block vent in the roof, a floor of clay Grates, and an Ingot mold right under the grates. The notebook's Blueprints tab has this as a starter design.<br>
|
||||
4. Ctrl+click coal and copper ore in the bag to blow a mix. Fly inside (the bot passes through what you build) and right click to blow an even mix onto the grates: about 50 of each. (Ore first and coal on top works too; coal buried under ore will not light.) Light a coal grain on top with the Igniter.<br>
|
||||
5. When the ingot sets, bars drop out of the bottom of the mold onto whatever is under it. Leave one open block under it: a belt below that carries them off, or pick them up with the nozzle. Leftover metal stays for the next firing. Ingot molds empty themselves: bars, junk and scrap drop below, slag is thrown away and the grate above is raked. Removing any mold returns its set metal to your bag: whole bars as bars, the rest as scrap.<br>
|
||||
Screw tubes carry grains and whole bars: feed bars in from a belt, turn corners and unload at the mouth.<br>
|
||||
Screw tubes carry grains and whole bars: feed bars in from a belt, turn corners and unload at the mouth. Drag at about 45° for a diagonal run; R cycles eight directions.<br>
|
||||
6. Blow the bars into the trade dock: they sell the moment they drop through its grate, and bars pay far more than ore. Watch the Market page for orders that pay double. Money buys belts, screw tubes and fans; pay down the claim there too.<br>
|
||||
7. From there: deeper ores (tin, zinc, silver, gold), alloys, fans for hotter fires, iron and steel.</p>
|
||||
`;
|
||||
|
||||
+3
-3
@@ -106,7 +106,7 @@ function drawPreviews() {
|
||||
if (P.kind === 'block') {
|
||||
const v = pieceV(P);
|
||||
col = previewColor(code, v);
|
||||
cells = GAME.drag ? (modShift() ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by, isWall(code))) : [[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) || code === BP_TUBE)) : [[b.bx, b.by]];
|
||||
const dd = GAME.drag ? dragDirFor(P, GAME.drag, b) : -1;
|
||||
if (dd !== GAME.dragDir) { GAME.dragDir = dd; hudDirty = true; }
|
||||
const dIdx = dd >= 0 ? dd : (GAME.pieceDir[P.id] || 0);
|
||||
@@ -124,7 +124,7 @@ function drawPreviews() {
|
||||
previewBlock(code, x, y, w, h, true); ctx.setLineDash([]);
|
||||
if (P.dirs && (P.id === 'belt' || P.id === 'tube' || P.filter || P.id === 'pump' || P.id === 'outlet')) {
|
||||
// a chevron on every block, pointing the way it will carry (or pump, or pour)
|
||||
const [dx, dy] = P.id === 'tube' ? [[0, -1], [1, 0], [0, 1], [-1, 0]][dIdx] : P.id === 'pump' ? PUMP_V[dIdx] : P.id === 'outlet' ? OUTLET_V[dIdx] : [[-1, 0], [1, 0]][dIdx];
|
||||
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 cx = x + w / 2, cy = y + h / 2, k = w * 0.22, px_ = -dy, py_ = dx;
|
||||
ctx.strokeStyle = 'rgba(20,16,12,0.55)'; ctx.lineWidth = Math.max(2, zp * 0.9); ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||
const chev = () => { ctx.beginPath(); ctx.moveTo(cx - dx * k * 0.6 + px_ * k, cy - dy * k * 0.6 + py_ * k); ctx.lineTo(cx + dx * k * 0.7, cy + dy * k * 0.7); ctx.lineTo(cx - dx * k * 0.6 - px_ * k, cy - dy * k * 0.6 - py_ * k); ctx.stroke(); };
|
||||
@@ -135,7 +135,7 @@ function drawPreviews() {
|
||||
if (GAME.drag && dd >= 0 && cells.length > 1 && !modShift()) {
|
||||
const [x0, y0, w0, h0] = blockRectPx(cells[0][0], cells[0][1], 1, 1), [x1, y1, w1, h1] = blockRectPx(cells[cells.length - 1][0], cells[cells.length - 1][1], 1, 1);
|
||||
let sx = x0 + w0 / 2, sy = y0 + h0 / 2, ex = x1 + w1 / 2, ey = y1 + h1 / 2;
|
||||
const [dx, dy] = P.id === 'tube' ? [[0, -1], [1, 0], [0, 1], [-1, 0]][dd] : [[-1, 0], [1, 0]][dd];
|
||||
const [dx, dy] = P.id === 'tube' ? TUBE_DIRS[dd] : [[-1, 0], [1, 0]][dd];
|
||||
if ((ex - sx) * dx + (ey - sy) * dy < 0) { const tx = sx, ty = sy; sx = ex; sy = ey; ex = tx; ey = ty; }
|
||||
const off = BS * zp * 1.0, hx = ex + dx * BS * zp * 0.55, hy = ey + dy * BS * zp * 0.55, ah = Math.max(8, BS * zp * 0.5);
|
||||
const ox = dy ? off : 0, oy = dx ? off : 0;
|
||||
|
||||
@@ -695,6 +695,23 @@ with sync_playwright() as p:
|
||||
check('square wall drag can also place a diagonal', ev(f'Array.from({{length:4}}, (_,k)=>blockP[({bby}-12+k)*BW+{bbx}-5+k]).every(c=>c===BP_WALL)'))
|
||||
pg.keyboard.press('Control+z'); settle(60)
|
||||
|
||||
# Diagonal tubes use real mouse placement, preview arrows, R and picking.
|
||||
check('select screw tube', select('3', "piece().id === 'tube'"))
|
||||
ev('GAME.pieceDir.tube = 0')
|
||||
directions = []
|
||||
for _ in range(8):
|
||||
directions.append(ev('GAME.pieceDir.tube'))
|
||||
pg.keyboard.press('r'); settle(40)
|
||||
check('R cycles eight tube directions', directions == list(range(8)) and ev('GAME.pieceDir.tube') == 0)
|
||||
drag(lambda: blk(bbx - 5, bby - 12), lambda: blk(bbx - 2, bby - 9))
|
||||
check('diagonal tube drag places four down-right blocks', ev(f'Array.from({{length:4}}, (_,k)=>[blockP[({bby}-12+k)*BW+{bbx}-5+k],blockV[({bby}-12+k)*BW+{bbx}-5+k]]).every(([c,v])=>c===BP_TUBE && v===5)'))
|
||||
pg.mouse.move(*blk(bbx - 4, bby - 11)); settle(60)
|
||||
check('diagonal hover names its direction', ev(f"hoverInfo(({bbx}-4)*4+1,({bby}-11)*4+1).t1.includes('down-right')"))
|
||||
pg.mouse.click(*blk(bbx - 4, bby - 11), button='middle'); settle(60)
|
||||
check('picking a diagonal tube keeps its direction', ev("piece().id === 'tube' && GAME.pieceDir.tube === 5"))
|
||||
pg.screenshot(path=str(out / '04-diagonal-tubes.png'))
|
||||
pg.keyboard.press('Control+z'); settle(60)
|
||||
|
||||
# 6. ingot mold: click to place, pour, bars drop below, vacuum them
|
||||
ok = select('3', "piece().id === 'ingotmold'")
|
||||
check('E cycles to the ingot mold', ok)
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// Diagonal screws: sealed four-lane corner transfers, bars, joins and persistence.
|
||||
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
|
||||
const fs = require('fs');
|
||||
const { run } = load();
|
||||
const { check, done } = suite();
|
||||
function fresh() { flatWorld(run, 256, 192, 176); run('RS=7; nextPowerBuild=0; money=10000; AMB.fill(20); temp.fill(20); hot.fill(0);'); }
|
||||
function line(v, chain = false) {
|
||||
fresh();
|
||||
const [dx, dy] = run(`TUBE_DIRS[${v}]`), bx = dx > 0 ? 16 : 36, by = dy < 0 ? 32 : 12;
|
||||
run(`for(let k=0;k<10;k++) placeBlock(BP_TUBE,${v},${bx}+k*${dx},${by}+k*${dy},true);`);
|
||||
let tail = bx, head = bx + 9 * dx;
|
||||
if (chain) {
|
||||
run(`for(let k=1;k<=3;k++) { placeBlock(BP_TUBE,${dx > 0 ? 1 : 3},${bx}-k*${dx},${by},true); placeBlock(BP_TUBE,${dx > 0 ? 1 : 3},${head}+k*${dx},${by + 9 * dy},true); }`);
|
||||
tail -= 3 * dx; head += 3 * dx;
|
||||
}
|
||||
return { dx, dy, bx, by, tail, head, hy: by + 9 * dy };
|
||||
}
|
||||
function drain(s, steps, feed = false) {
|
||||
return run(`(() => {
|
||||
let out=0, leaks=0, added=0;
|
||||
for(let k=0;k<${steps};k++) {
|
||||
${feed ? `for(let l=0;l<4;l++) { const i=(${s.by}*4+l)*W+${s.tail}*4+${s.dx > 0 ? 0 : 3}; if(mat[i]===EMPTY) {setCell(i,SAND,20,0);added++;} }` : ''}
|
||||
simTick();
|
||||
for(let i=0;i<N;i++) if(mat[i]===SAND && !IS_TUBE[part[i]]) {
|
||||
const x=i%W; if(${s.dx}>0 ? x>=${(s.head + 1) * 4} : x<${s.head * 4}) out++; else leaks++;
|
||||
setCell(i,EMPTY,20,0);
|
||||
}
|
||||
for(const f of flyers) if(f.m===SAND) {
|
||||
if(${s.dx}>0 ? f.x>=${(s.head + 1) * 4} : f.x<${s.head * 4}) out++; else leaks++;
|
||||
}
|
||||
flyers=flyers.filter(f=>f.m!==SAND);
|
||||
}
|
||||
return {out,leaks,added,inside:mat.reduce((n,m)=>n+(m===SAND),0)};
|
||||
})()`);
|
||||
}
|
||||
for (const v of [4, 5, 6, 7]) {
|
||||
const s = line(v);
|
||||
check(`direction ${v} uses appended part ids and marks power`,run(`part[${s.by * 4}*W+${s.bx * 4}]===${v + 7} && blockV[${s.by}*BW+${s.bx}]===${v} && pcells.size===160`));
|
||||
// Fill every lane, in every block: a corner or side leak is counted before it can fall away.
|
||||
run('for(let i=0;i<N;i++) if(IS_TUBE[part[i]]) setCell(i,SAND,20,0);');
|
||||
const r = drain(s, 300);
|
||||
check(`direction ${v} carries sand through ten blocks and out the head`, r.out === 160, JSON.stringify(r));
|
||||
check(`direction ${v} has no corner or side spills and conserves grains`, r.leaks === 0 && r.out+r.inside === 160,JSON.stringify(r));
|
||||
run('rebuildPower();');
|
||||
check(`direction ${v} joins corner-touching power networks`,run('nets.length===2 && nets[1].nt===160'));
|
||||
const c = line(v, true);
|
||||
rect(run,c.tail*4,c.by*4,c.tail*4+3,c.by*4+3,run('SAND'));
|
||||
const joined = drain(c,500);
|
||||
check(`direction ${v}: straight to diagonal to straight`,joined.out===16 && joined.leaks===0,JSON.stringify(joined));
|
||||
}
|
||||
// A saturated ten-block line: 80 warmup ticks, then 600 measured ticks at 60 Hz.
|
||||
const rates = [];
|
||||
for (const v of [0, 4, 5]) {
|
||||
let s;
|
||||
if (v) s=line(v);
|
||||
else {
|
||||
fresh();run('for(let by=23;by<=32;by++) placeBlock(BP_TUBE,0,16,by,true);');
|
||||
s={dx:0,tail:16,by:32,head:16};
|
||||
}
|
||||
const result = v ? (drain(s,80,true),drain(s,600,true)) : run(`(() => {
|
||||
let out=0;
|
||||
for(let k=0;k<680;k++) {
|
||||
for(let l=0;l<4;l++) {const i=131*W+64+l;if(mat[i]===EMPTY)setCell(i,SAND,20,0);}
|
||||
simTick();
|
||||
if(k>=80) out+=flyers.filter(f=>f.m===SAND).length;
|
||||
flyers=[];
|
||||
}return {out};
|
||||
})()`);
|
||||
rates.push(result.out/10);
|
||||
}
|
||||
console.log('Saturated throughput, ten blocks, grains/s: straight/up-diagonal/down-diagonal '+rates.join('/'));
|
||||
check('diagonals match straight throughput per block',rates.every(n=>n===120),rates);
|
||||
// A belt's last carrying cell feeds the tail side of an upward diagonal screw.
|
||||
const belt=line(4);
|
||||
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,33,true);');
|
||||
rect(run,39,100,39,131,run('STONE'));
|
||||
rect(run,40,120,55,131,run('SAND'));
|
||||
const beltOut=run(`(()=>{let out=0;for(let k=0;k<1000;k++){simTick();for(let f=flyers.length-1;f>=0;f--)if(flyers[f].m===SAND && flyers[f].x>=104){out++;flyers.splice(f,1);}}return out;})()`);
|
||||
check('a belt feeds sand into a diagonal tail and out its head',beltOut>150,beltOut);
|
||||
for(const v of [4,5,6,7]) {
|
||||
const s=line(v);
|
||||
rect(run,s.bx*4,s.by*4-8,s.bx*4+3,s.by*4-1,run('WATER'));
|
||||
ticks(run,400);
|
||||
check(`water flows freely through diagonal ${v}`,count(run,run('WATER'),0,0,255,172)===0 && count(run,run('WATER'))===32,count(run,run('WATER')));
|
||||
}
|
||||
// Bars keep their horizontal shape, with no saved ride state.
|
||||
function putBar(x,y) {
|
||||
run(`{ const e=spawnBar(BARCU,20,20,0,0); entMark(e,0); e.x=e.ix=${x}; e.y=e.iy=${y}; entMark(e,1); }`);
|
||||
}
|
||||
function watchBars(s, n=600) {
|
||||
return run(`(()=>{let out=0,overlap=false;for(let k=0;k<${n};k++){
|
||||
simTick();
|
||||
for(let a=0;a<ents.length;a++)for(let b=0;b<a;b++) {
|
||||
const e=ents[a],f=ents[b];if(e.ix<f.ix+4 && f.ix<e.ix+4 && e.iy<f.iy+2 && f.iy<e.iy+2)overlap=true;
|
||||
}
|
||||
if(mat.some((m,i)=>occ[i] && (PH[m]===PH_SOLID || PH[m]===PH_POWDER)))overlap=true;
|
||||
for(let a=ents.length-1;a>=0;a--) {const e=ents[a];if(${s.dx}>0 ? e.ix>=${(s.head + 1) * 4} : e.ix+4<=${s.head * 4}) {out++;removeEnt(e);} }
|
||||
}return {out,overlap,left:ents.length};})()`);
|
||||
}
|
||||
for(const v of [4,5,6,7]) for(const chain of [false,true]) {
|
||||
const s=line(v,chain);
|
||||
putBar(s.tail*4,s.by*4);
|
||||
putBar(s.tail*4,s.by*4+2); // a queued bar must wait for the first one
|
||||
const r=watchBars(s);
|
||||
check(`bars travel diagonal ${v}${chain?' and both straight joins':''} without overlap`,r.out===2 && !r.overlap,JSON.stringify(r));
|
||||
}
|
||||
// Vertical straight joins reach the same tails and heads too.
|
||||
for (const v of [4,5,6,7]) {
|
||||
const s=line(v), vv=s.dy<0?0:2;
|
||||
run(`for(let k=1;k<=3;k++){placeBlock(BP_TUBE,${vv},${s.bx},${s.by}-k*${s.dy},true);placeBlock(BP_TUBE,${vv},${s.head},${s.hy}+k*${s.dy},true);}`);
|
||||
putBar(s.bx*4,(s.by-3*s.dy)*4+1);
|
||||
const r=run(`(()=>{let out=false;for(let k=0;k<500;k++){simTick();if(${s.dy}<0?ents[0].iy+2<=${(s.hy-3)*4}:ents[0].iy>=${(s.hy+4)*4})out=true;}return {out,overlap:mat.some((m,i)=>occ[i] && PH[m]===PH_SOLID)};})()`);
|
||||
check(`a bar crosses vertical joins at diagonal ${v}`,r.out && !r.overlap,JSON.stringify(r));
|
||||
// Sand from the vertical intake exits the far vertical head; no outside grains are left behind.
|
||||
run('for(const e of ents.slice())removeEnt(e);');
|
||||
rect(run,s.bx*4,(s.by-3*s.dy)*4,s.bx*4+3,(s.by-3*s.dy)*4+3,run('SAND'));
|
||||
const grain=run(`(()=>{let out=0,leak=0;for(let k=0;k<500;k++){simTick();for(const f of flyers)if(f.m===SAND){if(${s.dy}<0?f.y<${(s.hy-3)*4}:f.y>=${(s.hy+4)*4})out++;else leak++;}flyers=[];}return {out,leak};})()`);
|
||||
check(`sand crosses vertical joins at diagonal ${v}`,grain.out===16 && !grain.leak,JSON.stringify(grain));
|
||||
}
|
||||
for(const v of [4,5]) {
|
||||
const s=line(v);
|
||||
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,'+(s.by+1)+',true);spawnBar(BARCU,44,'+(s.by*4+2)+',0,0);');
|
||||
check(`a belt feeds a bar into diagonal ${v}`,watchBars(s).out===1);
|
||||
// An ascending screw discharges above the receiving belt; a descending one lands on its top.
|
||||
const b=line(v), beltY=v===4?b.hy:b.hy+1;
|
||||
run(`for(let k=1;k<=8;k++)placeBlock(BP_BELT,1,${b.head}+k,${beltY},true);`);
|
||||
rect(run,b.bx*4,b.by*4,b.bx*4+3,b.by*4+3,run('SAND'));
|
||||
const onBelt=run(`(()=>{let seen=false;for(let k=0;k<300;k++){simTick();for(let x=${(b.head+2)*4};x<${(b.head+8)*4};x++)if(mat[${beltY*4-1}*W+x]===SAND)seen=true;}return seen;})()`);
|
||||
check(`diagonal ${v} unloads sand onto a belt`,onBelt);
|
||||
run('for(let i=0;i<N;i++)if(mat[i]===SAND)setCell(i,EMPTY,20,0);flyers=[];');
|
||||
putBar(b.bx*4,b.by*4+1);
|
||||
check(`diagonal ${v} unloads a bar onto a belt`,run(`(()=>{for(let k=0;k<400;k++){simTick();if(ents[0].ix>=${(b.head+2)*4} && ents[0].iy===${beltY*4-2})return true;}return false;})()`));
|
||||
}
|
||||
const savedRide=line(4);
|
||||
putBar(savedRide.bx*4,savedRide.by*4+1);ticks(run,24);
|
||||
run('globalThis.rideSave=serializeWorld();deserializeWorld(rideSave.meta,rideSave.buffers);');
|
||||
check('a bar saved mid-diagonal resumes its ride',watchBars(savedRide).out===1);
|
||||
// Mouth obstruction must hold the bar, and clearing it must resume transport.
|
||||
const blocked=line(4);
|
||||
putBar(blocked.bx*4,blocked.by*4+1);
|
||||
rect(run,(blocked.head+1)*4,(blocked.hy-1)*4,(blocked.head+2)*4-1,blocked.hy*4-1,run('STONE'));
|
||||
ticks(run,300);
|
||||
check('a blocked diagonal mouth holds its bar without wall overlap',run('ents.length===1 && entTubeAt(ents[0].ix,ents[0].iy) && !mat.some((m,i)=>m===STONE && occ[i])'));
|
||||
rect(run,(blocked.head+1)*4,(blocked.hy-1)*4,(blocked.head+2)*4-1,blocked.hy*4-1,run('EMPTY'));
|
||||
check('the diagonal bar resumes after the mouth clears',watchBars(blocked).out===1);
|
||||
for(const v of [4,5,6,7]) {
|
||||
fresh(); run(`placeBlock(BP_TUBE,${v},20,20,true);`);
|
||||
run('globalThis.saved=serializeWorld();deserializeWorld(saved.meta,saved.buffers);');
|
||||
check(`save/load keeps direction ${v} and power`,run(`blockV[20*BW+20]===${v} && part[80*W+80]===${v+7} && isPow[80*W+80]`));
|
||||
run('globalThis.clip=copyRegion(80,80,83,83);bot.x=100;bot.y=80;undoRecord(\'paste\',()=>pasteGhost(clip,24,20));ghostTick();');
|
||||
check(`copy/paste keeps direction ${v}`,run(`blockV[20*BW+24]===${v} && blockIntegrity(20*BW+24)===16`));
|
||||
run('undoLast();');
|
||||
check(`undo removes pasted diagonal ${v}`,run('blockP[20*BW+24]===0'));
|
||||
run('globalThis.flipped=flipBlueprint(clip);');
|
||||
check(`flip maps direction ${v}`,run('flipped.blocks[0][3]')===[7,6,5,4][v-4]);
|
||||
run('undoRecord(\'cut\',()=>cutRegion(80,80,83,83));undoLast();');
|
||||
check(`cut and undo restore direction ${v}`,run(`blockV[20*BW+20]===${v} && blockIntegrity(20*BW+20)===16`));
|
||||
run('money=100;removeBlock(20,20);');
|
||||
check(`diagonal ${v} refunds the straight tube price`,run('money')===125);
|
||||
}
|
||||
// Exercise the actual UI functions without starting a browser or simulating a replacement.
|
||||
run(fs.readFileSync('src/ui/selection.js','utf8'));
|
||||
run(fs.readFileSync('src/ui/actions.js','utf8'));
|
||||
run('globalThis.GAME={pieceDir:{tube:0}};globalThis.hudDirty=false;globalThis.toolId=()=>\'build\';globalThis.piece=()=>PIECE_BY_ID.tube;globalThis.playSfx=()=>{};');
|
||||
const cycle=run('(()=>{const ds=[];for(let k=0;k<8;k++){ds.push(GAME.pieceDir.tube);rotate();}return ds;})()');
|
||||
check('R cycles all eight tube directions then wraps',cycle.join(',')==='0,1,2,3,4,5,6,7' && run('GAME.pieceDir.tube')===0,cycle);
|
||||
for(const [dx,dy,v] of [[5,-4,4],[4,5,5],[-5,4,6],[-4,-5,7]]) {
|
||||
check(`a diagonal drag picks direction ${v}`,run(`dragDirFor(PIECE_BY_ID.tube,{bx:20,by:20},{bx:${20+dx},by:${20+dy}})`)===v);
|
||||
check(`direction ${v} lays a corner-touching diagonal run`,run(`blockLine(20,20,${20+dx},${20+dy},true).every(([x,y],k)=>x===20+k*${Math.sign(dx)} && y===20+k*${Math.sign(dy)})`));
|
||||
}
|
||||
done();
|
||||
@@ -0,0 +1,7 @@
|
||||
// Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt.
|
||||
const tx = BX + 12, ty = BY - 1;
|
||||
for (let x = BX + 3; x < tx; x++) placeBlock(BP_BELT, 1, x, BY, true);
|
||||
for (let k = 0; k < 10; k++) placeBlock(BP_TUBE, 4, tx + k, ty - k, true);
|
||||
for (let x = tx + 10; x < tx + 18; x++) placeBlock(BP_BELT, 1, x, ty - 9, true);
|
||||
for (let y = BY * 4 - 22; y < BY * 4; y++) for (let x = (BX + 4) * 4; x < (BX + 11) * 4; x++) setCell(y * W + x, SAND, 20, 0);
|
||||
window.CAM = [(tx + 4) * 4, (ty - 4) * 4];
|
||||
Reference in New Issue
Block a user