Files
deepgrain-foundry/src/ui/actions.js
T

244 lines
14 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.
// What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame.
// Which overlays are on, shown as a coloured frame round the screen and a named badge
let ovShown = '';
function updateOverlayUI() {
const on = (GAME.running && R.lens ? 'lens ' : '') + (GAME.running && GAME.infoOverlay ? 'info' : '');
if (on + SET.thermalPalette === ovShown) return;
ovShown = on + SET.thermalPalette;
const f = $('ovframe'), b = $('ovbadges');
if (!f || !b) return;
f.className = on ? 'on ' + on : '';
let h = '';
if (R.lens) {
// a little key of the lens colours, from freezing to white hot
const tl = THERM[SET.thermalPalette] || THERM.inferno, stops = [0, 20, 150, 700, 1600].map(T => { const j = thermIdx(T); return `rgb(${tl[j]},${tl[j + 1]},${tl[j + 2]})`; });
h += `<span class="ovbadge lens"><span class="dot"></span>Thermal lens<span class="scale" style="background:linear-gradient(90deg,${stops.join(',')})" title="0°C to 1600°C"></span><kbd>H</kbd></span>`;
}
if (GAME.infoOverlay) h += `<span class="ovbadge info"><span class="dot"></span>Info overlay<kbd>I</kbd></span>`;
b.innerHTML = h;
}
function toggleLens() {
if (!player.lens) { setStatus('The thermal lens unlocks at your first fire.'); playSfx('error'); return; }
R.lens = !R.lens; playSfx('ui');
}
function doInteract() {
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
const fb = filterBlockAt(ax, ay);
if (fb >= 0) {
if (!inReach(ax, ay)) { setStatus('Out of reach.'); playSfx('error'); return; }
const run = filterRun(fb);
openFilterPicker(Object.assign(filterInfo(fb), { dir: blockV[fb] & 128 ? 1 : 0, code: blockP[fb] }), f => {
applyFilterRun(run, f);
GAME.filterCfg = { m: f.m.slice(), inv: f.inv }; GAME.pieceDir.filter = f.dir;
setStatus(`${run.length} ${run.length === 1 ? 'filter' : 'filters'} updated, carrying ${beltDirection(blockP[fb], f.dir ? 128 : 0)}.`, 'info');
}, run.length > 1 ? `Filter (${run.length} in this row)` : 'Filter');
GAME.editRun = run;
return;
}
if (toolId() === 'build' && pieceUsesWall(piece())) { openWallPicker(); return; }
if (toolId() === 'build' && piece().filter) { openFilterPicker(Object.assign({}, GAME.filterCfg, { dir: GAME.pieceDir.filter || 0, code: pieceCode(piece()) }), f => { GAME.filterCfg = { m: f.m, inv: f.inv }; GAME.pieceDir.filter = f.dir; }, 'New filters'); return; }
let msg = '';
outer: for (let r = 0; r <= 2; r++) for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) {
if (!inReach(ax + dx, ay + dy)) continue;
msg = interactAt(ax + dx, ay + dy);
if (msg) break outer;
}
if (msg) setStatus(msg, 'info', /^New (drill bit|bag)/.test(msg));
else if ((moldAt(ax, ay) || {}).kind !== 'ingot') setStatus('Nothing to use there.', 'info'); // an ingot mold needs no F: say nothing
}
// Pick (middle mouse): the piece under the cursor becomes the one you build, with its
// direction, wall material and filter settings. On loose material it picks that to blow out.
function pickUnderCursor() {
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return;
const i = ay * W + ax, m = mat[i];
let bi = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pid = null;
// a sloped belt's band is drawn across its block's edge, so the belt you see can lie in the next block
if (!blockP[bi] && !ghostBlocks.has(bi)) { const sb = slopeBeltAt(i, ax, ay, R.smoothOn); if (sb >= 0) bi = sb; }
const ghost = ghostBlocks.get(bi);
if (mach[i] && machines[mach[i]]) { const mm = machines[mach[i]]; pid = mm.type; if (PIECE_BY_ID[pid].dirs) GAME.pieceDir[pid] = mm.dir; }
else if (moldAt(ax, ay)) { const mo = moldAt(ax, ay); pid = mo.kind + 'mold'; GAME.wallMat = mo.wall; }
else if (blockP[bi] || ghost) {
const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1];
const P = PIECE_BY_CODE[code]; pid = P.id;
if (isBelt(code) || isFilter(code)) { GAME.pieceShape = GAME.pieceShape || {}; GAME.pieceShape[P.id] = code; }
if (isWall(code)) { GAME.wallMat = v; GAME.pieceDir.wall = isSlope(code) ? code - BP_SLOPE_BL + 1 : 0; }
else if (P.filter) { GAME.filterCfg = blockP[bi] ? filterInfo(bi) : cleanFilterCfg(ghost[2]); GAME.pieceDir[P.id] = v >> 7; }
else if (P.dirs) GAME.pieceDir[P.id] = v % P.dirs.length;
}
if (pid) {
const P = PIECE_BY_ID[pid];
if (P.hidden) { setStatus(`${P.name} cannot be built.`); playSfx('error'); return; }
GAME.pieceIdx = P.index; GAME.buildCat = catOf(P.id); GAME.paste = false; selectTool(2); hudDirty = true; playSfx('ui');
setStatus(`Picked: ${P.name}.`, 'info');
return;
}
const t = PH[m] === PH_POWDER || PH[m] === PH_LIQUID ? m : 0;
if (t) { GAME.blow = new Set([t]); selectTool(1); hudDirty = true; playSfx('ui'); setStatus(`Picked: ${MD[t].name} to blow out.`, 'info'); }
else setStatus('Nothing built here to pick.', 'info');
}
function aimBlock() { return blockOf(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)); }
function stampOrigin(bw, bh) { const { bx, by } = aimBlock(); return { bx: bx - ((bw - 1) >> 1), by: by - ((bh - 1) >> 1) }; }
// Copy and cut: hold C or X and drag a box with the left button; letting go does it.
function selModeHeld() { return GAME.running && !anyModal() ? (keys.has('x') ? 'cut' : keys.has('c') ? 'copy' : null) : null; }
function startPaste() {
if (!GAME.clip) { setStatus('Hold C and drag over built pieces to copy.', 'info'); playSfx('error'); return; }
GAME.paste = true; GAME.drag = null; hudDirty = true; playSfx('ui');
}
function stopSelectAndPaste() { GAME.selDrag = null; GAME.paste = false; hudDirty = true; playSfx('ui'); }
function plural(n, word) { return `${n} ${word}${n === 1 ? '' : 's'}`; }
function piecesIn(rec, tag) { return rec.ops.filter(o => o.t === tag).length; }
function doUndo() {
GAME.drag = null; GAME.selDrag = null;
const msg = undoLast();
setStatus(msg, /could not|Nothing/.test(msg) ? 'err' : 'info');
playSfx(/Nothing/.test(msg) ? 'error' : 'decon'); hudDirty = true;
}
// right-click take-backs while the button is held count as one thing to undo
function endRmbRecord() { if (GAME.rmbRec) { const r = GAME.rmbRec; GAME.rmbRec = null; const n = r.ops.length; r.label = `taking back ${plural(n, 'piece')}`; undoPush(r); } }
// Belts, filters and screws run the way you drag them; a single click uses the direction R sets
function dragDirFor(P, a, b) {
if (!a || !P.dirs || !(P.id === 'belt' || P.id === 'screw' || P.filter)) return -1;
const dx = b.bx - a.bx, dy = b.by - a.by;
if (!dx && !dy) return -1;
if ((P.id === 'belt' || P.filter) && diagonalDrag(dx, dy)) return P.filter ? (dx * dy > 0 ? 1 : 0) : (dx > 0 ? 1 : 0);
if (P.id === 'screw' && diagonalDrag(dx, dy)) return dx > 0 ? (dy < 0 ? 4 : 5) : (dy > 0 ? 6 : 7);
if (P.id === 'screw') return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0);
return Math.abs(dx) >= Math.abs(dy) ? (dx > 0 ? 1 : 0) : -1;
}
function blockLabel(P, rec) {
const put = piecesIn(rec, 'block+'), took = piecesIn(rec, 'block-') + piecesIn(rec, 'filter');
return took ? `replacing ${plural(took, 'piece')}` : `placing ${plural(put, P.name.toLowerCase())}`;
}
// press / release actions (one-shot)
function primaryPress(shift) {
const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
const sel = selModeHeld();
if (sel) { GAME.selDrag = { x: ax, y: ay, mode: sel }; GAME.drag = null; hudDirty = true; return; }
if (GAME.paste && GAME.clip) {
const { bx, by } = stampOrigin(GAME.clip.bw, GAME.clip.bh);
undoRecord('pasting a blueprint', () => pasteGhost(GAME.clip, bx, by)); playSfx('place'); hudDirty = true;
return;
}
if (t === 'vacuum') {
GAME.vacFilter = shift ? -1 : GAME.blow.size ? -2 : grainAt(ax, ay);
hudDirty = true;
} else if (t === 'build') {
const P = piece();
if (P.kind === 'block') { GAME.drag = aimBlock(); }
else {
const { bw, bh } = pieceSize(P);
const { bx, by } = stampOrigin(bw, bh);
if (!inBuildRange(bx * BS + bw * BS / 2, by * BS + bh * BS / 2)) { setStatus('Out of reach.'); playSfx('error'); return; }
const place = () => P.kind === 'mold' ? placeMoldAt(P.mold, bx, by, GAME.wallMat) : placeMachineAt(P.id, GAME.pieceDir[P.id] || 0, bx, by);
const check = () => P.kind === 'mold' ? canPlaceMold(P.mold, bx, by) : canPlaceMachine(P.id, bx, by);
let err = '';
undoRecord(rec => piecesIn(rec, 'block-') + piecesIn(rec, 'mach-') + piecesIn(rec, 'mold-') ? `replacing with a ${P.name.toLowerCase()}` : `placing a ${P.name.toLowerCase()}`, () => {
err = check() || '';
// G: take down what you built in the way (refunded) and put this in its place
if (err && GAME.replace && /already/.test(err)) {
err = footprintBlocker(bx, by, bw, bh);
if (!err && !canAfford(pieceCostNow(P), 1)) err = needText(pieceCostNow(P), 1);
if (!err) err = clearFootprint(bx, by, bw, bh);
}
if (!err) err = place();
});
if (err) { setStatus(err + (/already/.test(err) ? `; hold G to replace it.` : '')); playSfx('error'); } else { playSfx('place'); hudDirty = true; }
}
}
}
function primaryRelease(shift) {
const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
if (GAME.selDrag) {
const sd = GAME.selDrag; GAME.selDrag = null; hudDirty = true;
if (sd.mode === 'cut') {
let bp = null;
undoRecord(rec => `cutting ${plural(piecesIn(rec, 'block-') + piecesIn(rec, 'mach-') + piecesIn(rec, 'mold-'), 'piece')}`, () => { bp = cutRegion(sd.x, sd.y, ax, ay); });
if (bp) { GAME.clip = bp; GAME.paste = true; playSfx('decon'); setStatus(`Cut ${bp.bw}×${bp.bh} blocks; click to paste.`, 'info'); }
else { setStatus('Nothing you built is inside that box.'); playSfx('error'); }
} else {
const bp = copyRegion(sd.x, sd.y, ax, ay);
if (bp) { GAME.clip = bp; GAME.paste = true; playSfx('cast'); setStatus(`Copied ${bp.bw}×${bp.bh} blocks; click to place.`, 'info'); }
else { setStatus('Nothing you built is inside that box.'); playSfx('error'); }
}
return;
}
if (t === 'build' && GAME.drag) {
const P = piece(), plan = dragPlan(P, GAME.drag, aimBlock(), shift), { code, v, cells: list } = plan;
if (plan.dd >= 0) GAME.pieceDir[P.id] = plan.dd;
const ex = pieceExtra(P);
// blocks past the bot's reach become ghosts it builds when it gets close, so a line can be any length
const near = list.filter(([bx, by]) => blockInReach(bx, by)), far = list.filter(([bx, by]) => !blockInReach(bx, by));
let ghosts = 0;
const r = undoRecord(rec => blockLabel(P, rec) + (piecesIn(rec, 'paste') ? ' (and its ghosts)' : ''), () => {
const res = placeBlocks(code, v, near, ex, GAME.replace);
const g = { t: 'paste', blocks: [], machines: [], molds: [] };
for (const [bx, by] of far) {
const b = by * BW + bx;
if (bx < 0 || by < 0 || bx >= BW || by >= BH || blockIs(b, code, v, ex) || ghostBlocks.has(b)) continue;
// held G over a different piece: the ghost replaces it when the bot comes in range
ghostBlocks.set(b, GAME.replace && blockP[b] ? [code, v, ex, 1] : ex ? [code, v, ex] : [code, v]); g.blocks.push([bx, by, code]); ghosts++;
}
if (ghosts) undoNote(g);
return res;
});
const hint = /already built/.test(r.status) ? `; hold G to replace it.` : '';
const gtxt = ghosts ? `${r.n} placed; ${ghosts} ghosts waiting for the bot.` : '';
if (r.n || ghosts) { playSfx('place'); setStatus(gtxt || (r.status ? r.status + hint : ''), 'info'); } else { setStatus((r.status || 'Nothing placed.') + hint); playSfx('error'); }
GAME.drag = null; GAME.dragDir = -1; hudDirty = true;
}
if (t === 'vacuum') { GAME.vacFilter = 0; hudDirty = true; }
}
function secondaryPress(shift) {
const t = toolId();
GAME.rmbSeen = new Set(); GAME.blowAll = !!shift;
if (GAME.selDrag) { GAME.selDrag = null; hudDirty = true; GAME.rmbConsumed = true; }
else if (GAME.paste) { GAME.paste = false; hudDirty = true; playSfx('ui'); GAME.rmbConsumed = true; }
else if (t === 'build' && GAME.drag) { GAME.drag = null; GAME.dragDir = -1; GAME.rmbConsumed = true; }
else { GAME.rmbConsumed = false; if (t === 'build') { endRmbRecord(); GAME.rmbRec = { label: '', ops: [] }; } }
}
// continuous actions, once per simulation tick
function toolTick() {
const t = toolId(), ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
if (GAME.lmb && !GAME.selDrag && !GAME.paste && !selModeHeld()) {
if (t === 'drill') {
const r = toolPulse(ax, ay, GAME.drillW, GAME.drillTune);
if (r.fired) { playSfx('pulse', GAME.drillW); if (r.status) setStatus(r.status); }
} else if (t === 'vacuum') {
if (GAME.vacFilter === 0) { GAME.vacFilter = grainAt(ax, ay); if (GAME.vacFilter) hudDirty = true; }
const f = GAME.vacFilter;
if (f !== 0) {
const r = toolVacuum(ax, ay, GAME.brush[1], f === -2 ? m => GAME.blow.has(m) : f < 0 ? () => true : m => m === f);
if (r.got) { playSfx('suck'); hudDirty = true; }
if (r.status) setStatus(r.status);
}
} else if (t === 'ignite') {
if (inReach(ax, ay)) { const r = toolIgnite(ax, ay); playSfx('spark'); if (Math.random() < 0.6) spawnSpark(ax + 0.5, ay + 0.5); if (!r.fuel) setStatus('Aim at coal or wood touching air to light it.'); }
else setStatus('Out of reach.');
}
}
if (GAME.rmb && !GAME.rmbConsumed) {
if (t === 'vacuum') {
if (!GAME.blowAll && !GAME.blow.size) { setStatus('Nothing selected: click or Q/E in the bag.'); return; }
const list = GAME.blowAll ? blowAllList() : blowList();
if (!GAME.blowAll && !list.length) { setStatus('None of the selected materials are in the bag.'); return; }
const r = GAME.blowAll && !list.length ? { placed: 0, status: 'Nothing loose in the bag to blow.' } : toolBlow(ax, ay, GAME.brush[1], list[0] || 0, list.length > 1 ? list : null);
// blown down to nothing: it leaves the selection, so an empty selection picks up what you click again
for (const m of list) if (bag[m] <= 0 && GAME.blow.delete(m)) { lastBagKey = ''; hudDirty = true; }
if (r.placed) { playSfx('blow'); hudDirty = true; } else if (r.status) setStatus(r.status);
} else if (t === 'build') {
const key = `${(ax / BS) | 0},${(ay / BS) | 0}`;
if (!GAME.rmbSeen.has(key) && inBuildRange(ax, ay)) {
GAME.rmbSeen.add(key);
const keep = undoRec; undoRec = GAME.rmbRec;
const msg = removePieceAt(ax, ay);
undoRec = keep;
if (msg) { playSfx('decon'); setStatus(msg, 'info'); hudDirty = true; }
}
}
}
}