105 lines
9.3 KiB
JavaScript
105 lines
9.3 KiB
JavaScript
// 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', 'grinder', 'saw', '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"/>',
|
||
grinder: '<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"/>',
|
||
saw: '<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());
|
||
}
|