// 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 = `
${c.swatch ? `` : ''}${esc(c.kind)}
` + `
${esc(c.title)}
${(c.body || []).filter(Boolean).map((t, k) => `
${esc(t)}
`).join('')}` + (timed ? `` : ''); 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(pipeContentsText(pn)); } 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(pipeContentsText(pn)); 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 ? `${esc(r.t1)}` : ''}${r.t2.length ? `${esc(r.t2.join(' · '))}` : ''}${r.t3 && r.t3.length ? `${esc(r.t3.join(' · '))}` : ''}`; 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(); } function pipeContentsText(pn) { return 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})`; }