187 lines
14 KiB
JavaScript
187 lines
14 KiB
JavaScript
// Discovery cards, hover info (I) and the cursor label.
|
||
// ---------- Discovery cards ----------
|
||
// Short cards at the bottom centre 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 'press': return `Pressed from ${f} by a Brick press.`;
|
||
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 (b === BP_RACK) {
|
||
const top = rackTop((ay >> 2) * BW + (ax >> 2)), counts = new Map();
|
||
let n = 0, capacity = 0;
|
||
for (let q = top; q < NB && blockP[q] === BP_RACK; q += BW) {
|
||
const s = rackState.get(q); capacity += RACK_CAP; n += s.n;
|
||
for (const [m, count] of s.q) counts.set(m, (counts.get(m) || 0) + count);
|
||
}
|
||
t1 = 'Bar rack'; t2.push(`${n} / ${capacity} bars`);
|
||
t2.push(...[...counts].slice(0, 4).map(([m, count]) => `${count} ${MD[m].name.toLowerCase()}`));
|
||
if (counts.size > 4) t2.push('+ more');
|
||
t2.push(rackState.get(top).open ? 'open: letting bars out' : 'closed: F lets bars out');
|
||
if (n === capacity) t2.push('full');
|
||
} else if (isProcCode(b)) {
|
||
const s = procState.get((ay >> 2) * BW + (ax >> 2)), saw = b === BP_SAW, press = b === BP_PRESS;
|
||
t1 = press ? 'Brick press' : saw ? 'Saw' : 'Grinder';
|
||
t2.push(s && s.blocked ? saw || press ? 'stopped: no room underneath; a belt there carries them away' : 'stopped: clear the space under it' : s && tick - s.busyT < 30 ? press ? 'pressing' : saw ? 'cutting' : 'grinding' : press ? 'idle: drop clay lumps on top' : saw ? 'idle: drop stone rubble or wood chunks on top' : 'idle: drop rubble, gravel, sand or dirt on top', press ? `${stats.pressed || 0} green bricks pressed` : 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();
|
||
}
|