From 18528a7f73e7dc0f1df8ef149a06b7314f4b3415 Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Mon, 26 Oct 2026 23:27:06 -0400 Subject: [PATCH] Shift-drag draws a line for belts and other transport pieces Belts, filters, screw conveyors, shafts and pipe pieces never fill a rectangle; a diagonal belt drag stays sloped with Shift held. Co-Authored-By: Claude Opus 5.5 --- docs/DESIGN.md | 4 ++++ docs/PROJECT_MAP.md | 14 +++++++------- docs/agent-log.md | 11 +++++++++++ src/sim/pieces.js | 2 ++ src/ui/actions.js | 2 +- src/ui/build-menu.js | 2 +- src/ui/notebook.js | 2 +- src/ui/previews.js | 2 +- src/ui/selection.js | 3 ++- test/play.py | 6 ++++++ 10 files changed, 36 insertions(+), 12 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index a79d1ea..4bf8db1 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -477,6 +477,10 @@ above, including: chose to make stickiness grain against grain only: on anything built, every material slides freely, so slopes are useful for every material, while heaps keep their shape. Real chutes clog with sticky material and need cleaning, but that is micromanagement the game doesn't need. +- **Belts and other transport pieces never fill a rectangle (2026-10-27).** The owner said Shift + doesn't need to fill a rectangle for belts, conveyors and the like. Belts, filters, screw + conveyors, shafts, pipes, pumps and outlets (`lineOnly()`) always draw a line, Shift or not, and + a diagonal belt drag stays sloped. Walls, grates and gates still Shift-fill. ## Ideas not built yet diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index 833ff31..d7f96ca 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -205,9 +205,9 @@ Material definitions: ids, properties and metal families. ids, and drill/tank ti Material tables: typed properties, casting groups, flags, parts and tool tiers. Typed property tables - variables: PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, P_NONE, P_SCREW_U, P_SCREW_D, P_SCREW_L, P_SCREW_R, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, P_SCREW_UR, P_SCREW_DR, P_SCREW_DL, P_SCREW_UL, SCREW_PARTS, SCREW_DIRS, IS_SCREW, SCREW_DX, SCREW_DY, BIT_TIERS, TANK_TIERS, REACH -### src/sim/pieces.js (177 lines) +### src/sim/pieces.js (179 lines) Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost. -- functions: isSlopeBelt, isFilter, isBelt, beltGrade, slopeShape, beltFaceY, filterVariant, beltDirection, isSlope, isWall, slopeCell, slopeCode, isPipeCode, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost +- functions: isSlopeBelt, isFilter, isBelt, beltGrade, slopeShape, beltFaceY, filterVariant, beltDirection, isSlope, isWall, slopeCell, slopeCode, isPipeCode, lineOnly, filterable, cleanFilterCfg, filterRec, filterPassAt, filterInfo, sameFilter, wallCost - variables: BS, BP_WALL, BP_BELT, BP_SCREW, BP_SHAFT, BP_GRATE, BP_GATE, BP_FILTER, BP_PIPE, BP_PUMP, BP_OUTLET, BP_GRIND, BP_SAW, BP_PRESS, BP_RACK, BP_SLOPE_BL, BP_SLOPE_BR, BP_SLOPE_TL, BP_SLOPE_TR, BP_BELT_UP, BP_BELT_DOWN, BP_FILTER_UP, BP_FILTER_DOWN, filterCfg, WALL_OF, WALL_LIST, BAR_OF, GRAINS_PER_BAR, POWER_FREE, CASH, PIECES, PIECE_BY_ID, PIECE_BY_CODE, BELT_POWER, SCREW_POWER, FAN_POWER, WHEEL_K, ENGINE_K, MOLDS ### src/sim/economy.js (152 lines) @@ -230,7 +230,7 @@ Exploration only: never changes grains or the random stream. - functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll - variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH -### src/sim/cells.js (223 lines) +### src/sim/cells.js (225 lines) Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove). - functions: setCell, swapCells, move, enter, enterFree, powder, passable, liquid, gas, wakeBotPowder, simMove - variables: SORTP, SORTP_FUEL, LSTEP, LFILM, CORNER_SEAL @@ -274,9 +274,9 @@ Cleats are rubber ribs standing up from a sloped belt's face, one every CLEAT_SP - 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 -### src/sim/blocks.js (219 lines) +### src/sim/blocks.js (235 lines) Building blocks: placing, integrity, removing, lines and rectangles of blocks. -- functions: blockOf, blockCellList, pieceCost, setSlopeSeal, slopeSeal, slopeSealOwner, wallCells, canPlaceBlock, placeBlock, blockIntegrity, removeWallCell, removeBlock, diagonalDrag, blockLine, blockRect, blockInReach, blockIs, placeBlocks +- functions: blockOf, blockCellList, pieceCost, setSlopeSeal, slopeSeal, slopeSealOwner, blockOwnsCell, clearStraySeals, wallCells, canPlaceBlock, placeBlock, blockIntegrity, removeWallCell, removeBlock, diagonalDrag, blockLine, blockRect, blockInReach, blockIs, placeBlocks ### src/sim/molds.js (317 lines) Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal. @@ -347,7 +347,7 @@ World generation helpers: noise and removal of tiny hard rock specks. World generation: staged terrain, ore, caves and placement, with synchronous and async entry points. - functions: generateWorld, generateWorldAsync -### src/sim/save.js (179 lines) +### src/sim/save.js (180 lines) Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration. - functions: serializeWorld, deserializeWorld, startLoadWake, wakeLoadArea, wakeLoadNearBot, loadWakeTick - variables: SAVE_VERSION, TOFF @@ -449,7 +449,7 @@ The build menu along the bottom: categories, piece tiles and their icons. - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu -### src/ui/selection.js (120 lines) +### src/ui/selection.js (121 lines) Tool selection: cycling, drill tuning, piece variants and costs. - functions: pieceSize, pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterBlockAt, chipCost, pieceCostNow, pieceCode, turnSlope diff --git a/docs/agent-log.md b/docs/agent-log.md index a14e293..02e1bbb 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -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-27: Shift-drag draws a line for transport pieces (Claude Code) + +- Owner request: Shift shouldn't fill a rectangle for belts, conveyors and similar. New + `lineOnly(code)` in `src/sim/pieces.js` (belts, filters, screw conveyors, shafts, pipe pieces); + `primaryRelease` (`src/ui/actions.js`) and the drag preview (`src/ui/previews.js`) ignore Shift + for them, and `pieceCode` (`src/ui/selection.js`) no longer flattens a diagonal belt drag on + Shift. Build-menu "How to place" and the notebook say so; the notebook's stale "cleats up or + down" line now says uphill only. +- Tests: play.py checks a Shift-drag with a belt lays a 3-piece sloped line. Full + `run_all --browser` passes. + ## 2026-10-27: t61 sticky materials slide freely on built surfaces (Claude Code) - The owner's video showed clay lumps creeping down a sloped wall; removing a piece was diff --git a/src/sim/pieces.js b/src/sim/pieces.js index 67b4326..e2d4016 100644 --- a/src/sim/pieces.js +++ b/src/sim/pieces.js @@ -32,6 +32,8 @@ function slopeCode(dx, dy, orientation) { return dx * dy > 0 ? (orientation >= 2 ? BP_SLOPE_TR : BP_SLOPE_BL) : (orientation >= 2 ? BP_SLOPE_TL : BP_SLOPE_BR); } function isPipeCode(c) { return c === BP_PIPE || c === BP_PUMP || c === BP_OUTLET; } +// pieces that carry something along a run: a Shift-drag still draws a line, never a filled rectangle +function lineOnly(c) { return isBelt(c) || isFilter(c) || c === BP_SCREW || c === BP_SHAFT || isPipeCode(c); } // Filters: each filter block has a set of materials and a mode. In drop mode (inv 0) // the chosen materials fall through and everything else rides along on top; in carry // mode (inv 1) the chosen ones ride along and everything else falls through. The block diff --git a/src/ui/actions.js b/src/ui/actions.js index 9cbeff3..be2ca81 100644 --- a/src/ui/actions.js +++ b/src/ui/actions.js @@ -167,7 +167,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) || isSlopeBelt(code) || code === BP_SCREW); + const list = shift && !lineOnly(code) ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by, isWall(code) || isSlopeBelt(code) || code === BP_SCREW); const dd = dragDirFor(P, GAME.drag, b); if (dd >= 0) GAME.pieceDir[P.id] = dd; const v = pieceV(P), ex = pieceExtra(P); diff --git a/src/ui/build-menu.js b/src/ui/build-menu.js index 9dd1605..dc48359 100644 --- a/src/ui/build-menu.js +++ b/src/ui/build-menu.js @@ -87,7 +87,7 @@ function renderBuildBar() { if (!tip) return; const size = tip.kind === 'machine' ? `${tip.bw}×${tip.bh} blocks` : tip.kind === 'mold' ? `${MOLDS[tip.mold].bw}×${MOLDS[tip.mold].bh} blocks` : '1 block'; pop.innerHTML = `
${pieceIcon(tip.id)}${esc(tip.name)}
${chipCost(pieceCostNow(tip))}${size}${tip.power ? `${tip.power} power` : ''}
-
${esc(tip.desc)}
How to place ${tip.kind === 'block' ? 'Drag for a line, Shift-drag for a rectangle; out of reach becomes ghosts.' : 'Click to place.'}${tip.dirs && tip.kind === 'block' ? ' A dragged line runs the way you drag it; a single click uses R.' : tip.dirs ? ' R turns it.' : pieceUsesWall(tip) ? ' R cycles materials; F opens the material menu.' : ''}
+
${esc(tip.desc)}
How to place ${tip.kind === 'block' ? (lineOnly(tip.code) ? 'Drag for a line' : 'Drag for a line, Shift-drag for a rectangle') + '; out of reach becomes ghosts.' : 'Click to place.'}${tip.dirs && tip.kind === 'block' ? ' A dragged line runs the way you drag it; a single click uses R.' : tip.dirs ? ' R turns it.' : pieceUsesWall(tip) ? ' R cycles materials; F opens the material menu.' : ''}
${tip.filter ? `
New ones will ${esc(filterSentence(GAME.filterCfg))}. F sets materials and mode (or click materials in the bag, or T over one); F on a placed filter changes its whole row.
` : ''}
hold G replace · Ctrl+Z undo · right click take back · middle mouse pick · hold C/X copy/cut · V paste
`; const tile = bar.querySelector(`[data-piece="${tip.id}"]`); if (!tile) { pop.hidden = true; return; } diff --git a/src/ui/notebook.js b/src/ui/notebook.js index 746897d..36c3297 100644 --- a/src/ui/notebook.js +++ b/src/ui/notebook.js @@ -71,7 +71,7 @@ function controlsHtml() { const tools = [ ['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'], ['2 Pneumatic nozzle', "Left drag picks up the selection, or what you click if nothing is selected. Right click blows the selection. Click a bag name or Q/E selects one; Ctrl+click mixes. Hold Ctrl to see everything found. Clear removes the selection. Bars blow alone. Blowing out the last of a material deselects it. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."], - ['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, screw conveyors, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws, Brick presses and Bar racks, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. A diagonal wall drag makes a sealed sloped face, solid below; T flips it. For one wall block, T cycles square and four slope corners. Diagonal belts use cleats to carry pockets up or down. Extra grains roll back. Diagonal filters always screen downhill, whichever way you drag. R leaves a picked sloped filter downhill. Right click takes a piece back. Your bot flies through anything you build.'], + ['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, screw conveyors, filters, grates, gates, pipes, pumps, outlets, Grinders, Saws, Brick presses and Bar racks, machines (fans and silos) and molds. Drag for a line, hold Shift while dragging for a filled rectangle (belts, screw conveyors, filters, shafts and pipes always draw a line). A diagonal wall drag makes a sealed sloped face, solid below; T flips it. For one wall block, T cycles square and four slope corners. Uphill belts use cleats to carry pockets up; downhill they carry like a flat belt. Extra grains roll back. Diagonal filters always screen downhill, whichever way you drag. R leaves a picked sloped filter downhill. Right click takes a piece back. Your bot flies through anything you build.'], ['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'], ['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'], ]; diff --git a/src/ui/previews.js b/src/ui/previews.js index 538b684..b462dd5 100644 --- a/src/ui/previews.js +++ b/src/ui/previews.js @@ -105,7 +105,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) || isSlopeBelt(code) || code === BP_SCREW)) : [[b.bx, b.by]]; + cells = GAME.drag ? (modShift() && !lineOnly(code) ? blockRect(GAME.drag.bx, GAME.drag.by, b.bx, b.by) : blockLine(GAME.drag.bx, GAME.drag.by, b.bx, b.by, isWall(code) || isSlopeBelt(code) || code === BP_SCREW)) : [[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); diff --git a/src/ui/selection.js b/src/ui/selection.js index aba48f8..9681668 100644 --- a/src/ui/selection.js +++ b/src/ui/selection.js @@ -99,7 +99,8 @@ function pieceCostNow(P, m = GAME.wallMat) { function pieceCode(P, end = aimBlock()) { if (P.id === 'belt' || P.filter) { const a = GAME.drag || end; - if (GAME.drag && diagonalDrag(end.bx - a.bx, end.by - a.by) && !(typeof modShift === 'function' && modShift())) + // Shift changes nothing here: belts and filters only ever draw a line + if (GAME.drag && diagonalDrag(end.bx - a.bx, end.by - a.by)) return (P.filter ? BP_FILTER_UP : BP_BELT_UP) + ((end.bx - a.bx) * (end.by - a.by) > 0 ? 1 : 0); if (end.bx === a.bx && end.by === a.by) return GAME.pieceShape && GAME.pieceShape[P.id] || P.code; return P.code; diff --git a/test/play.py b/test/play.py index 874fe3c..db5fbee 100644 --- a/test/play.py +++ b/test/play.py @@ -663,6 +663,12 @@ with sync_playwright() as p: drag(lambda: blk(bbx - 10, bby - 8), lambda: blk(bbx - 8, bby - 6), 4, True) nb2 = ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)') check('Shift-drag fills a 3×3 rectangle', nb2 - nb1 == 9, nb2 - nb1) + ev("GAME.pieceIdx = PIECE_BY_ID.belt.index; for (const [m, n] of PIECE_BY_ID.belt.cost) bag[m] += n * 10; hudDirty = true") + drag(lambda: blk(bbx + 3, bby - 10), lambda: blk(bbx + 5, bby - 8), 4, True) + belts = ev('[blockP.reduce((a, v) => a + isBelt(v), 0), blockP.reduce((a, v) => a + isSlopeBelt(v), 0), ghostBlocks.size]') + check('Shift-drag with a belt still lays a sloped line, not a rectangle', belts == [3, 3, 0], f'{belts}, {ev("GAME.status")}') + ev('doUndo()'); settle(30) + ev("GAME.pieceIdx = PIECE_BY_ID.wall.index; hudDirty = true") r0 = ev('bag[RSTONE]') pg.mouse.move(*blk(bbx + 3, bby - 4)); pg.mouse.down(button='right'); settle(120); pg.mouse.up(button='right'); settle(80) check('right click takes a block back with a refund', ev('bag[RSTONE]') - r0 == 16 and ev('blockP.reduce((a, v) => a + (v === BP_WALL), 0)') == nb2 - 1, ev('bag[RSTONE]') - r0)