237 lines
18 KiB
JavaScript
237 lines
18 KiB
JavaScript
// 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 = `<div class="fmode" role="radiogroup" aria-label="Mode">
|
||
<button class="fm" role="radio" data-inv="0"><b>↓ Drop these</b><span>They fall through; the rest rides along</span></button>
|
||
<button class="fm" role="radio" data-inv="1"><b>→ Carry these</b><span>They ride along; the rest falls through</span></button>
|
||
</div>
|
||
<div class="fdir" role="radiogroup" aria-label="Rollers turn"><span class="label">Rollers turn</span><button class="fd" role="radio" data-dir="0">← Left</button><button class="fd" role="radio" data-dir="1">Right →</button></div>`
|
||
+ (list.length ? `<div class="fpick">${list.map(m => `<button class="fp" role="checkbox" data-m="${m}"><span class="ck" aria-hidden="true"></span><span class="sw" style="background:${rgbOf(m)}"></span><span>${esc(MD[m].name)}</span></button>`).join('')}</div>` : '<p class="note">Find something loose first.</p>')
|
||
+ `<p class="note fp-sum" id="fp-sum"></p>`;
|
||
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 `<span class="chip ${ok ? 'ok' : 'bad'}">${esc(costText(cost, 1))}</span>`; }
|
||
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 = `<div class="row"><span class="chip bad">✂ Cut</span><span class="name grow">Hold X and drag a box</span></div>
|
||
<div class="sub">Let go to take down everything you built in the box (refunded to your bag) and pick it up to paste somewhere else. <kbd>Ctrl</kbd>+<kbd>Z</kbd> puts it back.</div>`;
|
||
} else if (sel === 'copy') {
|
||
h = `<div class="row"><span class="chip ok">⧉ Copy</span><span class="name grow">Hold C and drag a box</span></div>
|
||
<div class="sub">Let go to copy what you built in the box. Then click to place ghosts of it; your bot builds them from your bag.</div>`;
|
||
} else if (GAME.paste && GAME.clip) {
|
||
h = `<div class="row"><span class="chip ok">Paste</span><span class="name grow">Blueprint</span><span class="chip">${GAME.clip.bw}×${GAME.clip.bh} blocks</span></div>
|
||
<div class="sub">Click to place a ghost; your bot builds ghosts from your bag when it is close. <kbd>R</kbd> flips it. Right click or <kbd>Esc</kbd> stops pasting, <kbd>V</kbd> starts again.</div><div class="sub">Needs ${esc(costText(bpCost(GAME.clip), 1) || 'nothing')}</div>`;
|
||
if (ng) h += `<div class="sub" style="color:var(--patina)">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</div>`;
|
||
} else if (t === 'drill') {
|
||
const b = BIT_TIERS[player.bitTier];
|
||
const pw = PULSE_W[GAME.drillW], tn = GAME.drillTune;
|
||
h = `<div class="row cycle"><kbd>Q</kbd><span class="name grow">Resonance drill</span><span class="chip hot">${pw.name} pulse</span><kbd>E</kbd></div>
|
||
<div class="row" style="flex-wrap:wrap;gap:4px"><span class="chip">${esc(b.name.toLowerCase())}</span><span class="chip">hardness ≤ ${b.hard}</span><span class="chip">reach ${drillReach(GAME.drillW)}</span>${tn ? `<span class="chip ok"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(tn)}"></span>tuned: ${esc(MD[tn].name.toLowerCase())} · R</span>` : '<span class="chip">untuned · R tunes</span>'}</div>
|
||
<div class="sub">Hold to send pulses at the cursor: rock they shake hard enough shatters and falls. <kbd>Q</kbd>/<kbd>E</kbd> width: narrow bores a long tunnel fast, wide clears a big face up close. <kbd>R</kbd> over a material tunes to it, so only that shatters. Built blocks are never harmed.</div>`;
|
||
} else if (t === 'build') {
|
||
// the build tool in general; the piece itself is described in the menu's hover popup
|
||
const P = piece();
|
||
const chips = buildVariantChips(P) + (GAME.replace ? '<span class="chip hot">Replace · G</span>' : '')
|
||
+ (ng ? `<span class="chip ok">${ng} ghost pieces waiting${ghostMissing ? ', some short on materials' : ''}.</span>` : '');
|
||
h = '<div class="row"><span class="name grow">Build</span></div>'
|
||
+ (chips ? `<div class="row" style="flex-wrap:wrap;gap:4px">${chips}</div>` : '')
|
||
+ '<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 · 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') {
|
||
ensureDropMat();
|
||
const m = GAME.dropMat, f = GAME.lmb ? GAME.vacFilter : 0;
|
||
const list = [...GAME.collect];
|
||
const mixL = mixList();
|
||
const blowRow = GAME.blowMix ? `<div class="row cycle"><kbd>Q</kbd>${mixL.map(k => `<span class="sw" style="background:${rgbOf(k)}"></span>`).join('')}<span class="name grow">Ticked mix</span><span class="num chip">${mixL.reduce((a, k) => a + bag[k], 0).toLocaleString('en-US')}</span><kbd>E</kbd></div>`
|
||
: m ? `<div class="row cycle"><kbd>Q</kbd><span class="sw" style="background:${rgbOf(m)}"></span><span class="name grow">${esc(MD[m].name)}</span><span class="num chip ${bag[m] ? '' : 'bad'}">${bag[m] ? bag[m].toLocaleString('en-US') : 'none left'}</span><kbd>E</kbd></div>`
|
||
: `<div class="row"><span class="name">Vacuum</span><span class="sub">bag empty</span></div>`;
|
||
const sucking = f < 0 ? 'everything' : list.length ? 'only the ticked materials' : f > 0 ? 'only ' + esc(MD[f].name.toLowerCase()) : 'what you start on';
|
||
h = `${blowRow}
|
||
<div class="sub"><b>Left</b> sucks up ${sucking}${list.length && f >= 0 ? ' (untick them all to pick by what you click on)' : ''}. Standing still, <kbd>Shift</kbd> takes everything.</div>
|
||
<div class="sub"><b>Right</b> blows ${GAME.blowMix ? 'an even mix of the ticked materials' : m ? 'only ' + esc(MD[m].name.toLowerCase()) : 'the chosen material'} out. ${mixL.length > 1 || GAME.blowMix ? '<kbd>Q</kbd>/<kbd>E</kbd> switch between single materials and the ticked mix.' : 'Tick 2+ materials to blow them as a mix.'}</div>
|
||
<div class="row" style="flex-wrap:wrap;gap:4px"><span class="chip">nozzle ${GAME.brush[1]}</span>${list.map(k => `<span class="chip"><span class="sw" style="display:inline-block;vertical-align:-2px;width:9px;height:9px;margin-right:4px;background:${rgbOf(k)}"></span>${esc(MD[k].name)}</span>`).join('')}${list.length ? '' : '<span class="sub">Nothing ticked. <kbd>R</kbd> ticks what is under the cursor.</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>`;
|
||
}
|
||
h += `<div class="status${GAME.statusKind === 'info' ? ' info' : ''}">${esc(status)}</div>`;
|
||
if (c.innerHTML !== h) c.innerHTML = h;
|
||
}
|
||
let lastBagKey = '';
|
||
function renderBag() {
|
||
const cap = bagCap(), pct = Math.min(100, bagTotal / cap * 100);
|
||
$('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));
|
||
const t = toolId();
|
||
const key = l.map(m => m + ':' + bag[m] + (GAME.collect.has(m) ? 'c' : '')).join(',') + '|' + GAME.dropMat + '|' + GAME.blowMix + '|' + GAME.wallMat + '|' + GAME.filterCfg.m.join('.') + '|' + t + '|' + GAME.pieceIdx;
|
||
if (key === lastBagKey) return;
|
||
lastBagKey = key;
|
||
const box = $('baglist'); box.innerHTML = '';
|
||
if (!l.length) { box.appendChild(el('div', { class: 'empty' }, 'Empty. Break rock with the Drill, then use the Vacuum.')); return; }
|
||
for (const m of l) {
|
||
const sel = (t === 'vacuum' && (GAME.blowMix ? GAME.collect.has(m) : m === GAME.dropMat)) || (t === 'build' && m === GAME.wallMat && pieceUsesWall(piece())) || (t === 'build' && piece().filter && GAME.filterCfg.m.includes(m));
|
||
const row = el('div', { class: 'bagrow' + (sel ? ' on' : '') });
|
||
const tick = el('button', { class: 'tick' + (GAME.collect.has(m) ? ' on' : ''), title: GAME.collect.has(m) ? 'Ticked: the vacuum takes only ticked materials, and they make up the Ticked mix' : 'Tick to vacuum only ticked materials', 'aria-label': `Tick ${MD[m].name}`, 'aria-pressed': GAME.collect.has(m) });
|
||
tick.onclick = () => toggleCollect(m);
|
||
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 = () => {
|
||
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.dropMat = m; GAME.matChosen = true; GAME.blowMix = false; if (t !== 'vacuum') selectTool(1); }
|
||
hudDirty = true; lastBagKey = ''; playSfx('ui');
|
||
};
|
||
row.appendChild(tick); row.appendChild(b); box.appendChild(row);
|
||
}
|
||
if (t === 'vacuum' && mixList().length > 1) {
|
||
// the ticked mix is a choice of its own, like any material
|
||
const mx = mixList(), row = el('div', { class: 'bagrow mixrow' + (GAME.blowMix ? ' on' : '') });
|
||
const b = el('button', { class: 'pick', title: 'Blow an even mix of the ticked materials' });
|
||
b.innerHTML = `<span class="mixsw">${mx.slice(0, 4).map(k => `<span class="sw" style="background:${rgbOf(k)}"></span>`).join('')}</span><span class="nm">Ticked mix</span><span class="n num">${mx.reduce((a, k) => a + bag[k], 0).toLocaleString('en-US')}</span>`;
|
||
b.onclick = () => { GAME.blowMix = true; GAME.matChosen = true; hudDirty = true; lastBagKey = ''; playSfx('ui'); };
|
||
row.appendChild(b); box.appendChild(row);
|
||
}
|
||
if (t === 'vacuum') box.appendChild(el('div', { class: 'baghint' }, 'Click a material to blow only that, or Ticked mix to blow an even mix of the ticked ones. Ticked ones always come along when you vacuum.'));
|
||
}
|