The magic grip is gone: cleats every 8 face cells, 6 grains tall, carry pockets uphill (about 71 grains/s against 90 flat) and overflow rolls back. Sloped filters always run downhill; grains travel by gravity and a shake, and sorting stays exact. Drawing needs another pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
62 lines
5.5 KiB
JavaScript
62 lines
5.5 KiB
JavaScript
// Selection pickers: filter rules, filter settings and wall materials.
|
|
// 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) {
|
|
undoRecord('filter settings', () => {
|
|
for (const b of run) { undoNote({ t: 'filter', bx: b % BW, by: (b / BW) | 0, v: blockV[b], extra: filterInfo(b) }); blockV[b] = filterVariant(blockP[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 = beltGrade(cur.code) ? (beltGrade(cur.code)>0 ? 1 : 0) : 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="Screen travel"><span class="label">Screen travel</span>${beltGrade(cur.code) ? `<span>${esc(beltDirection(cur.code, dir << 7))}</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)}, ${beltGrade(cur.code) ? beltDirection(cur.code, dir << 7) : 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 openWallPicker() {
|
|
const P = piece(), title = P.kind === 'mold' ? `${P.name} material` : 'Wall material';
|
|
const list = WALL_LIST.filter(m => bag[m] > 0).concat(WALL_LIST.filter(m => !bag[m]));
|
|
const body = `<div class="wpick" role="radiogroup" aria-label="${esc(title)}">${list.map(m => {
|
|
const d = MD[WALL_OF[m]], cost = pieceCostNow(P, m), on = m === GAME.wallMat;
|
|
// two sources build the same wall (glass from shards or from bars): say which
|
|
const name = WALL_LIST.filter(k => WALL_OF[k] === WALL_OF[m]).length > 1 ? `${d.name} from ${MD[m].name.toLowerCase()}` : d.name;
|
|
const heat = d.ign ? `burns at ${fmtT(d.ign)}` : d.melt ? `melts at ${fmtT(d.melt)}` : "won't melt";
|
|
return `<button class="wp${on ? ' on' : ''}${canAfford(cost, 1) ? '' : ' short'}" role="radio" aria-checked="${on}" data-wall="${m}"><span class="sw" style="background:${rgbOf(WALL_OF[m])}"></span><span><b>${esc(name)}</b><span>you have ${bag[m].toLocaleString('en-US')}${PH[m] === PH_ITEM ? ` ${esc(MD[m].name.toLowerCase())}${bag[m] === 1 ? '' : 's'}` : ''}</span><span>cost: ${esc(costText(cost, 1))}</span><span>${heat}</span></span></button>`;
|
|
}).join('')}</div>`;
|
|
dialog(title, body, [{ label: 'Cancel' }]);
|
|
$('dlg-body').querySelectorAll('[data-wall]').forEach(b => b.onclick = () => {
|
|
GAME.wallMat = +b.dataset.wall; closeModal('m-dialog'); playSfx('ui'); hudDirty = true; lastBagKey = '';
|
|
setStatus(`${P.kind === 'mold' ? P.name : 'Walls'}: ${MD[WALL_OF[GAME.wallMat]].name.toLowerCase()}.`, 'info');
|
|
});
|
|
}
|