diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 38fa730..f65b817 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -230,6 +230,9 @@ above, including: the Vacuum (when nothing in reach can be used) or a click on the card opens a menu with ⬇ Pick up and ⬆ Blow toggles per material; the bag rows lose their buttons. T no longer does anything for the vacuum. The owner found t6's version had too many modes. + Later the same day the owner added: Shift + right click blows an even mix of everything loose in + the bag; Ctrl+click a bag name adds it to the blow mix and Ctrl+right-click to pick-up; the menu + is one line per material because the first version was too large. ## Ideas not built yet diff --git a/docs/PROJECT_MAP.md b/docs/PROJECT_MAP.md index c280d91..61a9d17 100644 --- a/docs/PROJECT_MAP.md +++ b/docs/PROJECT_MAP.md @@ -395,16 +395,16 @@ The notebook (N): milestones, market, wallet, blueprints, controls. - functions: openNotebook, renderNotebook, fmtClock, orderName, orderLeft, renderMarketTab, fmtPrice, itemsText, renderWallet, renderBlueprintTab, copyCode, controlsHtml - variables: nbTab, MARKET_GROUPS -### src/ui/hud.js (37 lines) +### src/ui/hud.js (39 lines) The tool hotbar and what is selected. -- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, ensureBlow, piece +- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, ensureBlow, piece ### src/ui/build-menu.js (102 lines) The build menu along the bottom: categories, piece tiles and their icons. - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu -### src/ui/selection.js (270 lines) +### src/ui/selection.js (276 lines) The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel. - functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleCollect, toggleBlow, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openVacuumMenu, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag - variables: lastBagKey diff --git a/docs/agent-log.md b/docs/agent-log.md index c250451..1a11730 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -4,6 +4,19 @@ 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-24: t16 follow-up: Shift blows all, Ctrl+click in the bag, compact menu (Claude Code) + +- The owner's subtask "shift to blow all" and two chat requests. Codex was out of credits, so + Claude made the edits. +- `input.js`/`actions.js`: `secondaryPress(shift)` sets `GAME.blowAll`; `hud.js` `blowAllList()` + (loose bag materials, no bars); `previews.js` shows it. `selection.js`: Ctrl+click a bag name + toggles blow, Ctrl+right-click toggles pick-up; the vacuum menu is one `.vrow` per material + (CSS in `style.css`). Text in the card, bag hint and `notebook.js`. +- `test/play.py`: Shift blow-all (replaces the old "Shift still blows only coal" check, per the + owner's change), Ctrl+click and Ctrl+right-click, plain right click does nothing, one line per + menu row at 1280 and 900. +- Tests: `run_all.py --browser` all pass (215 `play.py` checks). Screenshots checked. + ## 2026-10-24: t16 Vacuum tool rework, again (Claude Code, Codex) - The owner: too many modes; chose an F menu with two pick-up states. diff --git a/src/styles/style.css b/src/styles/style.css index 484b69e..8cee95c 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -315,14 +315,14 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border- .toast { animation: none; } } /* wall material picker */ -:is(.wpick, .vpick) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; max-height: min(60vh, 490px); overflow-y: auto; padding: 2px; } -:is(.wpick, .vpick) .wp { display: flex; align-items: flex-start; gap: 8px; text-align: left; padding: 8px; border-radius: 6px; border: 1px solid var(--soot-4); background: var(--soot-2); color: var(--bone); font: inherit; font-size: 12px; cursor: pointer; min-width: 0; } -:is(.wpick, .vpick) .wp > span:last-child { min-width: 0; overflow-wrap: anywhere; } -:is(.wpick, .vpick) .wp span span { display: block; color: var(--ash); margin-top: 3px; } -:is(.wpick, .vpick) .wp.short { opacity: 0.55; } -:is(.wpick, .vpick) .wp:hover { background: #2d2724; } -:is(.wpick, .vpick) .wp.on { border-color: #e59a52; background: #33261c; } -:is(.wpick, .vpick) .wp:focus-visible, [data-wall-picker]:focus-visible { outline: 2px solid #e59a52; outline-offset: 1px; } +.wpick { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; max-height: min(60vh, 490px); overflow-y: auto; padding: 2px; } +.wpick .wp { display: flex; align-items: flex-start; gap: 8px; text-align: left; padding: 8px; border-radius: 6px; border: 1px solid var(--soot-4); background: var(--soot-2); color: var(--bone); font: inherit; font-size: 12px; cursor: pointer; min-width: 0; } +.wpick .wp > span:last-child { min-width: 0; overflow-wrap: anywhere; } +.wpick .wp span span { display: block; color: var(--ash); margin-top: 3px; } +.wpick .wp.short { opacity: 0.55; } +.wpick .wp:hover { background: #2d2724; } +.wpick .wp.on { border-color: #e59a52; background: #33261c; } +.wpick .wp:focus-visible, [data-wall-picker]:focus-visible { outline: 2px solid #e59a52; outline-offset: 1px; } [data-wall-picker] { cursor: pointer; } /* filter picker */ .fpick { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 6px; max-height: min(52vh, 420px); overflow-y: auto; padding: 2px; } @@ -362,8 +362,14 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border- #m-dialog { z-index: 21; } #continue-meta { max-width: 72%; overflow-wrap: anywhere; text-align: right; } -.vpick { max-height: min(48vh, 390px); } -.vtoggles { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; } -.vtoggles button { padding: 4px 7px; font-size: 11px; } -.vtoggles button.on { border-color: #e59a52; background: #533820; color: var(--bone); } +.vpick { display: grid; gap: 1px; max-height: min(56vh, 460px); overflow-y: auto; padding: 2px; } +.vrow { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto 52px 52px; align-items: center; gap: 8px; padding: 2px 6px; border-radius: 4px; font-size: 12px; } +.vrow:not(.vhead):hover { background: var(--soot-2); } +.vrow .sw { width: 12px; height: 12px; border-radius: 2px; } +.vrow .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.vrow .num { color: var(--ash); } +.vrow.vhead { color: var(--ash-dim); white-space: nowrap; font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; } +.vrow.vhead span:nth-child(n+4) { text-align: center; } +.vrow button { padding: 1px 0; min-height: 0; font-size: 12px; justify-content: center; } +.vrow button.on { border-color: #e59a52; background: #533820; color: var(--bone); } .vacchoice { display: flex; align-items: center; gap: 4px; text-align: left; min-width: 0; background: transparent; border: 0; padding: 0; cursor: pointer; } diff --git a/src/ui/actions.js b/src/ui/actions.js index 23cd30b..8fd76cc 100644 --- a/src/ui/actions.js +++ b/src/ui/actions.js @@ -188,9 +188,9 @@ function primaryRelease(shift) { } if (t === 'vacuum') { GAME.vacFilter = 0; hudDirty = true; } } -function secondaryPress() { +function secondaryPress(shift) { const t = toolId(); - GAME.rmbSeen = new Set(); + GAME.rmbSeen = new Set(); GAME.blowAll = !!shift; if (GAME.selDrag) { GAME.selDrag = null; hudDirty = true; GAME.rmbConsumed = true; } else if (GAME.paste) { GAME.paste = false; hudDirty = true; playSfx('ui'); GAME.rmbConsumed = true; } else if (t === 'build' && GAME.drag) { GAME.drag = null; GAME.dragDir = -1; GAME.rmbConsumed = true; } @@ -216,8 +216,8 @@ function toolTick() { if (GAME.rmb && !GAME.rmbConsumed) { if (t === 'vacuum') { ensureBlow(); - const list = blowList(); - const r = toolBlow(ax, ay, GAME.brush[1], list[0] || 0, list.length > 1 ? list : null); + const list = GAME.blowAll ? blowAllList() : blowList(); + const r = GAME.blowAll && !list.length ? { placed: 0, status: 'Nothing loose in the bag to blow.' } : toolBlow(ax, ay, GAME.brush[1], list[0] || 0, list.length > 1 ? list : null); if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status); } else if (t === 'build') { const key = `${(ax / BS) | 0},${(ay / BS) | 0}`; diff --git a/src/ui/hud.js b/src/ui/hud.js index ff21aa4..39d70d1 100644 --- a/src/ui/hud.js +++ b/src/ui/hud.js @@ -29,6 +29,8 @@ function selectTool(k) { } function bagMaterials() { return BAG_ORDER.filter(m => bag[m] > 0); } function blowList() { return [...GAME.blow]; } +// Shift + right click: an even mix of everything loose in the bag (bars never mix) +function blowAllList() { return bagMaterials().filter(m => PH[m] !== PH_ITEM && (PH[m] !== PH_LIQUID || player.flask)); } // Empty materials stay chosen until the player changes the marks. function ensureBlow() { if (GAME.blow.size) return; diff --git a/src/ui/input.js b/src/ui/input.js index efbc083..47594d4 100644 --- a/src/ui/input.js +++ b/src/ui/input.js @@ -53,7 +53,7 @@ canvas.addEventListener('mousedown', e => { auInit(); GAME.usingPad = false; if (e.button === 0) { GAME.lmb = true; primaryPress(e.shiftKey && !steering()); } - else if (e.button === 2) { GAME.rmb = true; secondaryPress(); } + else if (e.button === 2) { GAME.rmb = true; secondaryPress(e.shiftKey && !steering()); } else if (e.button === 1) { e.preventDefault(); pickUnderCursor(); } }); window.addEventListener('mouseup', e => { diff --git a/src/ui/notebook.js b/src/ui/notebook.js index 4bdf045..fc19c72 100644 --- a/src/ui/notebook.js +++ b/src/ui/notebook.js @@ -170,7 +170,7 @@ function controlsHtml() { const kb = [ ['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'], ['Tools', '1 – 4'], ['Cycle what you blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'], ['Wall or mold material menu (Build)', 'F'], - ['Mark/unmark what is under the cursor (Vacuum)', 'R'], ['Vacuum menu: what to pick up and blow', 'F (with the Vacuum, away from anything to use)'], ['Nozzle or brush size', '[ ] or Shift + wheel'], + ['Mark/unmark what is under the cursor (Vacuum)', 'R'], ['Vacuum menu: what to pick up and blow', 'F (with the Vacuum, away from anything to use)'], ['Add to blow · to pick up (Vacuum)', 'Ctrl+click · Ctrl+right-click a bag name'], ['Blow everything loose in the bag', 'Shift + right click'], ['Nozzle or brush size', '[ ] or Shift + wheel'], ['Replace what is built (Build)', 'Hold G'], ['Copy a box of what you built', 'Hold C, drag'], ['Cut a box (refunded, paste it back)', 'Hold X, drag'], ['Paste again', 'V'], ['Undo building', 'Ctrl + Z'], ['Blueprint library', 'B'], ['Zoom', 'Mouse wheel'], ['Interact (gates, molds, fans, filters)', 'F'], ['Thermal lens', 'H'], ['Info overlay: filters, belt and tube directions', 'I'], ['Bot lamp', 'L'], ['Pick the piece under the cursor (with its direction and settings)', 'Middle mouse'], ['Notebook', 'N'], ['Clear all blueprint ghosts', 'Delete'], ['Save', 'Ctrl + S'], ['Pause · menu', 'Esc'], ['Mute', 'M'], ]; @@ -181,7 +181,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 Vacuum', 'Left click picks up everything, or only marked materials once you mark some. F opens the vacuum menu to mark what to pick up (⬇) and blow (⬆). R over a grain marks it. Hold Shift while not flying to take everything. Right click blows the ⬆ materials as an even mix, bars one at a time. Click a bag name or use Q/E to blow only one; it stays chosen when empty.'], + ['2 Vacuum', 'Left click picks up everything, or only marked materials once you mark some. F opens the vacuum menu to mark what to pick up (⬇) and blow (⬆). R over a grain marks it. Hold Shift while not flying to take everything. Right click blows the ⬆ materials as an even mix, bars one at a time; Shift + right click blows an even mix of everything loose in the bag. Click a bag name or use Q/E to blow only one; it stays chosen when empty. Ctrl+click a bag name adds it to what you blow; Ctrl+right-click adds it to what you pick up.'], ['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps and outlets, machines (fans, the Refiner) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. 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 ffa34b4..67d3b67 100644 --- a/src/ui/previews.js +++ b/src/ui/previews.js @@ -153,7 +153,7 @@ function drawPreviews() { if (t === 'vacuum') { // what the right button would blow, next to the cursor; what the left is sucking, while it sucks const sz = Math.max(6, zp * 1.6), qx = p.x + (r + 1.2) * zp, qy = p.y - (r + 1.2) * zp; - const swatches = GAME.lmb ? GAME.vacFilter >= 0 && GAME.collect.size ? [...GAME.collect] : [] : blowList(); + const swatches = GAME.lmb ? GAME.vacFilter >= 0 && GAME.collect.size ? [...GAME.collect] : [] : GAME.rmb && GAME.blowAll ? blowAllList() : blowList(); swatches.slice(0, 4).forEach((m, k) => { ctx.fillStyle = rgbOf(m); ctx.fillRect(qx + k * (sz + 2), qy, sz, sz); }); if (blowing || GAME.lmb) { // faint line from the nozzle: the path the grains take diff --git a/src/ui/selection.js b/src/ui/selection.js index f81ec52..f3c5f8b 100644 --- a/src/ui/selection.js +++ b/src/ui/selection.js @@ -163,7 +163,10 @@ function openVacuumMenu() { const blow = blowList(); box.innerHTML = `
${esc(GAME.collect.size ? 'Picking up only ' + matList([...GAME.collect]) + '.' : 'Picking up everything.')} Blowing out ${blow.length > 1 ? `an even mix of ${blow.length}` : blow.length ? esc(MD[blow[0]].name.toLowerCase()) : 'nothing'}.
` + (!GAME.collect.size ? 'Mark materials to pick up only those.
' : '') - + (list.length ? `Find something loose first.
'); + + (list.length ? `Find something loose first.
'); box.querySelectorAll('[data-vm-pick]').forEach(b => b.onclick = () => { toggleCollect(+b.dataset.vmPick); sync(); }); box.querySelectorAll('[data-vm-blow]').forEach(b => b.onclick = () => { toggleBlow(+b.dataset.vmBlow); sync(); }); } @@ -231,7 +234,7 @@ function renderSelCard() { const count = list.reduce((n, m) => n + bag[m], 0); h = `