// The selected tool and piece: R / Q / E, drill width, filter picker, the selection card, the bag panel. function pieceUsesWall(P) { return P.variants === 'wall'; } function setDrillWidth(w) { const n = Math.max(0, Math.min(PULSE_W.length - 1, w)); if (n === GAME.drillW) return; GAME.drillW = n; drillWidth = n; hudDirty = true; playSfx('ui'); const what = ['long beam', 'balanced cone', 'broad fan'][n]; setStatus(`${PULSE_W[n].name[0].toUpperCase() + PULSE_W[n].name.slice(1)} pulse, ${what} (reach ${drillReach(n)}).`, 'info'); } // R with the drill: tune it to the material under the cursor, or back to everything function tuneUnderCursor() { const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y); if (ax < 0 || ay < 0 || ax >= W || ay >= H) return; const m = mat[ay * W + ax]; const to = PH[m] === PH_SOLID ? YLD[m] : PH[m] === PH_POWDER ? m : 0; GAME.drillTune = !to || to === GAME.drillTune ? 0 : to; hudDirty = true; playSfx('ui'); setStatus(GAME.drillTune ? `Tuned to ${MD[GAME.drillTune].name.toLowerCase()}: only it shatters.` : 'Untuned: the drill shatters any rock it can.', 'info'); } function cycleItem(d) { const t = toolId(); if (t === 'drill') { setDrillWidth(GAME.drillW + d); return; } if (t === 'vacuum') { // what right click blows: each material in the bag, plus an even mix of the ticked ones const l = bagMaterials().map(String); if (mixList().length >= 2) l.push('mix'); if (!l.length) return; let k = l.indexOf(GAME.blowMix ? 'mix' : String(GAME.dropMat)); k = (k + d + l.length) % l.length; if (k < 0) k = 0; if (l[k] === 'mix') GAME.blowMix = true; else { GAME.blowMix = false; GAME.dropMat = +l[k]; } GAME.matChosen = true; } else if (t === 'build') { let k = BUILD_ORDER.indexOf(piece().id); k = (k + d + BUILD_ORDER.length) % BUILD_ORDER.length; GAME.pieceIdx = PIECE_BY_ID[BUILD_ORDER[k]].index; GAME.buildCat = catOf(BUILD_ORDER[k]); GAME.catPick = GAME.catPick || {}; GAME.catPick[GAME.buildCat] = BUILD_ORDER[k]; GAME.drag = null; } else return; playSfx('ui'); hudDirty = true; } function cycleWall(d) { // prefer materials you actually have, but allow choosing any const have = WALL_LIST.filter(m => bag[m] > 0); const list = have.length ? have.concat(WALL_LIST.filter(m => !have.includes(m))) : WALL_LIST; let k = list.indexOf(GAME.wallMat); k = (k + d + list.length) % list.length; GAME.wallMat = list[k]; } function rotate() { const t = toolId(); if (GAME.paste && GAME.clip) GAME.clip = flipBlueprint(GAME.clip); else if (t === 'build') { const P = piece(); 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 return; playSfx('ui'); hudDirty = true; } function toggleCollect(m) { if (!m) return; if (GAME.collect.has(m)) GAME.collect.delete(m); else GAME.collect.add(m); try { localStorage.setItem('deepgrain.collect', JSON.stringify([...GAME.collect])); } catch (e) { /* storage unavailable */ } lastBagKey = ''; hudDirty = true; playSfx('ui'); } function pickFilterUnderCursor() { const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y); const bar = ents.find(e => ax >= e.ix && ax < e.ix + BAR_W && ay >= e.iy && ay < e.iy + BAR_H); const m = bar ? bar.m : grainAt(ax, ay); if (!m || !filterable(m)) { setStatus('Aim at something loose to add it to the filter.', 'info'); return; } toggleFilterMat(m); } function toggleFilterMat(m) { const f = GAME.filterCfg, k = f.m.indexOf(m); if (k >= 0) f.m.splice(k, 1); else f.m.push(m); f.m.sort((a, b) => a - b); 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 tick it.', 'info'); return; } toggleCollect(m); setStatus(`${MD[m].name} ${GAME.collect.has(m) ? 'ticked for vacuuming' : 'unticked'}.`, 'info'); } function setBrush(d) { const t = toolId(); if (t === 'drill') { setDrillWidth(GAME.drillW + d); return; } if (t === 'build' || t === 'blueprint' || t === 'ignite') return; GAME.brush[GAME.tool] = Math.max(0, Math.min(t === 'vacuum' ? 6 : 8, GAME.brush[GAME.tool] + d)); hudDirty = true; } // The block variant a piece is placed with: wall material, or direction (a filter's // materials and mode travel separately, as GAME.filterCfg) function pieceV(P) { if (P.code === BP_WALL) return GAME.wallMat; if (P.filter) return (GAME.pieceDir[P.id] || 0) << 7; return GAME.pieceDir[P.id] || 0; } function pieceExtra(P) { return P.filter ? { m: GAME.filterCfg.m.slice(), inv: GAME.filterCfg.inv } : undefined; } // What a filter can hold: loose things and bars you have found, plus anything it already holds function filterChoices(cur) { return BAG_ORDER.filter(m => filterable(m) && (seen[m] || cur.includes(m))); } function matList(ms) { const n = ms.map(m => MD[m].name.toLowerCase()); return n.length <= 3 ? n.join(n.length === 2 ? ' and ' : ', ') : `${n.slice(0, 2).join(', ')} and ${n.length - 2} more`; } // What a filter does, in words: what falls through and what rides along function filterParts(f) { if (!f.m.length) return f.inv ? { drops: 'everything', rides: 'nothing' } : { drops: 'nothing', rides: 'everything' }; return f.inv ? { drops: 'everything else', rides: matList(f.m) } : { drops: matList(f.m), rides: 'everything else' }; } function filterSentence(f) { const p = filterParts(f); return `drop ${p.drops} and carry ${p.rides}`; } // Change the whole run of matching filters at once function applyFilterRun(run, f) { for (const b of run) { blockV[b] = f.dir ? 128 : 0; setFilter(b, f); } filtersDirty = true; } function openFilterPicker(cur, onDone, title) { const sel = new Set(cur.m); let inv = cur.inv ? 1 : 0, dir = cur.dir ? 1 : 0; const list = filterChoices(cur.m); const body = `
Find something loose first.
') + ``; dialog(title || 'Filter', body, [{ label: 'Clear', cls: 'ghost', fn: () => { sel.clear(); sync(); return true; } }, { label: 'Cancel' }, { label: 'Set filter', cls: 'primary', fn: () => { onDone({ m: [...sel].sort((a, b) => a - b), inv, dir }); hudDirty = true; } }]); const box = $('dlg-body'); function sync() { box.querySelectorAll('[data-inv]').forEach(b => { const on = +b.dataset.inv === inv; b.classList.toggle('on', on); b.setAttribute('aria-checked', on); }); box.querySelectorAll('[data-dir]').forEach(b => { const on = +b.dataset.dir === dir; b.classList.toggle('on', on); b.setAttribute('aria-checked', on); }); box.querySelectorAll('[data-m]').forEach(b => { const on = sel.has(+b.dataset.m); b.classList.toggle('on', on); b.setAttribute('aria-checked', on); }); const f = { m: [...sel].sort((a, b) => a - b), inv }; box.querySelector('#fp-sum').textContent = `${title && /in this row/.test(title) ? 'These filters' : 'This filter'} will ${filterSentence(f)}, ${dir ? 'right' : 'left'}.`; } box.querySelectorAll('[data-dir]').forEach(b => b.onclick = () => { dir = +b.dataset.dir; playSfx('ui'); sync(); }); box.querySelectorAll('[data-inv]').forEach(b => b.onclick = () => { inv = +b.dataset.inv; playSfx('ui'); sync(); }); 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 filterBlockAt(x, y) { if (x < 0 || y < 0 || x >= W || y >= H) return -1; const b = ((y / BS) | 0) * BW + ((x / BS) | 0); return blockP[b] === BP_FILTER ? b : -1; } function chipCost(cost) { const ok = canAfford(cost, 1); return `${esc(costText(cost, 1))}`; } function pieceCostNow(P) { if (P.kind === 'mold') return wallCost(GAME.wallMat, MOLDS[P.mold].walls); if (P.kind === 'block' && P.code === BP_WALL) return wallCost(GAME.wallMat, BS * BS); return P.cost; } function renderSelCard() { const t = toolId(), c = $('selcard'); let h = ''; const status = performance.now() - GAME.statusT < 3200 ? GAME.status : ''; const sel = GAME.selDrag ? GAME.selDrag.mode : selModeHeld(); c.hidden = false; const ng = ghostBlocks.size + ghostMachines.length + ghostMolds.length; if (sel === 'cut') { h = `