Vacuum: Shift blows everything, Ctrl+click in the bag, one-line menu rows (t16)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 02:52:56 -04:00
co-authored by Claude Opus 5.5
parent 95b5eeb99d
commit 52a273b656
12 changed files with 88 additions and 38 deletions
+3
View File
@@ -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 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 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. 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 ## Ideas not built yet
+3 -3
View File
@@ -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 - functions: openNotebook, renderNotebook, fmtClock, orderName, orderLeft, renderMarketTab, fmtPrice, itemsText, renderWallet, renderBlueprintTab, copyCode, controlsHtml
- variables: nbTab, MARKET_GROUPS - variables: nbTab, MARKET_GROUPS
### src/ui/hud.js (37 lines) ### src/ui/hud.js (39 lines)
The tool hotbar and what is selected. 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) ### src/ui/build-menu.js (102 lines)
The build menu along the bottom: categories, piece tiles and their icons. The build menu along the bottom: categories, piece tiles and their icons.
- functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar - functions: pieceIcon, catOf, selectBuildCat, cycleBuildCat, buildVariantChips, renderBuildBar, setupBuildBar
- variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu - variables: BUILD_CATS, BUILD_ORDER, PIECE_ICONS, lastBuildKey, lastBuildMenu
### src/ui/selection.js (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. 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 - 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 - variables: lastBagKey
+13
View File
@@ -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 note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-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) ## 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. - The owner: too many modes; chose an F menu with two pick-up states.
+18 -12
View File
@@ -315,14 +315,14 @@ input.text { background: var(--soot-0); border: 1px solid var(--soot-4); border-
.toast { animation: none; } .toast { animation: none; }
} }
/* wall material picker */ /* 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; } .wpick { 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; } .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; }
:is(.wpick, .vpick) .wp > span:last-child { min-width: 0; overflow-wrap: anywhere; } .wpick .wp > span:last-child { min-width: 0; overflow-wrap: anywhere; }
:is(.wpick, .vpick) .wp span span { display: block; color: var(--ash); margin-top: 3px; } .wpick .wp span span { display: block; color: var(--ash); margin-top: 3px; }
:is(.wpick, .vpick) .wp.short { opacity: 0.55; } .wpick .wp.short { opacity: 0.55; }
:is(.wpick, .vpick) .wp:hover { background: #2d2724; } .wpick .wp:hover { background: #2d2724; }
:is(.wpick, .vpick) .wp.on { border-color: #e59a52; background: #33261c; } .wpick .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 .wp:focus-visible, [data-wall-picker]:focus-visible { outline: 2px solid #e59a52; outline-offset: 1px; }
[data-wall-picker] { cursor: pointer; } [data-wall-picker] { cursor: pointer; }
/* filter picker */ /* 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; } .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; } #m-dialog { z-index: 21; }
#continue-meta { max-width: 72%; overflow-wrap: anywhere; text-align: right; } #continue-meta { max-width: 72%; overflow-wrap: anywhere; text-align: right; }
.vpick { max-height: min(48vh, 390px); } .vpick { display: grid; gap: 1px; max-height: min(56vh, 460px); overflow-y: auto; padding: 2px; }
.vtoggles { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; } .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; }
.vtoggles button { padding: 4px 7px; font-size: 11px; } .vrow:not(.vhead):hover { background: var(--soot-2); }
.vtoggles button.on { border-color: #e59a52; background: #533820; color: var(--bone); } .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; } .vacchoice { display: flex; align-items: center; gap: 4px; text-align: left; min-width: 0; background: transparent; border: 0; padding: 0; cursor: pointer; }
+4 -4
View File
@@ -188,9 +188,9 @@ function primaryRelease(shift) {
} }
if (t === 'vacuum') { GAME.vacFilter = 0; hudDirty = true; } if (t === 'vacuum') { GAME.vacFilter = 0; hudDirty = true; }
} }
function secondaryPress() { function secondaryPress(shift) {
const t = toolId(); 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; } 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 (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; } 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 (GAME.rmb && !GAME.rmbConsumed) {
if (t === 'vacuum') { if (t === 'vacuum') {
ensureBlow(); ensureBlow();
const list = blowList(); const list = GAME.blowAll ? blowAllList() : blowList();
const r = toolBlow(ax, ay, GAME.brush[1], list[0] || 0, list.length > 1 ? list : null); 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); if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status);
} else if (t === 'build') { } else if (t === 'build') {
const key = `${(ax / BS) | 0},${(ay / BS) | 0}`; const key = `${(ax / BS) | 0},${(ay / BS) | 0}`;
+2
View File
@@ -29,6 +29,8 @@ function selectTool(k) {
} }
function bagMaterials() { return BAG_ORDER.filter(m => bag[m] > 0); } function bagMaterials() { return BAG_ORDER.filter(m => bag[m] > 0); }
function blowList() { return [...GAME.blow]; } 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. // Empty materials stay chosen until the player changes the marks.
function ensureBlow() { function ensureBlow() {
if (GAME.blow.size) return; if (GAME.blow.size) return;
+1 -1
View File
@@ -53,7 +53,7 @@ canvas.addEventListener('mousedown', e => {
auInit(); auInit();
GAME.usingPad = false; GAME.usingPad = false;
if (e.button === 0) { GAME.lmb = true; primaryPress(e.shiftKey && !steering()); } 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(); } else if (e.button === 1) { e.preventDefault(); pickUnderCursor(); }
}); });
window.addEventListener('mouseup', e => { window.addEventListener('mouseup', e => {
+2 -2
View File
@@ -170,7 +170,7 @@ function controlsHtml() {
const kb = [ 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'], ['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'], ['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'], ['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'], ['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 = [ const tools = [
['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'], ['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'],
['2 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.'], ['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.'], ['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'],
['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'], ['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'],
+1 -1
View File
@@ -153,7 +153,7 @@ function drawPreviews() {
if (t === 'vacuum') { if (t === 'vacuum') {
// what the right button would blow, next to the cursor; what the left is sucking, while it sucks // 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 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); }); swatches.slice(0, 4).forEach((m, k) => { ctx.fillStyle = rgbOf(m); ctx.fillRect(qx + k * (sz + 2), qy, sz, sz); });
if (blowing || GAME.lmb) { if (blowing || GAME.lmb) {
// faint line from the nozzle: the path the grains take // faint line from the nozzle: the path the grains take
+10 -4
View File
@@ -163,7 +163,10 @@ function openVacuumMenu() {
const blow = blowList(); const blow = blowList();
box.innerHTML = `<p class="note" id="vm-sum">${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'}.</p>` box.innerHTML = `<p class="note" id="vm-sum">${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'}.</p>`
+ (!GAME.collect.size ? '<p class="note">Mark materials to pick up only those.</p>' : '') + (!GAME.collect.size ? '<p class="note">Mark materials to pick up only those.</p>' : '')
+ (list.length ? `<div class="vpick">${list.map(m => `<div class="wp"><span class="sw" style="background:${rgbOf(m)}"></span><span><b>${esc(MD[m].name)}</b><span>${bag[m] ? `you have ${bag[m].toLocaleString('en-US')}` : 'none in bag'}</span><div class="vtoggles"><button data-vm-pick="${m}" aria-pressed="${GAME.collect.has(m)}" class="btn ${GAME.collect.has(m) ? 'on' : ''}">⬇ Pick up</button><button data-vm-blow="${m}" aria-pressed="${GAME.blow.has(m)}" class="btn ${GAME.blow.has(m) ? 'on' : ''}"${!bag[m] && !GAME.blow.has(m) ? ' disabled' : ''}>⬆ Blow</button></div></span></div>`).join('')}</div>` : '<p class="note">Find something loose first.</p>'); + (list.length ? `<div class="vpick"><div class="vrow vhead"><span></span><span></span><span></span><span>Pick up</span><span>Blow</span></div>${list.map(m => {
const n = esc(MD[m].name);
return `<div class="vrow"><span class="sw" style="background:${rgbOf(m)}"></span><span class="nm">${n}</span><span class="num">${bag[m] ? bag[m].toLocaleString('en-US') : 'none'}</span><button data-vm-pick="${m}" aria-pressed="${GAME.collect.has(m)}" aria-label="Pick up ${n}" title="Pick up ${n}" class="btn${GAME.collect.has(m) ? ' on' : ''}">⬇</button><button data-vm-blow="${m}" aria-pressed="${GAME.blow.has(m)}" aria-label="Blow out ${n}" title="Blow out ${n}" class="btn${GAME.blow.has(m) ? ' on' : ''}"${!bag[m] && !GAME.blow.has(m) ? ' disabled' : ''}>⬆</button></div>`;
}).join('')}</div>` : '<p class="note">Find something loose first.</p>');
box.querySelectorAll('[data-vm-pick]').forEach(b => b.onclick = () => { toggleCollect(+b.dataset.vmPick); sync(); }); 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(); }); 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); const count = list.reduce((n, m) => n + bag[m], 0);
h = `<div class="row vacrow"><b>Pick up</b><button class="chip hot" data-vac-menu>${marks.length ? 'Only marked' : 'Everything'} · F</button>${sws(marks)}</div> h = `<div class="row vacrow"><b>Pick up</b><button class="chip hot" data-vac-menu>${marks.length ? 'Only marked' : 'Everything'} · F</button>${sws(marks)}</div>
<div class="row vacrow"><b>Blow out</b><kbd>Q</kbd><button class="name grow vacchoice" data-vac-menu>${sws(list)}${list.length > 1 ? `Mix of ${list.length}` : list.length ? esc(MD[list[0]].name) : 'bag empty'}</button><span class="num chip${count ? '' : ' bad'}">${count ? count.toLocaleString('en-US') : 'none left'}</span><kbd>E</kbd></div> <div class="row vacrow"><b>Blow out</b><kbd>Q</kbd><button class="name grow vacchoice" data-vac-menu>${sws(list)}${list.length > 1 ? `Mix of ${list.length}` : list.length ? esc(MD[list[0]].name) : 'bag empty'}</button><span class="num chip${count ? '' : ' bad'}">${count ? count.toLocaleString('en-US') : 'none left'}</span><kbd>E</kbd></div>
<div class="sub">Left picks up, right blows out. F chooses what. R marks what is under the cursor. Shift takes everything. <span class="chip">nozzle ${GAME.brush[1]}</span></div>`; <div class="sub">Left picks up, right blows out. F chooses what. R marks what is under the cursor. Shift takes or blows everything. <span class="chip">nozzle ${GAME.brush[1]}</span></div>`;
} else if (t === 'ignite') { } else if (t === 'ignite') {
h = `<div class="row"><span class="name">Igniter</span><span class="chip hot">up to 640°C</span></div><div class="sub">Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.</div>`; h = `<div class="row"><span class="name">Igniter</span><span class="chip hot">up to 640°C</span></div><div class="sub">Hold on fuel to light it. Coal catches around 420°C, wood around 300°C. Fuel needs open air next to it.</div>`;
} }
@@ -257,7 +260,10 @@ function renderBag() {
if (t === 'vacuum' && GAME.collect.has(m)) row.appendChild(el('span', { class: 'tag', title: 'Marked to pick up', 'aria-label': 'Marked to pick up' }, '⬇')); if (t === 'vacuum' && GAME.collect.has(m)) row.appendChild(el('span', { class: 'tag', title: 'Marked to pick up', 'aria-label': 'Marked to pick up' }, '⬇'));
const b = el('button', { class: 'pick', title: WALL_OF[m] && t === 'build' ? `Build with ${MD[m].name}` : `Blow out ${MD[m].name}` }); const b = el('button', { class: 'pick', title: WALL_OF[m] && t === 'build' ? `Build with ${MD[m].name}` : `Blow out ${MD[m].name}` });
b.innerHTML = `<span class="sw" style="background:${rgbOf(m)}"></span><span class="nm">${esc(MD[m].name)}</span><span class="n num">${bag[m].toLocaleString('en-US')}</span>`; b.innerHTML = `<span class="sw" style="background:${rgbOf(m)}"></span><span class="nm">${esc(MD[m].name)}</span><span class="n num">${bag[m].toLocaleString('en-US')}</span>`;
b.onclick = () => { // Ctrl+click adds to (or removes from) what you blow; Ctrl+right-click, what you pick up
b.oncontextmenu = e => { e.preventDefault(); if (!(e.ctrlKey || e.metaKey)) return; toggleCollect(m); if (t !== 'vacuum') selectTool(1); };
b.onclick = e => {
if (e.ctrlKey || e.metaKey) { toggleBlow(m); if (t !== 'vacuum') selectTool(1); return; }
if (t === 'build' && piece().filter && filterable(m)) { toggleFilterMat(m); return; } if (t === 'build' && piece().filter && filterable(m)) { toggleFilterMat(m); return; }
else if (t === 'build' && WALL_OF[m]) { GAME.wallMat = m; if (!pieceUsesWall(piece())) GAME.pieceIdx = 0; } else if (t === 'build' && WALL_OF[m]) { GAME.wallMat = m; if (!pieceUsesWall(piece())) GAME.pieceIdx = 0; }
else { GAME.blow = new Set([m]); if (t !== 'vacuum') selectTool(1); } else { GAME.blow = new Set([m]); if (t !== 'vacuum') selectTool(1); }
@@ -266,5 +272,5 @@ function renderBag() {
row.appendChild(b); row.appendChild(b);
box.appendChild(row); box.appendChild(row);
} }
if (t === 'vacuum') box.appendChild(el('div', { class: 'baghint' }, 'F with the Vacuum: choose what to pick up and blow')); if (t === 'vacuum') box.appendChild(el('div', { class: 'baghint' }, 'Click: blow only this · Ctrl+click: add to blow · Ctrl+right-click: add to pick up · F: vacuum menu'));
} }
+15 -9
View File
@@ -4,18 +4,18 @@ status: Review
order: 2 order: 2
priority: P3 priority: P3
created: 2026-10-24 created: 2026-10-24
updated: 2026-10-24T06:28:51Z updated: 2026-10-24T06:36:11Z
title: vacuum tool rework, again title: vacuum tool rework, again
report: | report: |
(agent) Fewer modes. Left click picks up everything, or only what you've marked once you mark (agent) Fewer modes. Left click picks up everything, or only what you've marked once you mark
something; Shift takes everything for one click. F with the Vacuum (when nothing in reach can be something; Shift takes everything for one click. F with the Vacuum (when nothing in reach can be
used, like a mold or gate) opens the Vacuum menu: a tile per material with ⬇ Pick up and ⬆ Blow, used, like a mold or gate) opens the Vacuum menu: one line per material with ⬇ (pick up) and ⬆
plus Pick up everything and Blow one at a time. Clicking the Pick up / Blow out chips on the card (blow), plus Pick up everything and Blow one at a time. Clicking the Pick up / Blow out chips on
opens it too. The bag rows no longer have buttons (a small ⬇ shows what's marked); clicking a the card opens it too. Bag: click a name to blow only it, Ctrl+click to add it to the blow mix,
name still blows only that, Q/E still cycles, R still marks under the cursor. T does nothing Ctrl+right-click to add it to pick-up (a small ⬇ shows what's marked). Shift + right click blows
for the vacuum now. an even mix of everything loose in the bag. Q/E still cycles, R still marks under the cursor.
To check: press F with the Vacuum out in the open, mark coal and copper ore to blow, blow a mix; To check: Ctrl+click coal and copper ore in the bag and blow a mix; Ctrl+right-click copper ore
mark copper ore to pick up and vacuum a mixed pile; Pick up everything clears the marks. and vacuum a mixed pile; Shift + right click to blow everything; press F for the compact menu.
--- ---
## Context ## Context
@@ -26,4 +26,10 @@ report: |
- [x] Pick-up: everything unless something is marked (no mode variable) - [x] Pick-up: everything unless something is marked (no mode variable)
- [x] Vacuum menu on F and on the card chips; bag row buttons removed - [x] Vacuum menu on F and on the card chips; bag row buttons removed
- [x] Text, tests and screenshots updated - [x] Text, tests and screenshots up
- [x] shift to blow all
report: Shift + right click (while not flying) blows an even mix of everything loose in the bag; bars are left out. What you've chosen to blow doesn't change.
- [x] Ctrl+click bag items to select several for vac and blow (owner, in chat)
report: Ctrl+click a bag name adds it to (or takes it out of) what you blow; Ctrl+right-click does the same for pick-up. Plain click still blows only that one.
- [x] Compact the F menu: one line per item (owner, in chat)
report: One row per material: swatch, name, count, a ⬇ and a ⬆ button under Pick up / Blow headings.
+16 -2
View File
@@ -327,15 +327,28 @@ with sync_playwright() as p:
refill('bag[COAL] = 3') refill('bag[COAL] = 3')
hold(dx_, dy_, 400, 'right'); settle(400) hold(dx_, dy_, 400, 'right'); settle(400)
check('chosen material stays chosen after it runs out', ev('bag[COAL] === 0 && GAME.blow.size === 1 && GAME.blow.has(COAL)'), ev('[bag[COAL], ...GAME.blow]')) check('chosen material stays chosen after it runs out', ev('bag[COAL] === 0 && GAME.blow.size === 1 && GAME.blow.has(COAL)'), ev('[bag[COAL], ...GAME.blow]'))
# mark coal and ore: Shift no longer changes what is blown, the selection does # Shift + right click blows an even mix of everything loose in the bag, without changing the choice
refill('bag[COAL] = 60; bag[CUORE] = 60') refill('bag[COAL] = 60; bag[CUORE] = 60')
ev('GAME.collect = new Set([COAL, CUORE]); GAME.blow = new Set([COAL]); lastBagKey = ""; hudDirty = true;') ev('GAME.collect = new Set([COAL, CUORE]); GAME.blow = new Set([COAL]); lastBagKey = ""; hudDirty = true;')
mx_, my_ = cell_to_screen(bx - 26, gy - 6) mx_, my_ = cell_to_screen(bx - 26, gy - 6)
pg.mouse.move(mx_, my_); pg.keyboard.down('Shift'); pg.mouse.down(button='right'); settle(400); pg.mouse.up(button='right'); pg.keyboard.up('Shift'); settle(600) pg.mouse.move(mx_, my_); pg.keyboard.down('Shift'); pg.mouse.down(button='right'); settle(400); pg.mouse.up(button='right'); pg.keyboard.up('Shift'); settle(600)
check('Shift + right click still blows only the chosen coal', ev('bag[CUORE]') == 60 and ev('bag[COAL]') < 60, ev('[bag[COAL], bag[CUORE]]')) check('Shift + right click blows everything loose in the bag', ev('bag[CUORE]') < 60 and ev('bag[COAL]') < 60 and ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'), ev('[bag[COAL], bag[CUORE]]'))
refill('bag[COAL] = 60; bag[CUORE] = 60'); settle(200) refill('bag[COAL] = 60; bag[CUORE] = 60'); settle(200)
pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80) pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80)
check('clicking a bag name chooses one blow material', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)')) check('clicking a bag name chooses one blow material', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
ore_row = pg.locator('#baglist .pick').filter(has_text='Copper ore')
ore_row.click(modifiers=['Control']); settle(80)
check('Ctrl+click adds a bag item to the blow mix', ev('GAME.blow.size === 2 && GAME.blow.has(COAL) && GAME.blow.has(CUORE)') and 'Mix of 2' in pg.inner_text('#selcard'))
ore_row.click(modifiers=['Control']); settle(80)
check('Ctrl+click again takes it out of the mix', ev('GAME.blow.size === 1 && GAME.blow.has(COAL)'))
ev('GAME.collect = new Set(); lastBagKey = ""; hudDirty = true'); settle(80)
pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right', modifiers=['Control']); settle(80)
check('Ctrl+right-click marks a bag item for pick-up', ev('GAME.collect.size === 1 && GAME.collect.has(CUORE) && GAME.blow.size === 1'))
pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right', modifiers=['Control']); settle(80)
check('Ctrl+right-click again unmarks it', ev('!GAME.collect.size'))
pg.locator('#baglist .pick').filter(has_text='Copper ore').click(button='right'); settle(80)
check('a plain right click on the bag changes nothing', ev('!GAME.collect.size && GAME.blow.size === 1 && GAME.blow.has(COAL)'))
ev('GAME.collect = new Set([COAL, CUORE]); lastBagKey = ""; hudDirty = true'); settle(80)
pick_before = ev('[...GAME.collect]') pick_before = ev('[...GAME.collect]')
ev('GAME.blow = new Set([RSTONE]); hudDirty = true') ev('GAME.blow = new Set([RSTONE]); hudDirty = true')
pg.click('#selcard [data-vac-menu]'); settle(80) pg.click('#selcard [data-vac-menu]'); settle(80)
@@ -368,6 +381,7 @@ with sync_playwright() as p:
pg.click('#selcard [data-vac-menu]'); settle(80) pg.click('#selcard [data-vac-menu]'); settle(80)
pg.screenshot(path=str(out / f'vacuum-menu{suffix}.png')) pg.screenshot(path=str(out / f'vacuum-menu{suffix}.png'))
check(f'vacuum menu fits at {width}', ev("(() => { const e = $('dlg-body'), r = e.closest('.plate').getBoundingClientRect(); return e.scrollWidth <= e.clientWidth && document.querySelector('.vpick').scrollWidth <= document.querySelector('.vpick').clientWidth && r.left >= 0 && r.right <= innerWidth && r.bottom <= innerHeight; })()")) check(f'vacuum menu fits at {width}', ev("(() => { const e = $('dlg-body'), r = e.closest('.plate').getBoundingClientRect(); return e.scrollWidth <= e.clientWidth && document.querySelector('.vpick').scrollWidth <= document.querySelector('.vpick').clientWidth && r.left >= 0 && r.right <= innerWidth && r.bottom <= innerHeight; })()"))
check(f'vacuum menu has one line per material at {width}', ev("(() => { const rows = [...document.querySelectorAll('.vpick .vrow:not(.vhead)')]; return rows.length > 3 && rows.every(r => r.getBoundingClientRect().height <= 28); })()"))
pg.get_by_role('button', name='Blow one at a time', exact=True).click(); settle(80) pg.get_by_role('button', name='Blow one at a time', exact=True).click(); settle(80)
check(f'Blow one at a time keeps menu open at {width}', ev("GAME.blow.size === 1 && !$('m-dialog').hidden")) check(f'Blow one at a time keeps menu open at {width}', ev("GAME.blow.size === 1 && !$('m-dialog').hidden"))
pg.get_by_role('button', name='Done', exact=True).click(); settle(80) pg.get_by_role('button', name='Done', exact=True).click(); settle(80)