Files
deepgrain-foundry/src/ui/build-menu.js
T
44r0n7andClaude Opus 5.5 51bdd00a9f 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>
2026-10-24 13:34:44 -04:00

103 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// The build menu along the bottom: categories, piece tiles and their icons.
// ---------- Build menu ----------
const BUILD_CATS = [
['Structure', ['wall', 'grate', 'gate']],
['Moving', ['belt', 'filter', 'tube']],
['Liquids', ['pipe', 'pump', 'outlet']],
['Machines', ['fan', 'refiner', 'cutter', 'silo']],
['Molds', ['ingotmold', 'bitmold', 'tankmold']],
];
const BUILD_ORDER = BUILD_CATS.flatMap(c => c[1]);
const PIECE_ICONS = {
wall: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 12h18M9 5v7M15 12v7"/>',
grate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M7.5 5v14M12 5v14M16.5 5v14"/>',
gate: '<rect x="3" y="5" width="18" height="14" rx="1"/><path d="M9 5v14"/><path d="M12 12h6M16 9.5l2.5 2.5-2.5 2.5"/>',
belt: '<rect x="2" y="9" width="20" height="8" rx="2"/><circle cx="7" cy="13" r="2"/><circle cx="17" cy="13" r="2"/><path d="M9 5h7M14 3l2 2-2 2"/>',
filter: '<rect x="2" y="10" width="20" height="8" rx="2"/><path d="M7 10v8M12 10v8M17 10v8M2 14h20"/><path d="M12 2v5M10 5l2 2 2-2"/>',
tube: '<rect x="3" y="6" width="18" height="12" rx="2"/><path d="M6.5 18l3.5-12M11.5 18l3.5-12M16.5 18l3-10"/>',
pipe: '<path d="M2 10h20M2 14h20"/><path d="M6 7.5v9M18 7.5v9"/>',
pump: '<circle cx="11" cy="14" r="6"/><path d="M11 14l3.5-2.5M11 14l-1 4.2M11 14l-4-1.2"/><path d="M15.5 9.5L20 5M16 5h4v4"/>',
outlet: '<path d="M2 7h12a4 4 0 0 1 4 4v3"/><path d="M2 11h10a2 2 0 0 1 2 2v1"/><path d="M15 18v1.5M18 20v1.5M12 20v1.5"/>',
fan: '<circle cx="12" cy="12" r="1.8"/><path d="M12 10.2c-.4-3.6 1.8-6 4-4.8 2 1.2.4 3.8-4 4.8zM13.8 12c3.6-.4 6 1.8 4.8 4-1.2 2-3.8.4-4.8-4zM12 13.8c.4 3.6-1.8 6-4 4.8-2-1.2-.4-3.8 4-4.8zM10.2 12c-3.6.4-6-1.8-4.8-4 1.2-2 3.8-.4 4.8 4z"/>',
refiner: '<path d="M3 4h18l-5 7H8z"/><circle cx="9" cy="15.5" r="2.8"/><circle cx="15" cy="15.5" r="2.8"/><path d="M12 20v2"/>',
cutter: '<path d="M5 3h14l-3 5H8z"/><circle cx="12" cy="15" r="5"/><circle cx="12" cy="15" r="1.2"/><path d="M12 9.2v1.6M17.8 15h-1.6M12 20.8v-1.6M6.2 15h1.6"/>',
silo: '<path d="M6 5.5h12v11H6z"/><path d="M6 5.5l1-2h10l1 2M6 16.5l4 3h4l4-3M7 16.5v5M17 16.5v5"/><path d="M15.5 8v6"/>',
ingotmold: '<path d="M3 9h18l-2 7H5z"/><path d="M9.2 9l.8 7M14.8 9l-.8 7"/>',
bitmold: '<path d="M3 9h10l7 3-7 3H3z"/><path d="M6.5 9v6M9.5 9v6"/>',
tankmold: '<rect x="5" y="7" width="14" height="13" rx="3"/><path d="M9 4h6v3M8.5 12h7"/>',
};
function pieceIcon(id) { return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true">${PIECE_ICONS[id] || ''}</svg>`; }
function catOf(id) { const c = BUILD_CATS.findIndex(([, ids]) => ids.includes(id)); return c < 0 ? 0 : c; }
let lastBuildKey = '', lastBuildMenu = '';
function selectBuildCat(c) {
GAME.buildCat = c;
const ids = BUILD_CATS[c][1], keep = GAME.catPick && GAME.catPick[c];
GAME.pieceIdx = PIECE_BY_ID[ids.includes(keep) ? keep : ids[0]].index;
GAME.catPick = GAME.catPick || {}; GAME.catPick[c] = piece().id;
GAME.drag = null; hudDirty = true; lastBuildKey = ''; playSfx('ui');
}
function cycleBuildCat(d) {
const c = GAME.buildCat != null ? GAME.buildCat : catOf(piece().id);
selectBuildCat((c + d + BUILD_CATS.length) % BUILD_CATS.length);
}
function buildVariantChips(P) {
const dir = P.dirs ? P.dirs[GAME.pieceDir[P.id] || 0] : null;
// while dragging a line of belts, filters or tubes, the direction is the drag's
const dragging = GAME.drag && GAME.dragDir >= 0 && P.dirs;
const glyph = d => ({ left: '←', right: '→', up: '↑', down: '↓' })[d] || '';
const dirChip = !P.dirs ? '' : dragging ? `<span class="chip ok">${glyph(P.dirs[GAME.dragDir])} ${esc(P.dirs[GAME.dragDir])} · from your drag</span>` : `<span class="chip hot">${glyph(dir)} ${esc(dir)} · R</span>`;
const fc = GAME.filterCfg;
const sws = ms => ms.slice(0, 6).map(m => `<span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:2px;background:${rgbOf(m)}"></span>`).join('') + (ms.length > 6 ? `+${ms.length - 6}` : '');
return pieceUsesWall(P) ? `<button class="chip hot" data-wall-picker><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>${esc(MD[WALL_OF[GAME.wallMat]].name.toLowerCase())} · R · F</button>`
: P.filter ? `<span class="chip hot">${fc.inv ? 'carries' : 'drops'} ${fc.m.length ? sws(fc.m) : 'nothing'} · F</span>${dirChip}`
: dir ? dirChip : '';
}
function renderBuildBar() {
const bar = $('buildbar'); if (!bar) return;
const show = GAME.running && toolId() === 'build' && !GAME.paste && !GAME.selDrag && !selModeHeld();
if (bar.hidden === show) { bar.hidden = !show; $('hud').classList.toggle('building', show); }
if (!show) { lastBuildKey = ''; return; }
const P = piece(), cat = GAME.buildCat != null ? GAME.buildCat : catOf(P.id), hov = GAME.bbHover;
const ids = BUILD_CATS[cat][1], tip = PIECE_BY_ID[GAME.usingPad ? P.id : hov && ids.includes(hov) ? hov : ''];
const afford = ids.map(id => canAfford(pieceCostNow(PIECE_BY_ID[id]), 1) ? 1 : 0).join('');
const key = [cat, P.id, tip && tip.id, afford, GAME.wallMat, Math.round(money), GAME.pieceDir[P.id], GAME.filterCfg.inv, GAME.filterCfg.m.join('.'), innerWidth, innerHeight, SET.uiScale].join('|');
if (key === lastBuildKey) return;
lastBuildKey = key;
let h = '<div class="bb-head">' + BUILD_CATS.map(([name], k) => `<button class="bb-tab${k === cat ? ' on' : ''}" data-cat="${k}">${esc(name)}</button>`).join('')
+ '<span class="bb-keys"><kbd>Q</kbd>/<kbd>E</kbd> cycle · <kbd>Shift</kbd>+<kbd>Q</kbd>/<kbd>E</kbd> tab</span></div><div class="bb-tiles">';
for (const id of ids) {
const Q = PIECE_BY_ID[id], cost = pieceCostNow(Q), ok = canAfford(cost, 1);
const sw = Q.variants === 'wall' ? `<span class="sw" style="background:${rgbOf(WALL_OF[GAME.wallMat])}"></span>` : '';
h += `<button class="bb-tile${id === P.id ? ' on' : ''}" data-piece="${id}" aria-pressed="${id === P.id}" aria-label="${esc(Q.name)}">${sw}${pieceIcon(id)}<span class="n">${esc(Q.name)}</span><span class="c${ok ? '' : ' bad'}">${esc(costText(cost, 1) || 'free')}</span></button>`;
}
h += '</div>';
if (!bar.querySelector('.bb-menu')) bar.innerHTML = '<div class="bb-menu"></div><div id="bb-pop" hidden></div>';
const menu = bar.querySelector('.bb-menu');
if (lastBuildMenu !== h) { menu.innerHTML = h; lastBuildMenu = h; }
const pop = $('bb-pop'); pop.hidden = !tip;
if (!tip) return;
const size = tip.kind === 'machine' ? `${tip.bw}×${tip.bh} blocks` : tip.kind === 'mold' ? `${MOLDS[tip.mold].bw}×${MOLDS[tip.mold].bh} blocks` : '1 block';
pop.innerHTML = `<div class="bb-title">${pieceIcon(tip.id)}<b>${esc(tip.name)}</b></div><div class="bb-chips">${chipCost(pieceCostNow(tip))}<span class="chip">${size}</span>${tip.power ? `<span class="chip">${tip.power} power</span>` : ''}</div>
<div class="bb-desc">${esc(tip.desc)}</div><div class="bb-place"><b>How to place</b> ${tip.kind === 'block' ? 'Drag for a line, Shift-drag for a rectangle; out of reach becomes ghosts.' : 'Click to place.'}${tip.dirs && tip.kind === 'block' ? ' A dragged line runs the way you drag it; a single click uses <kbd>R</kbd>.' : tip.dirs ? ' <kbd>R</kbd> turns it.' : pieceUsesWall(tip) ? ' <kbd>R</kbd> cycles materials; <kbd>F</kbd> opens the material menu.' : ''}</div>
${tip.filter ? `<div class="bb-place">New ones will ${esc(filterSentence(GAME.filterCfg))}. <kbd>F</kbd> sets materials and mode (or click materials in the bag, or <kbd>T</kbd> over one); <kbd>F</kbd> on a placed filter changes its whole row.</div>` : ''}
<div class="bb-place">hold <kbd>G</kbd> replace · <kbd>Ctrl</kbd>+<kbd>Z</kbd> undo · right click take back · middle mouse pick · hold <kbd>C</kbd>/<kbd>X</kbd> copy/cut · <kbd>V</kbd> paste</div>`;
const tile = bar.querySelector(`[data-piece="${tip.id}"]`); if (!tile) { pop.hidden = true; return; }
const r = tile.getBoundingClientRect(), br = bar.getBoundingClientRect(), w = pop.getBoundingClientRect().width;
pop.style.left = `${Math.max(8, Math.min(innerWidth - w - 8, r.left + r.width / 2 - w / 2)) - br.left}px`;
}
function setupBuildBar() {
$('selcard').addEventListener('click', e => { if (e.target.closest('[data-wall-picker]')) openWallPicker(); });
const bar = $('buildbar'); if (!bar) return;
bar.addEventListener('click', e => {
const t = e.target.closest('[data-cat],[data-piece]'); if (!t) return;
if (t.dataset.cat != null) { selectBuildCat(+t.dataset.cat); return; }
GAME.pieceIdx = PIECE_BY_ID[t.dataset.piece].index; GAME.buildCat = catOf(t.dataset.piece);
GAME.catPick = GAME.catPick || {}; GAME.catPick[catOf(piece().id)] = piece().id;
GAME.drag = null; hudDirty = true; lastBuildKey = ''; playSfx('ui');
});
bar.addEventListener('mouseover', e => { const t = e.target.closest('[data-piece]'); const id = t ? t.dataset.piece : null; if (id !== GAME.bbHover) { GAME.bbHover = id; } });
bar.addEventListener('mouseleave', () => { GAME.bbHover = null; });
// clicks on the menu never reach the world
bar.addEventListener('mousedown', e => e.stopPropagation());
}