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
+4 -4
View File
@@ -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}`;
+2
View File
@@ -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;
+1 -1
View File
@@ -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 => {
+2 -2
View File
@@ -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.'],
+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] : [] : 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
+10 -4
View File
@@ -163,7 +163,10 @@ function openVacuumMenu() {
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">${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-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 = `<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="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') {
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' }, '⬇'));
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.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; }
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); }
@@ -266,5 +272,5 @@ function renderBag() {
row.appendChild(b);
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'));
}