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

174 lines
13 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.
// Discovery cards, hover info (I) and the cursor label.
// ---------- Discovery cards ----------
// Short cards in the lower right when you find or make something new. They fade
// on their own (hover holds them); everything they say is also in the notebook.
function showCard(c) {
if (!SET.cardSeconds) return;
const box = $('cards');
while (box.children.length >= 3) box.firstChild.remove();
const card = el('div', { class: 'card-pop plate interactive' + (c.accent ? ' ' + c.accent : ''), role: 'status' });
const timed = SET.cardSeconds > 0;
card.innerHTML = `<div class="top">${c.swatch ? `<span class="sw" style="background:${rgbOf(c.swatch)}"></span>` : ''}<span class="label">${esc(c.kind)}</span><button class="x" aria-label="Dismiss">×</button></div>`
+ `<div class="title">${esc(c.title)}</div>${(c.body || []).filter(Boolean).map((t, k) => `<div class="${k ? 'p2' : 'p1'}">${esc(t)}</div>`).join('')}`
+ (timed ? `<i class="life" style="--dur:${(SET.cardSeconds * (c.hold || 1)).toFixed(1)}s"></i>` : '');
const close = () => { if (card.classList.contains('out')) return; card.classList.add('out'); setTimeout(() => card.remove(), 380); };
card.querySelector('.x').onclick = () => { playSfx('ui'); close(); };
if (timed) card.querySelector('.life').addEventListener('animationend', close);
box.appendChild(card);
}
function howText(e) {
const F = e.from ? MD[e.from].name : '', f = F.toLowerCase();
switch (e.how) {
case 'dig': return e.from === WOOD ? 'Cut from a tree.' : `Broken loose from ${f}.`;
case 'melt': return `${F} melted at ${fmtT(e.T)}.`;
case 'smelt': return e.m === SLAG || e.m === MSLAG ? `Left over when ${f} smelted.` : `${F} smelted at ${fmtT(e.T)}.`;
case 'freeze': return e.from === WATER ? (e.m === SNOW ? 'Water froze into flakes in the cold air.' : 'Water froze in the cold air.') : `${F} set as it cooled below ${fmtT(e.T)}.`;
case 'boil': return `${F} boiled at ${fmtT(e.T)}.`;
case 'condense': return `${F} cooled below ${fmtT(e.T)} and condensed.`;
case 'ignite': return `${F} caught fire at ${fmtT(e.T)}.`;
case 'burn': return `What ${f} leaves behind when it burns.`;
case 'fire': return `${F} fired into brick in the heat above ${fmtT(e.T)}.`;
case 'blow': return 'Air blown through molten iron burned off its extra carbon.';
case 'alloy': return `${F} melted into molten copper and the two mixed.`;
case 'char': return `${F} got hot with no air to burn in, and charred.`;
case 'refine': return `Ground out of ${f}.`;
case 'cut': return `Cut from ${f} by a saw.`;
default: return '';
}
}
function discoveryCard(e) {
const m = e.m;
if (m === FIRE) { showCard({ kind: 'Discovery', title: 'Fire', body: [howText(e), 'Fuel burns where it touches air. More air, more heat.'] }); return; }
showCard({ kind: 'New material', title: MD[m].name, swatch: m, body: [howText(e), MD[m].desc] });
}
// Everything a material does when heated, cooled, wetted or lit, in short phrases. The hover
// box and the notebook's Materials page both show this list.
function reactionsOf(m, forNotebook) {
const d = MD[m], out = [];
if (!d || m === EMPTY || d.ph === PH_ITEM && !d.melt) return out;
const nm = k => MD[k].name.toLowerCase();
if (d.ign) out.push(`burns, catching at ${fmtT(d.ign)}`);
if (m === WOOD || m === LWOOD) out.push('turns to charcoal if heated with no air');
if (m === CLAY || m === LCLAY) out.push('fires into brick above 880°C');
if (m === WATER) out.push('freezes below 0°C', 'boils at 100°C');
else if (d.boil) out.push(`boils at ${fmtT(d.boil)}`);
if (d.melt) {
if (d.smelt) out.push(`smelts at ${fmtT(d.melt)} into ${nm(d.meltTo)} and slag`);
else if (d.meltTo === WATER) out.push('melts above 0°C');
else if (d.ph === PH_ITEM) out.push(`melts at ${fmtT(d.melt)}`);
else out.push(`melts at ${fmtT(d.melt)}${d.meltTo === MAGMA ? ' into magma' : d.meltTo === MGL && m !== GLASS ? ' into glass' : d.meltTo === MSLAG && m !== SLAG ? ' into slag' : ''}`);
}
if (d.freeze) out.push(`sets into ${nm(d.freezeTo)} below ${fmtT(d.freeze)}`);
if (m === STEAM) out.push('turns back to water below 97°C under a roof', 'rises into the clouds in the open');
else if (d.condT) out.push(`turns back to ${nm(d.condTo)} below ${fmtT(d.condT)}`);
if (HOTLIQ[m]) out.push(m === MAGMA ? 'boils water, and crusts into basalt in it' : 'boils water it touches');
if (ALLOY[m]) out.push(`mixes into molten copper as ${nm(FRZTO[ALLOY[m].into])}`);
if (m === MFE) out.push('turns to steel with air blown through it');
if (m === SNOW || m === ICE) out.push('chills water, and freezes it in cold air');
if (POROUS[m] && !d.ign) out.push('lets air through to a fire');
if (d.smelt && d.ph === PH_POWDER) out.push('smothers a fire under it');
if (m === ACORN) out.push('sprouts on dirt or grass under open sky');
if ((d.ph === PH_POWDER || d.ph === PH_ITEM) && m !== SNOW) out.push(DENS[m] < 1 ? 'floats on water' : 'sinks in water');
if (m === SNOW) out.push('floats on water');
if (forNotebook && d.ph === PH_SOLID && d.yld && d.yld !== m && MD[d.yld]) out.push(`drills into ${nm(d.yld)}`);
return out;
}
// What is under the cursor: t1 is the name, t2 the details, warn a short problem
// (too hard, out of reach) for the label next to the cursor.
function hoverInfo(ax, ay) {
if (SET.fog && fogSeen(ax, ay) < 64) return { t1: 'Unexplored', t2: [], t3: [] };
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return { t1: '', t2: ['Outside the world'], warn: '', air: true };
const i = ay * W + ax, m = mat[i];
let warn = '';
let t1 = MD[m].name, t2 = [];
const p = part[i], id = mach[i];
const b = blockP[((ay / BS) | 0) * BW + ((ax / BS) | 0)];
const bar = occ[i] ? ents.find(e => ax >= e.ix && ax < e.ix + BAR_W && ay >= e.iy && ay < e.iy + BAR_H) : null;
if (bar) { t1 = MD[bar.m].name; t2.push(fmtT(bar.T)); }
else if (id && machines[id]) {
const mm = machines[id], P = PIECE_BY_ID[mm.type]; t1 = P.name;
if (mm.type === 'dock') { t2.push('sells what drops through its grate', `${orders.length} open ${orders.length === 1 ? 'order' : 'orders'}`); }
if (mm.type === 'wheel' || mm.type === 'engine') t2.push(`making ${mm.out.toFixed(1)} power`);
if (mm.type === 'fan') { t2.push(mm.on ? 'on' : 'off · press F'); if (mm.hot > 760) { t2.push(`${mm.hot}°C · melts at 1085°C`); warn = 'too hot'; } }
if (mm.type === 'silo') t2.push(mm.sn ? `holds ${mm.sn.toLocaleString('en-US')} / ${SILO_CAP.toLocaleString('en-US')} ${MD[mm.sm].name.toLowerCase()}` : `empty (holds ${SILO_CAP.toLocaleString('en-US')} grains of one material)`, mm.open ? 'gate open · F shuts it' : 'gate shut · F opens it');
} else if (isProcCode(b)) {
const s = procState.get((ay >> 2) * BW + (ax >> 2)), saw = b === BP_SAW;
t1 = saw ? 'Saw' : 'Grinder';
t2.push(s && s.blocked ? saw ? 'stopped: no room underneath; a belt there carries them away' : 'stopped: clear the space under it' : s && tick - s.busyT < 30 ? saw ? 'cutting' : 'grinding' : saw ? 'idle: drop stone rubble or wood chunks on top' : 'idle: drop rubble, gravel, sand or dirt on top', saw ? `${(stats.bricks || 0).toLocaleString('en-US')} stone blocks, ${(stats.planks || 0).toLocaleString('en-US')} planks cut` : `${(stats.refined || 0).toLocaleString('en-US')} ground, ${(stats.refinedOre || 0).toLocaleString('en-US')} ore found`);
} else if (p === P_FILTER) {
const fb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pr = filterParts(filterInfo(fb)), ar = blockV[fb] & 128 ? '→' : '←';
t1 = 'Filter ' + ar; t2.push(`drops ${pr.drops}`, `carries ${pr.rides} ${ar}`, 'F to change · I shows all');
} else if (p === P_PIPE) {
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb);
t1 = 'Pipe' + (m !== EMPTY ? ` · ${MD[m].name} flowing past` : '');
t2.push(pn && pn.n ? `line holds ${pn.n.toLocaleString('en-US')} / ${pn.cap.toLocaleString('en-US')} ${MD[pn.m].name.toLowerCase()}` : `line empty (room for ${pn ? pn.cap : PIPE_CAP})`);
} else if (p) t1 = ({ 1: 'Screw tube ↑', 2: 'Screw tube ↓', 3: 'Screw tube ←', 4: 'Screw tube →', 5: 'Grate', 6: 'Gate (open)', 7: 'Water wheel', 8: 'Engine intake' })[p] + (m !== EMPTY ? ` · ${MD[m].name}` : '');
else if (m === BELTL || m === BELTR) t1 = 'Belt ' + (m === BELTL ? '←' : '→');
else if (m === PIPE && isPipeCode(b)) {
const pb = ((ay / BS) | 0) * BW + ((ax / BS) | 0), pn = pipeNetOf(pb), v = blockV[pb], arrows = ['↑', '→', '↓', '←'];
t1 = b === BP_PUMP ? `Pump ${arrows[v & 3]}` : b === BP_OUTLET ? `Outlet ${['↓', '←', '↑', '→'][v & 3]}` : 'Pipe';
t2.push(pn && pn.n ? `line holds ${pn.n.toLocaleString('en-US')} / ${pn.cap.toLocaleString('en-US')} ${MD[pn.m].name.toLowerCase()}` : `line empty (room for ${pn ? pn.cap : PIPE_CAP})`);
if (b === BP_PUMP) t2.push(v & 4 ? 'off · press F' : 'on · F turns it off');
if (b === BP_OUTLET) t2.push(v & 4 ? 'closed · press F' : 'open · F closes it');
}
else if (b === BP_WALL && PH[m] === PH_SOLID && !(flags[i] & F_NAT)) t1 = `${MD[m].name} block`;
const n = nets[netId[i]];
if (n && isPow[i] && !POWER_FREE) t2.push(`network ${n.supply.toFixed(1)} supply / ${n.demand.toFixed(1)} demand`);
if (player.lens && !bar) t2.push(fmtT(temp[i]));
if (!bar && m !== EMPTY && PH[m] === PH_SOLID && !PARTMAT[m] && (flags[i] & F_NAT)) {
const ok = HARD[m] <= BIT_TIERS[player.bitTier].hard;
t2.push(HARD[m] > 6 ? 'cannot be drilled' : ok ? `drillable (hardness ${HARD[m]})` : `too hard (${HARD[m]})`);
if (!ok && toolId() === 'drill') warn = HARD[m] > 6 ? 'cannot be drilled' : 'too hard';
}
if (!bar && PH[m] === PH_POWDER) t2.push('loose');
if (!bar && m === EMPTY && bgm[i]) { t1 = bgm[i] === WOOD ? 'Tree trunk' : 'Leaves'; t2.unshift(bgm[i] === WOOD ? 'drill it for wood chunks' : 'water and sand pass through'); }
if (flags[i] & F_BURN) t2.push('burning');
const mo = moldAt(ax, ay);
if (mo) {
t1 = MOLDS[mo.kind].name;
t2 = [mo.state === 'ready' ? (mo.kind === 'ingot' ? `${mo.ready.length} ${mo.ready.length === 1 ? 'bar' : 'bars'} set` : '100% set · press F') : mo.state === 'empty' ? 'empty' : `${mo.state === 'short' ? 'not full' : mo.state} ${Math.round(mo.fill * 100)}%`];
if (mo.blocked) t2.push('blocked: leave one open block under it');
if (mo.flaw) t2.push(mo.flaw);
const hint = furnaceJunkHint(mo).trim(); if (hint) t2.push(hint);
if (player.lens) t2.push(fmtT(temp[i]));
}
const reachOk = toolId() === 'drill' ? inDrillReach(ax, ay, GAME.drillW) : inReach(ax, ay);
if (!reachOk) { t2.push('out of reach'); warn = 'out of reach'; }
// what it does, unless the cursor is on a machine or mold, which have their own details
const what = bar ? bar.m : !mo && !id && !(p && p !== P_GRATE && p !== P_PIPE) && m !== BELTL && m !== BELTR && m !== PIPE ? (m === EMPTY && bgm[i] ? bgm[i] : m) : 0;
const t3 = what ? reactionsOf(what) : [];
return { t1, t2, t3, warn, air: m === EMPTY && !bgm[i] && !bar && !mo && !id && !p && !b };
}
function renderInfo() {
const r = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
const box = $('info');
const h = `${r.t1 ? `<span class="t1">${esc(r.t1)}</span>` : ''}${r.t2.length ? `<span class="t2">${esc(r.t2.join(' · '))}</span>` : ''}${r.t3 && r.t3.length ? `<span class="t3">${esc(r.t3.join(' · '))}</span>` : ''}`;
if (box.innerHTML !== h) box.innerHTML = h;
}
// Name of what is under the cursor, drawn right next to it
function drawCursorLabel(px, py, r) {
if (!SET.cursorLabel || (!GAME.mouse.in && !GAME.usingPad)) return;
const info = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
if (!info.t1 || info.air) return;
const ctx = R.ctx, dpr = R.canvas.width / window.innerWidth;
const fs = Math.round(12 * dpr * SET.uiScale);
if (!R.uiFont) R.uiFont = getComputedStyle(document.documentElement).getPropertyValue('--font-ui').trim() || 'sans-serif';
const main = info.t1 + (player.lens && R.lens ? '' : '');
const sub = info.warn || (R.lens ? info.t2.find(t => /°C$/.test(t)) || '' : '');
ctx.save();
ctx.font = `600 ${fs}px ${R.uiFont}`;
const w1 = ctx.measureText(main).width;
ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`;
const w2 = sub ? ctx.measureText(sub).width + fs * 0.6 : 0;
const padX = fs * 0.5, h = fs * 1.6;
let x = px + (r + 1.6) * R.zp + 6 * dpr, y = py + (r + 1.2) * R.zp + 4 * dpr;
if (x + w1 + w2 + padX * 2 > R.canvas.width) x = px - (r + 1.6) * R.zp - 6 * dpr - (w1 + w2 + padX * 2);
if (y + h > R.canvas.height) y = py - (r + 1.2) * R.zp - h;
ctx.fillStyle = 'rgba(20,17,15,0.82)';
ctx.beginPath(); ctx.roundRect(x, y, w1 + w2 + padX * 2, h, 4 * dpr); ctx.fill();
ctx.textBaseline = 'middle';
ctx.font = `600 ${fs}px ${R.uiFont}`; ctx.fillStyle = '#efe6d8'; ctx.fillText(main, x + padX, y + h / 2);
if (sub) { ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`; ctx.fillStyle = info.warn ? '#ef6a55' : '#ffd27a'; ctx.fillText(sub, x + padX + w1 + fs * 0.6, y + h / 2); }
ctx.restore();
}