Vacuum: holding Shift shows the blow-everything mix on the card and in the bag (t19)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 11:59:05 -04:00
co-authored by Claude Opus 5.5
parent 209d065f4e
commit ec687b8c6c
6 changed files with 33 additions and 8 deletions
+2 -2
View File
@@ -395,9 +395,9 @@ 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 (39 lines)
### src/ui/hud.js (40 lines)
The tool hotbar and what is selected.
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, ensureBlow, piece
- functions: renderHotbar, toolId, steering, modShift, grainAt, selectTool, bagMaterials, blowList, blowAllList, blowingAll, ensureBlow, piece
### src/ui/build-menu.js (102 lines)
The build menu along the bottom: categories, piece tiles and their icons.
+12
View File
@@ -4,6 +4,18 @@ 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: t19 Vacuum blow all visual (Claude Code, Codex)
- The owner's description was lost in kandown; chosen in chat: bag rows light up and the card
shows it while Shift is held. t18 was deleted at the owner's request (to decide later: what
blow does when the chosen material runs out).
- `hud.js` `blowingAll()` (Shift held and not flying, or blowing everything); `selection.js` card
("Everything · mix of N", no swatches so it stays one line) and bag `.on` rows; `previews.js`
cursor swatches.
- `test/play.py`: Shift preview on and off, flying doesn't preview (the bot is put back after,
or a later gamepad dig check fails). Tests: `run_all.py --browser` all pass (220 `play.py`
checks).
## 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
+1
View File
@@ -31,6 +31,7 @@ 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)); }
function blowingAll() { return toolId() === 'vacuum' && (GAME.rmb ? !GAME.rmbConsumed && GAME.blowAll : modShift()); }
// Empty materials stay chosen until the player changes the marks.
function ensureBlow() {
if (GAME.blow.size) return;
+1 -1
View File
@@ -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] : [] : GAME.rmb && GAME.blowAll ? blowAllList() : blowList();
const swatches = GAME.lmb ? GAME.vacFilter >= 0 && GAME.collect.size ? [...GAME.collect] : [] : blowingAll() ? 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
+5 -5
View File
@@ -229,11 +229,11 @@ function renderSelCard() {
+ '<div class="sub"><kbd>Q</kbd>/<kbd>E</kbd> piece · <kbd>Shift</kbd>+<kbd>Q</kbd>/<kbd>E</kbd> tab · <kbd>R</kbd> turn or material · <kbd>F</kbd> material menu · hold <kbd>G</kbd> replace · right click take back · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo</div>';
} else if (t === 'vacuum') {
ensureBlow();
const list = blowList(), marks = [...GAME.collect];
const all = blowingAll(), list = all ? blowAllList() : blowList(), marks = [...GAME.collect];
const sws = ms => ms.slice(0, 4).map(m => `<span class="sw" title="${esc(MD[m].name)}" style="background:${rgbOf(m)}"></span>`).join('') + (ms.length > 4 ? `+${ms.length - 4}` : '');
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>
<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>${all ? '' : sws(list)}${all ? list.length ? `Everything · mix of ${list.length}` : 'Everything · nothing loose' : 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 or blows everything. <span class="chip">nozzle ${GAME.brush[1]}</span></div>`;
} 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>`;
@@ -248,14 +248,14 @@ function renderBag() {
$('bagbar').firstChild.style.width = pct + '%';
$('bagbar').classList.toggle('full', bagTotal >= cap);
const l = BAG_ORDER.filter(m => bag[m] > 0 || GAME.collect.has(m) || GAME.blow.has(m));
const t = toolId();
const key = l.map(m => m + ':' + bag[m] + (GAME.collect.has(m) ? 'c' : '') + (GAME.blow.has(m) ? 'b' : '')).join(',') + '|' + GAME.wallMat + '|' + GAME.filterCfg.m.join('.') + '|' + t + '|' + GAME.pieceIdx;
const t = toolId(), all = blowingAll(), blowing = all ? blowAllList() : blowList();
const key = l.map(m => m + ':' + bag[m] + (GAME.collect.has(m) ? 'c' : '') + (GAME.blow.has(m) ? 'b' : '')).join(',') + '|' + GAME.wallMat + '|' + GAME.filterCfg.m.join('.') + '|' + t + '|' + GAME.pieceIdx + '|' + all;
if (key === lastBagKey) return;
lastBagKey = key;
const box = $('baglist'); box.innerHTML = '';
if (!l.length) { box.appendChild(el('div', { class: 'empty' }, 'Empty. Break rock with the Drill, then use the Vacuum.')); return; }
for (const m of l) {
const sel = (t === 'vacuum' && GAME.blow.has(m)) || (t === 'build' && m === GAME.wallMat && pieceUsesWall(piece())) || (t === 'build' && piece().filter && GAME.filterCfg.m.includes(m));
const sel = (t === 'vacuum' && blowing.includes(m)) || (t === 'build' && m === GAME.wallMat && pieceUsesWall(piece())) || (t === 'build' && piece().filter && GAME.filterCfg.m.includes(m));
const row = el('div', { class: 'bagrow' + (sel ? ' on' : '') });
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}` });
+12
View File
@@ -330,6 +330,18 @@ with sync_playwright() as p:
# 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.collect = new Set([COAL, CUORE]); GAME.blow = new Set([COAL]); lastBagKey = ""; hudDirty = true;')
pg.keyboard.down('Shift'); settle(150)
check('Shift previews everything in the blow card', 'Everything · mix of' in pg.inner_text('#selcard'))
check('Shift lights every loose bag row', ev("blowAllList().every(m => [...$('baglist').querySelectorAll('.bagrow.on .nm')].some(e => e.textContent === MD[m].name))"))
pg.screenshot(path=str(out / '03b-blow-all-preview.png'))
pg.keyboard.up('Shift'); settle(150)
check('releasing Shift restores the chosen blow material', ev("$('selcard').querySelector('.vacchoice').textContent.includes(MD[COAL].name) && !$('selcard').querySelector('.vacchoice').textContent.includes('Everything')"))
check('releasing Shift lights only coal', ev("(() => { const rows = [...$('baglist').querySelectorAll('.bagrow.on')]; return rows.length === 1 && rows[0].querySelector('.nm').textContent === MD[COAL].name; })()"))
bot_at = ev('[bot.x, bot.y]')
pg.keyboard.down('d'); pg.keyboard.down('Shift'); settle(150)
check('Shift while flying does not preview everything', 'Everything' not in pg.locator('#selcard .vacchoice').inner_text())
pg.keyboard.up('Shift'); pg.keyboard.up('d')
ev(f'bot.x = {bot_at[0]}; bot.y = {bot_at[1]}; bot.vx = bot.vy = 0;'); settle(150)
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)
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]]'))