Bars no longer get thrown up into the furnace, and F is no longer needed to collect them. Blocked bars wait in the trough; the short stub stays for the next firing; F is optional upkeep (clean, chip slag, rake). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
202 lines
21 KiB
JavaScript
202 lines
21 KiB
JavaScript
// The notebook (N): milestones, market, wallet, blueprints, controls.
|
||
// ---------- Notebook ----------
|
||
let nbTab = 'Ledger';
|
||
function openNotebook(tab) { if (tab) nbTab = tab; renderNotebook(); openModal('m-notebook'); }
|
||
function renderNotebook() {
|
||
const tabs = $('nb-tabs'); tabs.innerHTML = '';
|
||
for (const name of ['Ledger', 'Market', 'Materials', 'Pieces', 'Blueprints', 'Controls']) {
|
||
const b = el('button', { role: 'tab', class: name === nbTab ? 'on' : '', 'aria-selected': name === nbTab }, name);
|
||
b.onclick = () => { playSfx('ui'); nbTab = name; renderNotebook(); };
|
||
tabs.appendChild(b);
|
||
}
|
||
const body = $('nb-body');
|
||
if (nbTab === 'Ledger') {
|
||
const done = MILESTONES.filter(m => ledger[m.id]).length;
|
||
let h = `<p class="note">${done} of ${MILESTONES.length} reached. There is no set order. Tips stay hidden until you ask for one. Upgrades come from what you make: your bit is as hard as the metal you cast it from.</p><div class="ledger">`;
|
||
for (const m of MILESTONES) {
|
||
const isDone = !!ledger[m.id], tip = isDone || GAME.tipsOpen.has(m.id);
|
||
h += `<div class="ms ${isDone ? 'done' : 'open'}"><span class="mark" aria-hidden="true"></span><span class="h">${esc(m.title)}</span>`;
|
||
h += tip ? `<span class="p">${esc(m.hint)}</span>` : `<span class="p"><button class="linkbtn" data-tip="${m.id}">Show tip</button></span>`;
|
||
if (m.reward) h += `<span class="rw">${isDone ? '✓ ' : ''}${esc(m.reward)}</span>`;
|
||
h += '</div>';
|
||
}
|
||
h += `</div><p class="note grouphead">Tools: <b>${esc(BIT_TIERS[player.bitTier].name)}</b> (digs hardness ${BIT_TIERS[player.bitTier].hard} and below) · <b>${esc(TANK_TIERS[player.tankTier].name)}</b> (${bagCap().toLocaleString('en-US')} grains)${player.lens ? ' · <b>Thermal lens</b>' : ''}${player.flask ? ' · <b>Flask</b>' : ''}</p>`;
|
||
body.innerHTML = h;
|
||
body.querySelectorAll('[data-tip]').forEach(b => b.onclick = () => { playSfx('ui'); GAME.tipsOpen.add(b.dataset.tip); renderNotebook(); });
|
||
} else if (nbTab === 'Market') {
|
||
renderMarketTab(body);
|
||
} else if (nbTab === 'Materials') {
|
||
let rows = '', unknown = 0;
|
||
const list = [DIRT, LDIRT, STONE, RSTONE, CLAY, LCLAY, WOOD, LWOOD, ACORN, CHARCOAL, SNOW, ICE, COALV, COAL, CUV, CUORE, FEV, FEORE, SAND, GRAVEL, GRANITE, RGRAN, BASALT, RBAS,
|
||
BRICK, RBRICK, GLASS, CULLET, COPPER, SCRCU, BARCU, IRON, SCRFE, BARFE, STEEL, SCRST, BARST, ASH, SLAG, MSLAG, WATER, STEAM, MAGMA, MCU, MFE, MST, MGL, FIRE, SMOKE, SNV, SNORE, TIN, MSN, SCRSN, BARSN, ZNV, ZNORE, ZINC, MZN, SCRZN, BARZN, AGV, AGORE, SILVER, MAG, SCRAG, BARAG, AUV, AUORE, GOLD, MAU, SCRAU, BARAU, BRONZE, MBZ, SCRBZ, BARBZ, BRASS, MBS, SCRBS, BARBS];
|
||
for (const m of list) {
|
||
if (!seen[m]) { unknown++; continue; }
|
||
const d = MD[m];
|
||
const ph = ['', 'Solid', 'Loose', 'Liquid', 'Gas', 'Object'][d.ph];
|
||
const change = reactionsOf(m, true).map((t, k) => k ? t : t[0].toUpperCase() + t.slice(1)).join(' · ');
|
||
rows += `<tr><td><span class="sw" style="display:inline-block;vertical-align:-2px;margin-right:6px;background:${rgbOf(m)}"></span>${esc(d.name)}</td><td>${ph}</td><td class="num">${d.hard && d.ph === PH_SOLID ? d.hard : '–'}</td><td>${esc(change || '–')}</td><td class="p" style="color:var(--ash)">${esc(d.desc || '')}</td></tr>`;
|
||
}
|
||
body.innerHTML = `<p class="note">What you have found so far. Hardness is what your drill bit has to match. Solid rock breaks into loose rubble that you vacuum up and build with.${unknown ? ` ${unknown} more to discover.` : ''}</p><div class="tablewrap"><table class="mt"><thead><tr><th>Material</th><th>State</th><th>Hard</th><th>What it does</th><th>Notes</th></tr></thead><tbody>${rows}</tbody></table></div>`;
|
||
} else if (nbTab === 'Pieces') {
|
||
let h = '<p class="note">Everything you build snaps to a grid of 4×4-grain blocks. Pieces only move, hold, blow or sense material; every change to the material itself comes from heat. Select pieces with the Build tool (3). Your bot flies straight through anything you build.</p><div class="cards">';
|
||
for (const P of PIECES) {
|
||
if (P.hidden) continue;
|
||
const cost = P.kind === 'mold' ? `${MOLDS[P.mold].walls} grains of wall material (R picks it)` : P.code === BP_WALL ? '16 grains of rubble, lumps or chunks, or 2 bars' : costText(P.cost, 1);
|
||
const afford = P.kind === 'mold' || P.code === BP_WALL ? true : canAfford(P.cost, 1);
|
||
const size = P.kind === 'machine' ? `${P.bw}×${P.bh}` : P.kind === 'mold' ? `${MOLDS[P.mold].bw}×${MOLDS[P.mold].bh}` : '1×1';
|
||
h += `<div class="card"><div class="h"><span>${esc(P.name)}</span><span class="chip ${afford ? 'ok' : ''}">${P.power ? esc(P.power + ' power') : P.id === 'wheel' || P.id === 'engine' ? 'makes power' : size + (size === '1×1' ? ' block' : ' blocks')}</span></div><div class="p">${esc(P.desc)}</div><div class="p">Cost: ${esc(cost)}</div></div>`;
|
||
}
|
||
body.innerHTML = h + '</div>';
|
||
} else if (nbTab === 'Blueprints') {
|
||
renderBlueprintTab(body);
|
||
} else {
|
||
body.innerHTML = controlsHtml();
|
||
}
|
||
}
|
||
function fmtClock(ticks) { const s = Math.max(0, Math.ceil(ticks / 60)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`; }
|
||
function orderName(o) { return `${o.who ? o.who + ': ' : ''}${o.qty.toLocaleString('en-US')} ${MD[o.m].name.toLowerCase()}${PH[o.m] === PH_ITEM && o.qty !== 1 ? 's' : ''}`; }
|
||
function orderLeft(o) { return Math.max(0, o.due - tick); }
|
||
const MARKET_GROUPS = [
|
||
['Raw', [COAL, LWOOD, ACORN, SAND, LCLAY, RGRAN, RBAS, CUORE, FEORE, SNORE, ZNORE]],
|
||
['Bulk (builders)', [RSTONE, GRAVEL, BARSB]],
|
||
['Refined', [CHARCOAL, RBRICK, CULLET, BARGL, SCRCU, SCRFE, SCRST, SCRSN, SCRZN, BARCU, BARSN, BARZN, BARFE, BARST]],
|
||
['Alloys', [SCRBZ, SCRBS, BARBZ, BARBS]],
|
||
['Precious', [AGORE, AUORE, SCRAG, SCRAU, BARAG, BARAU]],
|
||
];
|
||
function renderMarketTab(body) {
|
||
let h = `<div class="mkt-sum"><div><div class="k">Money</div><div class="v">${fmtMoney(money)}</div></div>`
|
||
+ `<div><div class="k">${debt > 0 ? 'Still owed on the claim' : 'The claim'}</div><div class="v" style="color:${debt > 0 ? 'var(--melt)' : 'var(--ok)'}">${debt > 0 ? fmtMoney(debt) : 'Paid off'}</div></div>`
|
||
+ `<div><div class="k">Earned in total</div><div class="v">${fmtMoney(earned)}</div></div></div>`;
|
||
if (debt > 0) h += `<div class="mkt-pay"><span class="note" style="margin:0 6px 0 0">Pay toward the claim:</span>${[1000, 5000].map(a => `<button class="btn" data-pay="${a}" ${money >= Math.min(a, debt) ? '' : 'disabled'}>${fmtMoney(Math.min(a, debt))}</button>`).join('')}<button class="btn primary" data-pay="all" ${money > 0 ? '' : 'disabled'}>All I can (${fmtMoney(Math.min(money, debt))})</button></div>`;
|
||
else h += `<p class="note">The land is yours. Keep building: the ledger has bigger goals.</p>`;
|
||
h += `<p class="note">Anything that drops through the trade dock's grate is sold on the spot, once it is cool enough to handle. Junk like dirt, rubble, ash and slag goes for nothing. Build walls around the dock to funnel more in.</p>`;
|
||
h += `<p class="label grouphead">Orders · ${esc(STANDING[standing()])}</p>`;
|
||
if (orders.length) {
|
||
h += '<div class="orderlist">' + orders.map(o => `<div class="o"><span class="t"><span class="sw" style="display:inline-block;vertical-align:-2px;margin-right:6px;background:${rgbOf(o.m)}"></span>${esc(orderName(o))}</span><span class="pay">${fmtMoney(o.pay)}</span>`
|
||
+ `<span class="d">${o.got} of ${o.qty} delivered · ${o.mult}× market · ${fmtClock(orderLeft(o))} left</span><span></span><span class="pb"><i style="width:${(o.got / o.qty * 100).toFixed(1)}%"></i></span></div>`).join('') + '</div>';
|
||
} else h += `<p class="note">No open orders. Buyers post new ones every minute or two, for goods you have found.</p>`;
|
||
h += `<p class="note">Goods dropped into the dock fill a matching order first. A missed deadline costs nothing: what you delivered is paid at market price. ${ordersDone} filled so far; bigger buyers come with more.</p>`;
|
||
h += `<p class="label grouphead">Prices</p><p class="note">Per grain, or per bar. Selling a lot of one thing lowers its price for a while; it recovers over about half an hour.</p>`;
|
||
let rows = '', unknown = 0;
|
||
for (const [g, list] of MARKET_GROUPS) {
|
||
const known = list.filter(m => seen[m]);
|
||
unknown += list.length - known.length;
|
||
if (!known.length) continue;
|
||
rows += `<tr><td colspan="3" class="label" style="padding-top:8px">${esc(g)}</td></tr>`;
|
||
for (const m of known) {
|
||
const f = priceFactor(m), now = priceNow(m);
|
||
rows += `<tr><td><span class="sw" style="display:inline-block;vertical-align:-2px;margin-right:6px;background:${rgbOf(m)}"></span>${esc(MD[m].name)}</td><td class="num">${fmtPrice(PRICE[m])}</td><td class="num ${f < 0.97 ? 'dip' : ''}">${fmtPrice(now)}${f < 0.97 ? ` (${Math.round(f * 100)}%)` : ''}</td></tr>`;
|
||
}
|
||
}
|
||
h += rows ? `<div class="tablewrap"><table class="mt"><thead><tr><th>Goods</th><th>Base</th><th>Now</th></tr></thead><tbody>${rows}</tbody></table></div>` : '<p class="note">Find something worth selling first.</p>';
|
||
if (unknown) h += `<p class="note">${unknown} more goods to discover.</p>`;
|
||
h += `<p class="label grouphead">Sales, minute by minute</p>`;
|
||
const recent = saleAcc.total > 0 ? [{ total: saleAcc.total, items: saleAcc.items, now: true }].concat(shipLog) : shipLog;
|
||
h += recent.length ? `<div class="shiplog">${recent.map(sl => `<div><span class="what">${sl.now ? 'This minute: ' : ''}${esc(itemsText(sl.items))}</span><b class="num">+${fmtMoney(sl.total)}</b></div>`).join('')}</div>` : '<p class="note">Nothing sold yet.</p>';
|
||
body.innerHTML = h;
|
||
body.querySelectorAll('[data-pay]').forEach(b => b.onclick = () => {
|
||
const v = b.dataset.pay === 'all' ? money : +b.dataset.pay;
|
||
const paid = payClaim(v);
|
||
if (paid > 0) { playSfx('sale'); toast(`Paid ${fmtMoney(paid)} toward the claim.`); }
|
||
renderNotebook(); hudDirty = true;
|
||
});
|
||
}
|
||
function fmtPrice(v) { return v >= 10 ? '$' + Math.round(v) : '$' + v.toFixed(2); }
|
||
function itemsText(items) {
|
||
const e = Object.entries(items).map(([m, n]) => [+m, n]).sort((a, b) => b[1] * PRICE[b[0]] - a[1] * PRICE[a[0]]);
|
||
const parts = e.slice(0, 3).map(([m, n]) => PH[m] === PH_ITEM ? `${n} ${MD[m].name.toLowerCase()}${n === 1 ? '' : 's'}` : `${n} ${MD[m].name.toLowerCase()}`);
|
||
if (e.length > 3) parts.push(`${e.length - 3} more`);
|
||
return parts.join(', ');
|
||
}
|
||
function renderWallet() {
|
||
$('w-money').textContent = fmtMoney(money);
|
||
const c = $('w-claim');
|
||
c.className = 'line' + (debt <= 0 ? ' paid' : '');
|
||
c.innerHTML = debt > 0 ? `<span>Claim owed</span><b>${fmtMoney(debt)}</b>` : `<span>Claim</span><b>paid off ✓</b>`;
|
||
const h = orders.length ? orders.map(o => {
|
||
const left = orderLeft(o);
|
||
return `<span class="ord"><span class="sw" style="background:${rgbOf(o.m)}"></span><span class="nm">${o.got}/${esc(orderName(o))}</span><span class="tm${left < 3600 ? ' late' : ''}">${fmtClock(left)}</span><span class="pb"><i style="width:${(o.got / o.qty * 100).toFixed(1)}%"></i></span></span>`;
|
||
}).join('') : '<span class="none">No orders yet</span>';
|
||
const box = $('w-orders');
|
||
if (box.innerHTML !== h) box.innerHTML = h;
|
||
}
|
||
function renderBlueprintTab(body) {
|
||
const lib = libLoad();
|
||
let h = `<p class="note">Select the Blueprint tool (5), drag over something you built to copy it, then click to place a ghost. Your bot builds ghosts from the materials in your bag when it is within reach. Right-click clears ghosts.</p>`;
|
||
h += `<p class="label grouphead">Starter designs</p><p class="note">Optional. Tested builds to learn from or copy; place one as a ghost and your bot builds it from your bag. Clear the space first: ghosts wait for open air.</p><div class="cards">`;
|
||
STARTER_DESIGNS.forEach((d, k) => {
|
||
const bp = d.bp();
|
||
h += `<div class="card"><div class="h"><span>${esc(d.name)}</span><span class="chip">${bp.bw}×${bp.bh} blocks</span></div><div class="p">${esc(d.desc)}</div><div class="p">Build cost: ${esc(costText(bpCost(bp), 1))}</div><div style="display:flex;gap:6px;flex-wrap:wrap"><button class="btn" data-starter="${k}">Place it</button></div></div>`;
|
||
});
|
||
h += '</div><p class="label grouphead">Your blueprints</p>';
|
||
h += `<div class="set"><div><div>Clipboard</div><div class="d">${GAME.clip ? `${GAME.clip.bw} × ${GAME.clip.bh} blocks, ${GAME.clip.blocks.length} pieces${GAME.clip.machines.length ? `, ${GAME.clip.machines.length} machines` : ''}${GAME.clip.molds.length ? `, ${GAME.clip.molds.length} molds` : ''}. Needs ${esc(costText(bpCost(GAME.clip), 1) || 'nothing')}` : 'Empty'}</div></div><div class="ctl"><button class="btn" id="bp-save" ${GAME.clip ? '' : 'disabled'}>Save to library</button><button class="btn" id="bp-code" ${GAME.clip ? '' : 'disabled'}>Copy code</button></div></div>`;
|
||
h += `<div class="set"><div><div>Import a blueprint code</div><div class="d">Paste a code that starts with DGBP2.</div></div><div class="ctl"><button class="btn" id="bp-import">Paste code…</button></div></div>`;
|
||
h += `<p class="label grouphead">Library</p>`;
|
||
if (!lib.length) h += `<p class="note">No saved blueprints yet.</p>`;
|
||
else {
|
||
h += '<div class="cards">';
|
||
lib.forEach((it, k) => {
|
||
const ok = bpValid(it.bp);
|
||
h += `<div class="card"><div class="h"><span>${esc(it.name)}</span><span class="chip">${ok ? `${it.bp.bw}×${it.bp.bh}` : 'old format'}</span></div><div class="p">${ok ? esc(costText(bpCost(it.bp), 1) || 'free') : 'Made before the block grid. It cannot be placed.'}</div><div style="display:flex;gap:6px;flex-wrap:wrap">${ok ? `<button class="btn" data-bpload="${k}">Use</button><button class="btn ghost" data-bpcode="${k}">Copy code</button>` : ''}<button class="btn ghost danger" data-bpdel="${k}">Delete</button></div></div>`;
|
||
});
|
||
h += '</div>';
|
||
}
|
||
body.innerHTML = h;
|
||
const sv = $('bp-save'); if (sv) sv.onclick = () => {
|
||
dialog('Name this blueprint', '<input class="text" id="bp-name" maxlength="40" placeholder="Copper smelter">', [
|
||
{ label: 'Cancel', cls: 'ghost' },
|
||
{ label: 'Save', cls: 'primary', fn: () => { const n = ($('bp-name').value || 'Untitled').trim(); const l = libLoad(); l.push({ name: n, bp: GAME.clip }); if (libSave(l)) toast(`Saved “${n}”.`); renderNotebook(); } },
|
||
]);
|
||
setTimeout(() => $('bp-name') && $('bp-name').focus(), 30);
|
||
};
|
||
const cc = $('bp-code'); if (cc) cc.onclick = () => copyCode(bpEncode(GAME.clip));
|
||
$('bp-import').onclick = () => {
|
||
dialog('Import blueprint', '<textarea class="code" id="bp-paste" placeholder="DGBP1:…"></textarea>', [
|
||
{ label: 'Cancel', cls: 'ghost' },
|
||
{ label: 'Import', cls: 'primary', fn: () => { try { GAME.clip = bpDecode($('bp-paste').value); GAME.paste = true; hudDirty = true; toast('Blueprint ready. Click to place it.'); closeAllModals(); } catch (e) { toast(e.message); return true; } } },
|
||
]);
|
||
};
|
||
body.querySelectorAll('[data-bpload]').forEach(b => b.onclick = () => { GAME.clip = libLoad()[+b.dataset.bpload].bp; GAME.paste = true; hudDirty = true; closeAllModals(); toast('Blueprint ready. Click to place it.'); });
|
||
body.querySelectorAll('[data-starter]').forEach(b => b.onclick = () => { GAME.clip = STARTER_DESIGNS[+b.dataset.starter].bp(); GAME.paste = true; hudDirty = true; closeAllModals(); toast('Click to place it. R flips it.'); });
|
||
body.querySelectorAll('[data-bpcode]').forEach(b => b.onclick = () => copyCode(bpEncode(libLoad()[+b.dataset.bpcode].bp)));
|
||
body.querySelectorAll('[data-bpdel]').forEach(b => b.onclick = () => { const l = libLoad(); l.splice(+b.dataset.bpdel, 1); libSave(l); renderNotebook(); });
|
||
}
|
||
function copyCode(code) {
|
||
const done = () => toast('Blueprint code copied.');
|
||
const fallback = () => { dialog('Blueprint code', `<p class="note">Select the text and copy it.</p><textarea class="code" id="bp-out" readonly>${esc(code)}</textarea>`, [{ label: 'Done', cls: 'primary' }]); setTimeout(() => { const t = $('bp-out'); if (t) { t.focus(); t.select(); } }, 30); };
|
||
try { navigator.clipboard.writeText(code).then(done, fallback); } catch (e) { fallback(); }
|
||
}
|
||
function controlsHtml() {
|
||
const kb = [
|
||
['Fly', 'W A S D'], ['Boost while flying', 'Shift'], ['Tool modifier (while not flying)', 'Shift'], ['Use tool', 'Left mouse'], ['Second action', 'Right mouse'],
|
||
['Tools', '1 – 4'], ['Cycle what you blow · piece', 'Q / E'], ['Switch build tab', 'Shift + Q / E'], ['Variant: material · direction · flip', 'R'],
|
||
['Tick/untick what is under the cursor (Vacuum)', 'R or T'], ['Nozzle or brush size', '[ ] or Shift + wheel'],
|
||
['Replace what is built (Build)', 'Hold Ctrl'], ['Copy a box of what you built', 'Hold C, drag'], ['Cut a box (refunded, paste it back)', 'Hold X, drag'], ['Paste again', 'V'], ['Undo building', 'Ctrl + Z'], ['Blueprint library', 'B'], ['Zoom', 'Mouse wheel'], ['Interact (gates, molds, fans, filters)', 'F'], ['Thermal lens', 'H'], ['Info overlay: filters, belt and tube directions', 'I'], ['Bot lamp', 'L'],
|
||
['Pick the piece under the cursor (with its direction and settings)', 'Middle mouse'], ['Notebook', 'N'], ['Clear all blueprint ghosts', 'Delete'], ['Save', 'Ctrl + S'], ['Pause · menu', 'Esc'], ['Mute', 'M'],
|
||
];
|
||
const pad = [
|
||
['Fly', 'Left stick'], ['Boost', 'Left stick pushed all the way'], ['Aim', 'Right stick'], ['Use tool · suck', 'RT'], ['Second action · blow', 'LT'], ['Tools', 'LB / RB'],
|
||
['Cycle what you blow · piece', 'D-pad ← →'], ['Brush size', 'D-pad ↑ ↓'], ['Interact', 'A'], ['Variant · tick material', 'X'], ['Thermal lens', 'Y'],
|
||
['Cancel', 'B'], ['Zoom', 'R3'], ['Lamp', 'L3'], ['Notebook', 'View / Back'], ['Menu', 'Menu / Start'],
|
||
];
|
||
const tools = [
|
||
['1 Resonance drill', 'Hold left click to send pulses from the bot toward the cursor: rock they shake hard enough shatters and falls as rubble, ore, coal or scrap, and trees come apart into wood chunks and acorns. Q and E set the pulse width: narrow bores a tunnel fast, wide clears a big cone slowly, medium is in between. R over a material tunes the drill to it, so only that shatters (great for pulling a vein out of the rock); R again untunes. Built blocks are never harmed. Better bits dig harder rock, faster and further.'],
|
||
['2 Vacuum', 'Left click sucks up what you start on; if anything is ticked in your bag (R over it, or its tick box), it takes only the ticked materials instead. Hold Shift while not flying to take everything. Right click blows the chosen material out of the bot toward the cursor; Q and E choose it, and it stays chosen. Q and E (or clicking in your bag) can also pick Ticked mix, an even mix of everything ticked, which is how you charge a furnace. The swatches next to the cursor always show what right click will blow. R ticks or unticks what is under the cursor.'],
|
||
['3 Build', 'Places pieces on a grid of 4×4 blocks: wall blocks, belts, tubes, filters, grates, gates, pipes, pumps and outlets, machines (fans, the Refiner) and molds. Drag for a line, hold Shift while dragging for a filled rectangle. Right click takes a piece back. Your bot flies through anything you build.'],
|
||
['4 Igniter', 'Heats a small spot to 640°C, and its flame reaches a few grains into a pile. Enough to light coal and wood that touch air.'],
|
||
['Copy, cut and paste', 'Hold C (copy) or X (cut) and drag a box over what you built; let go and it is picked up, ready to paste. Cut takes it down and refunds it to your bag. Click to place ghosts: your bot builds them from your bag when it is close. R flips, V pastes again, right click or Esc stops. Ctrl+Z undoes building. The notebook (B) keeps saved blueprints and a few starter designs.'],
|
||
];
|
||
const tbl = rows => '<div class="keys">' + rows.map(([a, b]) => `<div><span>${esc(a)}</span><span>${esc(b)}</span></div>`).join('') + '</div>';
|
||
return `<p class="label grouphead">Tools</p><div class="cards">${tools.map(([a, b]) => `<div class="card"><div class="h">${esc(a)}</div><div class="p">${esc(b)}</div></div>`).join('')}</div>
|
||
<p class="label grouphead">Keyboard and mouse</p>${tbl(kb)}<p class="label grouphead">Gamepad</p>${tbl(pad)}
|
||
<p class="label grouphead">First steps</p>
|
||
<p class="note">1. Drill down a little. Stone falls as rubble; coal (dark seams), copper ore (green speckles) and clay (reddish) come loose too.<br>
|
||
2. With the Vacuum, click on coal to suck up only coal, then the copper ore, rubble and clay you want.<br>
|
||
3. Build (3) a chamber of stone wall blocks with a one-block vent in the roof, a floor of clay Grates, and an Ingot mold right under the grates. The notebook's Blueprints tab has this as a starter design.<br>
|
||
4. Tick coal and copper ore in your bag and pick Ticked mix (Q/E, or click it at the bottom of the bag). Fly inside (the bot passes through what you build) and right click to blow an even mix onto the grates: about 50 of each. (Ore first and coal on top works too; coal buried under ore will not light.) Light a coal grain on top with the Igniter.<br>
|
||
5. When the ingot sets, bars drop out of the bottom of the mold onto whatever is under it. Leave one open block under it: a belt below that carries them off, or vacuum them up. Leftover metal stays for the next firing. F on the mold is optional: it cleans the trough, chips off slag and rakes the furnace grate. Scrap goes to your bag: throw it in with the next charge.<br>
|
||
6. Blow the bars into the trade dock: they sell the moment they drop through its grate, and bars pay far more than ore. Watch the Market page for orders that pay double. Money buys belts, screw tubes and fans; pay down the claim there too.<br>
|
||
7. From there: deeper ores (tin, zinc, silver, gold), alloys, fans for hotter fires, iron and steel.</p>
|
||
`;
|
||
}
|