Vacuum: pick up what you click, blow out what's in hand (t20)

A drag takes only the material it starts on (Shift: everything). The blow
selection is the hand: click or Q/E for one material, Ctrl+click for a mix.
Empty materials leave the hand, an empty hand blows nothing, and a pickup
fills an empty hand. Removes the pick-up marks and the F vacuum menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 13:34:44 -04:00
co-authored by Claude Opus 5.5
parent 14194cb4be
commit c1a085ebb1
17 changed files with 129 additions and 186 deletions
+11 -57
View File
@@ -23,7 +23,7 @@ function cycleItem(d) {
if (t === 'vacuum') {
const l = bagMaterials();
if (!l.length) return;
const k = l.indexOf(blowList()[0]);
const k = GAME.blow.size ? l.indexOf(blowList()[0]) : d > 0 ? -1 : 0;
GAME.blow = new Set([l[(k + d + l.length) % l.length]]);
} else if (t === 'build') {
let k = BUILD_ORDER.indexOf(piece().id); k = (k + d + BUILD_ORDER.length) % BUILD_ORDER.length;
@@ -48,18 +48,10 @@ function rotate() {
if (pieceUsesWall(P)) cycleWall(1);
else if (P.dirs) GAME.pieceDir[P.id] = ((GAME.pieceDir[P.id] || 0) + 1) % P.dirs.length;
else return;
} else if (t === 'vacuum') { toggleCollectUnderCursor(); return; }
else if (t === 'drill') { tuneUnderCursor(); return; }
} else if (t === 'drill') { tuneUnderCursor(); return; }
else return;
playSfx('ui'); hudDirty = true;
}
function toggleCollect(m) {
if (!m) return;
const add = !GAME.collect.has(m);
if (add) GAME.collect.add(m); else GAME.collect.delete(m);
try { localStorage.setItem('deepgrain.collect', JSON.stringify([...GAME.collect])); } catch (e) { /* storage unavailable */ }
lastBagKey = ''; hudDirty = true; playSfx('ui');
}
function toggleBlow(m) {
if (GAME.blow.has(m)) GAME.blow.delete(m);
else if (PH[m] === PH_ITEM) GAME.blow = new Set([m]);
@@ -80,11 +72,6 @@ function toggleFilterMat(m) {
hudDirty = true; lastBagKey = ''; playSfx('ui');
setStatus(`New filter selection updated.`, 'info');
}
function toggleCollectUnderCursor() {
const m = grainAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
if (!m) { setStatus('Aim at something loose to mark it.', 'info'); return; }
toggleCollect(m);
}
function setBrush(d) {
const t = toolId();
if (t === 'drill') { setDrillWidth(GAME.drillW + d); return; }
@@ -141,37 +128,6 @@ function openFilterPicker(cur, onDone, title) {
box.querySelectorAll('[data-m]').forEach(b => b.onclick = () => { const m = +b.dataset.m; if (sel.has(m)) sel.delete(m); else sel.add(m); playSfx('ui'); sync(); });
sync();
}
function openVacuumMenu() {
ensureBlow();
dialog('Vacuum', '', [
{ label: 'Pick up everything', cls: 'ghost', fn: () => {
GAME.collect.clear();
try { localStorage.setItem('deepgrain.collect', '[]'); } catch (e) { /* storage unavailable */ }
lastBagKey = ''; hudDirty = true; playSfx('ui'); sync(); return true;
} },
{ label: 'Blow one at a time', cls: 'ghost', fn: () => {
GAME.blow = new Set(blowList().slice(0, 1));
lastBagKey = ''; hudDirty = true; playSfx('ui'); sync(); return true;
} },
{ label: 'Done', cls: 'primary' },
]);
const box = $('dlg-body');
function sync() {
const list = BAG_ORDER.filter(m => bag[m] > 0)
.concat(BAG_ORDER.filter(m => !bag[m] && (GAME.collect.has(m) || GAME.blow.has(m))))
.concat(BAG_ORDER.filter(m => !bag[m] && !GAME.collect.has(m) && !GAME.blow.has(m) && seen[m] && filterable(m)));
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>`
+ (!GAME.collect.size ? '<p class="note">Mark materials to pick up only those.</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-blow]').forEach(b => b.onclick = () => { toggleBlow(+b.dataset.vmBlow); sync(); });
}
sync();
}
function openWallPicker() {
const P = piece(), title = P.kind === 'mold' ? `${P.name} material` : 'Wall material';
const list = WALL_LIST.filter(m => bag[m] > 0).concat(WALL_LIST.filter(m => !bag[m]));
@@ -228,13 +184,12 @@ function renderSelCard() {
+ '<div class="sub">Place pieces from the menu below. Drag for a line, <kbd>Shift</kbd>-drag for a box; out of reach becomes ghosts.</div>'
+ '<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 all = blowingAll(), list = all ? blowAllList() : blowList(), marks = [...GAME.collect];
const all = blowingAll(), list = all ? blowAllList() : blowList();
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>${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>`;
h = `<div class="row vacrow"><b>Pick up</b><span>What you click · Shift: everything</span>${sws(GAME.lmb && GAME.vacFilter > 0 ? [GAME.vacFilter] : [])}</div>
<div class="row vacrow"><b>In hand</b><kbd>Q</kbd><span class="name grow vacchoice">${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) : 'empty'}</span><span class="num chip${count ? '' : ' bad'}">${count ? count.toLocaleString('en-US') : 'none'}</span><kbd>E</kbd></div>
<div class="sub">Left picks up what you click, right blows out what's in hand. Shift: everything. Click or Q/E in the bag to change the hand. <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>`;
}
@@ -247,9 +202,9 @@ function renderBag() {
$('bagcap').textContent = `${bagTotal.toLocaleString('en-US')} / ${cap.toLocaleString('en-US')}`;
$('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 l = BAG_ORDER.filter(m => bag[m] > 0 || GAME.blow.has(m));
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;
const key = l.map(m => m + ':' + bag[m] + (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 = '';
@@ -257,11 +212,10 @@ function renderBag() {
for (const m of l) {
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}` });
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>`;
// 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); };
// Ctrl+click adds to or removes from the hand.
b.oncontextmenu = e => e.preventDefault();
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; }
@@ -272,5 +226,5 @@ function renderBag() {
row.appendChild(b);
box.appendChild(row);
}
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'));
if (t === 'vacuum') box.appendChild(el('div', { class: 'baghint' }, 'Click: put in hand · Ctrl+click: add to the mix · Q/E: next'));
}