Vacuum: deselect what is blown out, Ctrl + wheel scrolls the bag, Ctrl+click fix (t20)
Blowing out the last of a selected material removes it from the selection, so an emptied selection picks up what you click again. Ctrl + wheel never zooms the page and scrolls the bag list under the mouse. A bag click while Ctrl is held adds to the selection even if the click lacks the Ctrl flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -250,6 +250,11 @@ above, including:
|
||||
(no menu). Selected materials stay selected when they run out, and picking up never selects
|
||||
anything (both reverse the first t20 version). Clear: a button in the bag, clicking the only
|
||||
selected row, or gamepad B.
|
||||
Then, after playing: blowing out the last of a material deselects it again (an empty selection
|
||||
goes back to picking what you click; a selection kept on an empty material got in the way). A
|
||||
material selected before you have any stays selected. Ctrl + wheel scrolls the bag instead of
|
||||
zooming the page, and a click while Ctrl is held adds to the selection even when the click
|
||||
arrives without the Ctrl flag (seen on the owner's Linux setup).
|
||||
|
||||
- **Fog of war also reveals the ground around the bot (t17, 2026-10-24).** About 20 grains in every
|
||||
direction, through rock, fully clear with a soft edge. Line of sight alone hid everything inside
|
||||
|
||||
+3
-3
@@ -404,7 +404,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 (241 lines)
|
||||
### src/ui/selection.js (242 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, toggleBlow, pickFilterUnderCursor, toggleFilterMat, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, openWallPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
|
||||
- variables: lastBagKey
|
||||
@@ -413,12 +413,12 @@ The selected tool and piece: R / Q / E, drill width, filter picker, the selectio
|
||||
Discovery cards, hover info (I) and the cursor label.
|
||||
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel
|
||||
|
||||
### src/ui/input.js (80 lines)
|
||||
### src/ui/input.js (89 lines)
|
||||
Keyboard and mouse listeners.
|
||||
- functions: isTyping
|
||||
- variables: canvas
|
||||
|
||||
### src/ui/actions.js (237 lines)
|
||||
### src/ui/actions.js (239 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
|
||||
|
||||
@@ -4,6 +4,16 @@ 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: t20 follow-ups: deselect when blown out, Ctrl + wheel, Ctrl+click (Claude Code)
|
||||
|
||||
- Blowing the last of a selected material removes it from the selection (`toolTick`); materials
|
||||
selected before you have any stay. Window `wheel` listener: Ctrl + wheel never zooms the page
|
||||
and scrolls the scrollable panel under the mouse (the Ctrl bag list). Bag clicks add when
|
||||
`GAME.ctrl` is set even if the click has no `ctrlKey` (owner had to hold Ctrl+Shift; not
|
||||
reproduced in headless Chromium, which passes ctrlKey).
|
||||
- Files: `src/ui/{actions,input,selection,notebook}.js`, `test/play.py` (229 checks).
|
||||
- Tests: `run_all.py --browser` all passed.
|
||||
|
||||
## 2026-10-24: t20 follow-up: one selection, full list while Ctrl is held (Claude Code, Codex)
|
||||
|
||||
- `GAME.blow` is now the selection for picking up and blowing out (`vacFilter` -2 = the selection).
|
||||
|
||||
@@ -222,6 +222,8 @@ function toolTick() {
|
||||
const list = GAME.blowAll ? blowAllList() : blowList();
|
||||
if (!GAME.blowAll && !list.length) { setStatus('None of the selected materials are in the bag.'); return; }
|
||||
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);
|
||||
// blown down to nothing: it leaves the selection, so an empty selection picks up what you click again
|
||||
for (const m of list) if (bag[m] <= 0 && GAME.blow.delete(m)) { lastBagKey = ''; hudDirty = true; }
|
||||
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}`;
|
||||
|
||||
@@ -68,6 +68,15 @@ canvas.addEventListener('wheel', e => {
|
||||
if (e.shiftKey || e.ctrlKey) setBrush(d);
|
||||
else { SET.zoom = Math.max(2, Math.min(12, SET.zoom + d)); saveSettings(); resizeRender(); R.snapNext = true; }
|
||||
}, { passive: false });
|
||||
// Ctrl + wheel would zoom the browser page (Ctrl shows the full bag list): scroll the list under the mouse instead
|
||||
window.addEventListener('wheel', e => {
|
||||
if (!e.ctrlKey || e.target === canvas) return;
|
||||
e.preventDefault();
|
||||
for (let n = e.target; n && n !== document.body; n = n.parentElement) {
|
||||
const oy = getComputedStyle(n).overflowY;
|
||||
if ((oy === 'auto' || oy === 'scroll') && n.scrollHeight > n.clientHeight) { n.scrollTop += e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? n.clientHeight : 1); break; }
|
||||
}
|
||||
}, { passive: false });
|
||||
window.addEventListener('resize', () => { if (R.canvas) resizeRender(); });
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden && GAME.running) saveWorld('auto', true);
|
||||
|
||||
+1
-1
@@ -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 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. Selections stay when materials run out. Shift while not flying takes everything; Shift + right click blows everything loose in the bag."],
|
||||
['2 Vacuum', "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, 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.'],
|
||||
|
||||
+3
-2
@@ -217,10 +217,11 @@ function renderBag() {
|
||||
const row = el('div', { class: 'bagrow' + (bag[m] > 0 ? '' : ' none') + (sel ? ' on' : '') });
|
||||
const b = el('button', { class: 'pick', title: 'Select ' + 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] > 0 ? bag[m].toLocaleString('en-US') : '–'}</span>`;
|
||||
// Ctrl+click adds to or removes from the selection.
|
||||
// Ctrl+click adds to or removes from the selection. GAME.ctrl too: on some Linux setups the click
|
||||
// arrives without ctrlKey even though the Ctrl keydown (which opened the full list) was seen.
|
||||
b.oncontextmenu = e => e.preventDefault();
|
||||
b.onclick = e => {
|
||||
if (e.ctrlKey || e.metaKey) { toggleBlow(m); if (t !== 'vacuum') selectTool(1); return; }
|
||||
if (e.ctrlKey || e.metaKey || GAME.ctrl) { toggleBlow(m); if (t !== 'vacuum') selectTool(1); 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 { GAME.blow = GAME.blow.size === 1 && GAME.blow.has(m) ? new Set() : new Set([m]); if (t !== 'vacuum') selectTool(1); }
|
||||
|
||||
+7
-4
@@ -12,12 +12,13 @@ report: |
|
||||
what you click (or the first loose grain it reaches). Right click blows the selected materials
|
||||
that are in the bag, as a mix. Click a bag name or Q/E selects one, Ctrl+click adds or removes.
|
||||
Hold Ctrl and the bag also lists everything you've found, under "Not in bag", so you can select
|
||||
materials you don't have yet. Selected materials stay selected when they run out. Clear the
|
||||
materials you don't have yet. Blowing out the last of a material deselects it (one picked
|
||||
before you had any stays selected). Ctrl + wheel scrolls the bag list. Clear the
|
||||
selection with the Clear button in the bag, by clicking the only selected row, or gamepad B.
|
||||
Shift takes or blows everything.
|
||||
To check: hold Ctrl, Ctrl+click two materials you don't have, then sweep a mixed pile (only
|
||||
those come up); blow a coal + ore mix until the coal runs out (ore keeps going, coal stays
|
||||
selected); Clear, then drag on something (only that comes up).
|
||||
those come up); blow a coal + ore mix until the coal runs out (ore keeps going, coal is
|
||||
deselected); Ctrl + wheel over the bag; Clear, then drag on something (only that comes up).
|
||||
---
|
||||
|
||||
# Context
|
||||
@@ -34,5 +35,7 @@ I dont like the current tool. I want to completely rethink how to pick up and dr
|
||||
- [x] Remove the pick-up marks (R, Ctrl+right-click, the "Only marked" chip) and the F vacuum menu
|
||||
- [x] One selection for both: a drag picks up only the selected materials (or what you click when nothing is selected), and right click blows out the selected ones in the bag
|
||||
- [x] Holding Ctrl lists every material found so far in the bag, below a "Not in bag" divider, so several can be selected before any are picked up
|
||||
- [x] Selected materials stay selected when they run out; blowing skips them
|
||||
- [x] Blowing out the last of a selected material deselects it; materials selected before you have any stay selected
|
||||
- [x] An easy way to clear the selection (a Clear button, clicking the only selected row, gamepad B)
|
||||
- [x] Ctrl + wheel over the bag scrolls the list instead of zooming the page
|
||||
- [x] Ctrl+click adds to the selection on setups where the click arrives without the Ctrl flag
|
||||
|
||||
+16
-2
@@ -333,10 +333,12 @@ with sync_playwright() as p:
|
||||
ev('GAME.blow.clear(); lastBagKey = ""; hudDirty = true'); settle(80)
|
||||
pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80)
|
||||
hold(dx_, dy_, 400, 'right'); settle(400)
|
||||
check('blowing the last coal keeps it selected', ev('bag[COAL] === 0 && GAME.blow.has(COAL)'))
|
||||
check('blowing the last coal deselects it', ev('bag[COAL] === 0 && !GAME.blow.size'))
|
||||
check('a selection emptied by blowing picks up what you click again', ev("(() => { GAME.vacFilter = 0; primaryPress(false); const f = GAME.vacFilter; primaryRelease(false); return f !== -2; })()"))
|
||||
check('a material selected before you have any stays selected', ev("(() => { GAME.blow = new Set([COAL]); const n = bagTotal, f = flyers.length; GAME.rmb = true; secondaryPress(false); toolTick(); GAME.rmb = false; return GAME.blow.has(COAL); })()"))
|
||||
check('selected but none in bag blows nothing', ev("(() => { const n = bagTotal, f = flyers.length; GAME.rmb = true; secondaryPress(false); toolTick(); GAME.rmb = false; renderSelCard(); return bagTotal === n && flyers.length === f && GAME.status === 'None of the selected materials are in the bag.' && $('selcard').querySelector('.vacchoice').textContent === 'none in bag'; })()"))
|
||||
check('nothing selected blows nothing and explains how to choose', ev("(() => { GAME.blow.clear(); const n = bagTotal, f = flyers.length; GAME.rmb = true; secondaryPress(false); toolTick(); GAME.rmb = false; return bagTotal === n && flyers.length === f && GAME.status === 'Nothing selected: click or Q/E in the bag.'; })()"))
|
||||
check('a mix keeps exhausted coal selected and continues blowing ore', ev("(() => { const b = bag.slice(), total = bagTotal, f = flyers.length; bag.fill(0); bag[COAL] = 1; bag[CUORE] = 100; bagTotal = 101; GAME.blow = new Set([COAL, CUORE]); GAME.rmb = true; secondaryPress(false); toolTick(); const ore = bag[CUORE]; toolTick(); GAME.rmb = false; const ok = !bag[COAL] && ore > 0 && bag[CUORE] < ore && GAME.blow.size === 2 && GAME.blow.has(COAL) && GAME.blow.has(CUORE); bag.set(b); bagTotal = total; flyers.length = f; return ok; })()"))
|
||||
check('a mix drops exhausted coal and continues blowing ore', ev("(() => { const b = bag.slice(), total = bagTotal, f = flyers.length; bag.fill(0); bag[COAL] = 1; bag[CUORE] = 100; bagTotal = 101; GAME.blow = new Set([COAL, CUORE]); GAME.rmb = true; secondaryPress(false); toolTick(); const ore = bag[CUORE]; toolTick(); GAME.rmb = false; const ok = !bag[COAL] && ore > 0 && bag[CUORE] < ore && GAME.blow.size === 1 && GAME.blow.has(CUORE); bag.set(b); bagTotal = total; flyers.length = f; return ok; })()"))
|
||||
# Shift + right click blows an even mix of everything loose in the bag, without changing the choice
|
||||
refill('bag[COAL] = 60; bag[CUORE] = 60')
|
||||
ev('GAME.blow = new Set([COAL]); lastBagKey = ""; hudDirty = true;')
|
||||
@@ -386,6 +388,11 @@ with sync_playwright() as p:
|
||||
pg.locator('#baglist .none .pick[title="Select Sand"]').click(modifiers=['Control']); settle(80)
|
||||
check('Ctrl click selects a material not in the bag and keeps its row after release', ev("GAME.blow.has(SAND) && !GAME.ctrl && !!$('baglist').querySelector('.bagrow.none.on .pick[title=\"Select Sand\"]')"))
|
||||
check('drag picks the newly selected missing material', ev("(() => { const aim = GAME.aim, x = Math.floor(bot.x)+6, y = Math.floor(bot.y)-6, i = y*W+x, old = [mat[i],temp[i],flags[i]]; setCell(i,SAND,20,0); GAME.aim = {x,y}; GAME.lmb = true; primaryPress(false); toolTick(); primaryRelease(false); GAME.lmb = false; GAME.aim = aim; const ok = bag[SAND] > 0; setCell(i,...old); return ok; })()"))
|
||||
# Ctrl held (keydown seen) but the click itself arrives without ctrlKey, as on some Linux setups: still adds
|
||||
ev("GAME.blow = new Set([COAL]); seen[GRAVEL] = 1; bagTotal -= bag[GRAVEL]; bag[GRAVEL] = 0; window.dispatchEvent(new KeyboardEvent('keydown', {key:'Control', ctrlKey:true})); renderBag()")
|
||||
pg.locator('#baglist .none .pick[title="Select Gravel"]').click(); settle(80)
|
||||
check('a click while Ctrl is held adds even without ctrlKey on the click', ev('GAME.blow.has(COAL) && GAME.blow.has(GRAVEL)'), ev('[...GAME.blow]'))
|
||||
ev("GAME.blow.delete(GRAVEL); GAME.blow.add(SAND)")
|
||||
ev("GAME.blow.delete(SAND); bagTotal -= bag[SAND]; bag[SAND] = 0; window.dispatchEvent(new KeyboardEvent('keyup', {key:'Control'})); renderBag()")
|
||||
check('Ctrl release hides unselected missing materials', ev("""!GAME.ctrl && !$('baglist').querySelector('.bagrow.none .pick[title="Select Sand"]')"""))
|
||||
ev("window.dispatchEvent(new KeyboardEvent('keydown', {key:'Meta', metaKey:true})); renderBag()")
|
||||
@@ -394,6 +401,13 @@ with sync_playwright() as p:
|
||||
check('Meta release hides unselected missing materials', ev('!GAME.ctrl'))
|
||||
ev("window.dispatchEvent(new KeyboardEvent('keydown', {key:'Control', ctrlKey:true})); window.dispatchEvent(new Event('blur')); renderBag()")
|
||||
check('blur clears the modifier', ev('!GAME.ctrl'))
|
||||
# Ctrl + wheel over the bag scrolls the list instead of zooming the page
|
||||
r = ev("""(() => { const b = $('baglist'), mh = b.style.maxHeight; b.style.maxHeight = '40px'; b.scrollTop = 0;
|
||||
const w = new WheelEvent('wheel', { deltaY: 30, ctrlKey: true, bubbles: true, cancelable: true }); b.querySelector('.pick').dispatchEvent(w);
|
||||
const out = [w.defaultPrevented, b.scrollTop]; b.style.maxHeight = mh; b.scrollTop = 0; return out; })()""")
|
||||
check('Ctrl + wheel over the bag scrolls it and never zooms the page', r[0] and r[1] > 0, r)
|
||||
r = ev("(() => { const w = new WheelEvent('wheel', { deltaY: 30, ctrlKey: true, bubbles: true, cancelable: true }); $('selcard').dispatchEvent(w); return w.defaultPrevented; })()")
|
||||
check('Ctrl + wheel over other HUD panels never zooms the page', r)
|
||||
pg.locator('[data-bag-clear]').click(); settle(80)
|
||||
check('Clear clears the selection', ev('!GAME.blow.size'))
|
||||
pg.locator('#baglist .pick').filter(has_text='Coal').click(); settle(80)
|
||||
|
||||
Reference in New Issue
Block a user