Fewer, smaller messages: mold percentages, markers for machine heat, short status lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 23:03:45 -04:00
co-authored by Claude Opus 5.5
parent a15af6ef9f
commit 10911d4c86
25 changed files with 164 additions and 106 deletions
+3 -3
View File
@@ -3,7 +3,7 @@
function removePieceAt(x, y) {
droppedLast = 0;
const msg = removePieceAt0(x, y);
return msg && droppedLast ? msg.replace('taken back.', 'taken back. Your bag is full, so the rest dropped where it stood.') : msg;
return msg && droppedLast ? msg.replace('taken back.', 'taken back; bag full, excess dropped here.') : msg;
}
function removePieceAt0(x, y) {
if (x < 0 || y < 0 || x >= W || y >= H) return '';
@@ -15,7 +15,7 @@ function removePieceAt0(x, y) {
if (mach[i] && machines[mach[i]]) {
const P = PIECE_BY_ID[machines[mach[i]].type];
if (P.fixed) return '';
if (siloFull(machines[mach[i]])) return `Empty the silo first: it holds ${machines[mach[i]].sn.toLocaleString('en-US')} ${MD[machines[mach[i]].sm].name.toLowerCase()}. Press F on it to open its gate.`;
if (siloFull(machines[mach[i]])) return 'Empty the silo first; press F to open its gate.';
removeMachine(mach[i], true, false); return `${P.name} taken back.`;
}
const mo = moldAt(x, y);
@@ -80,7 +80,7 @@ function interactAt(x, y) {
return blockP[pb] === BP_PUMP ? (blockV[pb] & 4 ? 'Pump off.' : 'Pump on.') : (blockV[pb] & 4 ? 'Outlet closed.' : 'Outlet open.');
}
const id = mach[y * W + x];
if (id && machines[id] && machines[id].type === 'silo') { const mm = machines[id]; mm.open = !mm.open; return mm.open ? (mm.sn ? 'Silo gate open.' : 'Silo gate open. It is empty.') : 'Silo gate shut.'; }
if (id && machines[id] && machines[id].type === 'silo') { const mm = machines[id]; mm.open = !mm.open; return mm.open ? (mm.sn ? 'Silo gate open.' : 'Silo gate open; it is empty.') : 'Silo gate shut.'; }
if (id && machines[id] && machines[id].type === 'fan') { machines[id].on = !machines[id].on; powerDirty = true; return machines[id].on ? 'Fan on.' : 'Fan off.'; }
return '';
}
+1 -1
View File
@@ -8,7 +8,7 @@ function canPlaceBlock(bx, by) {
if (blockP[by * BW + bx]) return 'Something is already built there';
for (const i of blockCellList(bx, by)) {
if (mach[i] || part[i]) return 'Something is already built there';
if (PH[mat[i]] === PH_SOLID) return 'Needs open space. Clear the rock first.';
if (PH[mat[i]] === PH_SOLID) return 'Needs open space; clear the rock first.';
if (occ[i]) return 'A bar is in the way';
}
return '';
+1 -1
View File
@@ -80,7 +80,7 @@ function footprintBlocker(bx, by, bw, bh) {
for (let y = by * BS; y < (by + bh) * BS; y++) for (let x = bx * BS; x < (bx + bw) * BS; x++) {
if (x < 0 || y < 0 || x >= W || y >= H) return 'Out of bounds';
const i = y * W + x;
if (PH[mat[i]] === PH_SOLID && (flags[i] & F_NAT)) return 'Needs open space. Clear the rock first.';
if (PH[mat[i]] === PH_SOLID && (flags[i] & F_NAT)) return 'Needs open space; clear the rock first.';
if (occ[i]) return 'A bar is in the way';
if (mach[i] && machines[mach[i]] && PIECE_BY_ID[machines[mach[i]].type].fixed) return 'That cannot be replaced';
if (mach[i] && siloFull(machines[mach[i]])) return 'Empty the silo first';
+2 -2
View File
@@ -53,11 +53,11 @@ function toolPulse(ax, ay, width, tune) {
if (HARD[m] > tier.hard) {
const need = BIT_TIERS.find(b => b.hard >= HARD[m]);
const nb = need ? need.name.toLowerCase() : 'better bit';
status = HARD[m] > 6 ? `${MD[m].name} cannot be drilled.` : `${MD[m].name} is too hard for your ${tier.name.toLowerCase()}. Needs ${/^[aeiou]/.test(nb) ? 'an' : 'a'} ${nb}.`;
status = HARD[m] > 6 ? `${MD[m].name} cannot be drilled.` : `${MD[m].name} is too hard; needs ${/^[aeiou]/.test(nb) ? 'an' : 'a'} ${nb}.`;
}
break;
}
if (!status && d > reach) status = `Out of reach. A ${Wd.name} pulse from your ${tier.name.toLowerCase()} reaches ${reach} grains from the bot.`;
if (!status && d > reach) status = `Out of reach; a ${Wd.name} pulse reaches ${reach} grains.`;
return { fired: true, status };
}
const THIN_BONUS = 0.75, CRUMBLE_MAX = 16;
+1 -1
View File
@@ -131,4 +131,4 @@ function canAfford(cost, mult) { for (const [m, n] of cost) if ((m === CASH ? mo
function payCost(cost, mult) { for (const [m, n] of cost) { if (m === CASH) money -= n * mult; else bagTake(m, n * mult); } }
function fmtMoney(v) { return '$' + Math.round(v).toLocaleString('en-US'); }
function costText(cost, mult) { return cost.map(([m, n]) => { const k = n * (mult || 1); return m === CASH ? fmtMoney(k) : `${k} ${MD[m].name.toLowerCase()}${PH[m] === PH_ITEM && k !== 1 ? 's' : ''}`; }).join(' + '); }
function needText(cost, mult) { const c = cost.find(([m, n]) => (m === CASH ? money : bag[m]) < n * (mult || 1)); return c && c[0] === CASH ? `Costs ${fmtMoney(c[1] * (mult || 1))}; you have ${fmtMoney(money)}.` : 'Not enough ' + costText(cost, mult); }
function needText(cost, mult) { const c = cost.find(([m, n]) => (m === CASH ? money : bag[m]) < n * (mult || 1)); return c && c[0] === CASH ? `Costs ${fmtMoney(c[1] * (mult || 1))}; you have ${fmtMoney(money)}.` : 'Not enough ' + costText(c ? [c] : cost, mult) + '.'; }
+3 -3
View File
@@ -82,11 +82,11 @@ function machinesTick() {
const sat = POWER_FREE ? 1 : net ? net.sat : 0;
mm.angle += sat * 0.5 * (mm.on ? 1 : 0);
if (mm.on && sat > 0.05) fanBlow(mm, sat);
// a fan getting close to melting says so (once every few seconds)
// Keep the measured heat for the marker and hover panel.
if ((tick + mm.id) % 30 === 0) {
let hotT = 0;
for (let y = mm.y; y < mm.y + 4; y++) for (let x = mm.x; x < mm.x + 4; x++) { const tt = temp[y * W + x]; if (tt > hotT) hotT = tt; }
if (hotT > 760 && (!mm.warnT || tick - mm.warnT > 600)) { mm.warnT = tick; emit({ t: 'toast', text: `A fan is at ${Math.round(hotT)}°C and melts at 1085°C. Move it out of the heat: outside the furnace wall, blowing in through a grate.` }); }
mm.hot = Math.round(hotT);
}
}
// belts and filters move what rests on them every other tick: half a grain per tick
@@ -118,7 +118,7 @@ function machinesTick() {
if (m === EMPTY || PH[m] !== PH_POWDER) { if (m === EMPTY || PH[m] !== PH_SOLID) tubeIntake(i, p, dx, dy); continue; }
if (stamp[i] === tickStamp) continue;
if (temp[i] > 1000 && rnd() < 0.02) {
part[i] = 0; markPow(i, false); emit({ t: 'toast', text: 'A screw tube melted. Copper softens around 1000°C.' }); wakeIdx(i);
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
continue;
}
const s = netSat(i);
+2 -2
View File
@@ -112,7 +112,7 @@ function canPlaceMachine(type, bx, by) {
for (const [cx, cy] of machineFootprint(type, bx * BS, by * BS)) {
const i = cy * W + cx;
if (mach[i] || part[i]) return 'Something is already built there';
if (PH[mat[i]] === PH_SOLID) return (flags[i] & F_NAT) ? 'Needs open space. Clear the rock first.' : 'Something is already built there';
if (PH[mat[i]] === PH_SOLID) return (flags[i] & F_NAT) ? 'Needs open space; clear the rock first.' : 'Something is already built there';
if (occ[i]) return 'A bar is in the way';
}
for (let y = by; y < by + P.bh; y++) for (let x = bx; x < bx + P.bw; x++) if (blockP[y * BW + x]) return 'Something is already built there';
@@ -150,5 +150,5 @@ function removeMachine(id, refund, scrap) {
}
machines[id] = null;
if (refund) { refundAt(PIECE_BY_ID[mm.type].cost, 1, mm.x + mm.w / 2, mm.y + mm.h / 2); undoNote({ t: 'mach-', type: mm.type, bx: mm.x / BS, by: mm.y / BS, dir: mm.dir, on: mm.on }); }
if (scrap) emit({ t: 'toast', text: `${PIECE_BY_ID[mm.type].name} was destroyed by heat.` });
if (scrap) emit({ t: 'warn', x: mm.x + mm.w / 2, y: mm.y, text: `${PIECE_BY_ID[mm.type].name} destroyed` });
}
+15 -24
View File
@@ -11,13 +11,13 @@ function canPlaceMold(kind, bx, by) {
if (bx < 0 || by < 0 || bx + md.bw > BW || by + md.bh > BH) return 'Out of bounds';
for (let y = by; y < by + md.bh; y++) for (let x = bx; x < bx + md.bw; x++) if (blockP[y * BW + x]) return 'Something is already built there';
const { walls, cavs } = moldCellsAt(kind, bx * BS, by * BS);
for (const i of walls.concat(...cavs)) if (!cellBuildable(i)) return PH[mat[i]] === PH_SOLID && (flags[i] & F_NAT) ? 'Needs open space. Clear the rock first.' : 'Something is already built there';
for (const i of walls.concat(...cavs)) if (!cellBuildable(i)) return PH[mat[i]] === PH_SOLID && (flags[i] & F_NAT) ? 'Needs open space; clear the rock first.' : 'Something is already built there';
if (moldAt(bx * BS + 1, by * BS + 1)) return 'There is already a mold there';
return null;
}
function newMoldRecord(kind, x, y, wall) {
const md = MOLDS[kind];
return { id: (molds.length ? molds[molds.length - 1].id : 0) + 1, kind, x, y, w: md.w, h: md.h, wall, cavs: moldCellsAt(kind, x, y).cavs, ready: [], state: 'empty', metal: 0, fill: 0, flaw: '' };
return { id: (molds.length ? molds[molds.length - 1].id : 0) + 1, kind, x, y, w: md.w, h: md.h, wall, cavs: moldCellsAt(kind, x, y).cavs, ready: [], state: 'empty', metal: 0, fill: 0, flaw: '', blocked: false };
}
function addMold(kind, bx, by, wallLoose, free) {
const md = MOLDS[kind];
@@ -132,7 +132,7 @@ function rakeAbove(mo) {
if (n) wakeXY(mo.x + (mo.w >> 1), mo.y - 8);
return n;
}
// After a knock-out: if the furnace over the mold is badly loaded, say what to change
// Hover advice when the furnace over the mold is badly loaded.
function furnaceJunkHint(mo) {
let ore = 0, fuel = 0, open = 0;
for (let x = mo.x; x < mo.x + mo.w; x++) for (let y = mo.y - 1, k = 0; y >= 0 && k < 48; y--, k++) {
@@ -161,6 +161,7 @@ function unplugMold(mo) {
const grains = n => `${n} ${n === 1 ? 'grain' : 'grains'}`;
const barWord = (m, n) => m === GLASS ? (n === 1 ? 'glass brick' : 'glass bricks') : n === 1 ? 'bar' : 'bars';
function ejectIngot(mo) {
mo.blocked = false;
let r = troughState(mo), n = 0;
const metal = r.metal, x = mo.x + (mo.w >> 1), y = mo.y + mo.h;
if (r.st === 'ready') {
@@ -173,6 +174,7 @@ function ejectIngot(mo) {
free = true;
if (spawnBar(BAR_OF[metal], sx, y, (rnd() - 0.5) * 0.15, 0.2, T)) n++;
}
mo.blocked = !n && !free;
if (n) {
// Take from the top so the short stub stays on the trough floor.
let take = n * GRAINS_PER_BAR;
@@ -203,7 +205,7 @@ function ejectIngot(mo) {
setCell(dest, loose, temp[i], 0); slot++; placed = true; break;
}
}
if (!placed) continue;
if (!placed) { mo.blocked = true; continue; }
}
setCell(i, EMPTY, AMB[(i / W) | 0], 0); pushed++;
}
@@ -241,16 +243,6 @@ function moldAt(x, y) {
for (const mo of molds) if (x >= mo.x && y >= mo.y && x < mo.x + mo.w && y < mo.y + mo.h) return mo;
return null;
}
// What a knock-out sent where, as one short sentence after the main message
function knockNote(scrap, other, chipped, raked) {
let s = '';
if (scrap) s += ` The leftover ${grains(scrap)} of scrap went into your bag${other ? ', with the grit that got in' : ''}.`;
else if (other) s += ' The grit that got in went into your bag.';
if (chipped && raked) s += ' The slag was chipped off and the grate raked clean.';
else if (chipped) s += ' The slag was chipped off.';
else if (raked) s += ' Slag and ash were raked off the grate.';
return s;
}
function tally(got) {
let scrap = 0, other = 0;
for (const [k, c] of got) { if (PH[k] === PH_POWDER && MELTTO[k] && CASTMETAL[MELTTO[k]] && !SMELTP[k]) scrap += c; else other += c; }
@@ -264,39 +256,38 @@ function collectMold(mo) {
const scrap = g.scrap + u.metal;
if (!scrap && !g.other && !g.dross && !u.chipped && !u.raked) return 'The mold is empty.';
emit({ t: 'sfx', s: 'ui' });
return 'Knocked the mold clean.' + knockNote(scrap, g.other, g.dross + u.chipped, u.raked);
return 'Knocked the mold clean.';
}
let msg = '';
if (mo.state === 'short' && !(mo.knockAsk != null && tick - mo.knockAsk >= 0 && tick - mo.knockAsk < 300)) {
// an unfinished casting can still be topped up, so the first press only says so
mo.knockAsk = tick;
return (mo.flaw || 'The casting is not full yet.').replace(', or press F to knock it out as scrap.', '.') + ' Press F again to knock it out as scrap.';
return 'Not full; press F again to knock it out as scrap.';
}
if (mo.state === 'flawed' || mo.state === 'short') {
// a spoiled or unfinished casting is knocked out as scrap, so the mold is ready for another go
const short = mo.state === 'short', g = tally(clearCavity(mo.cavs.flat())), u = unplugMold(mo);
const short = mo.state === 'short'; clearCavity(mo.cavs.flat()); unplugMold(mo);
mo.state = 'empty'; mo.ready = [];
emit({ t: 'sfx', s: 'ui' });
const what = mo.kind === 'bit' ? 'bit' : 'casting';
return (short ? `The ${what} was not full, so it was knocked out as scrap.` : `The ${what} was spoiled, so it was knocked out as scrap.`)
+ knockNote(g.scrap + u.metal, g.other, g.dross + u.chipped, u.raked) + furnaceJunkHint(mo);
return (short ? `Unfinished ${what} knocked out as scrap.` : `Spoiled ${what} knocked out as scrap.`);
}
const tier = METAL_TIER[mo.metal];
const cav = mo.cavs[0];
if (tier <= 0) { msg = `${MD[mo.metal] ? MD[mo.metal].name : 'That metal'} is too ${mo.metal === GLASS ? 'brittle' : 'soft'} for a ${mo.kind === 'bit' ? 'drill bit' : 'tank'}. The casting broke into scrap, which went into your bag.`; }
if (tier <= 0) { msg = `${MD[mo.metal] ? MD[mo.metal].name : 'That metal'} is too ${mo.metal === GLASS ? 'brittle' : 'soft'} for a ${mo.kind === 'bit' ? 'drill bit' : 'tank'}; returned as scrap.`; }
else if (mo.kind === 'bit') {
if (tier > player.bitTier) { player.bitTier = tier; msg = `New drill bit: ${BIT_TIERS[tier].name}.`; emit({ t: 'upgrade', kind: 'bit', tier }); }
else { msg = `You already have a ${BIT_TIERS[player.bitTier].name.toLowerCase()}. The casting broke into scrap, which went into your bag.`; }
else { msg = `You already have a ${BIT_TIERS[player.bitTier].name.toLowerCase()}; returned as scrap.`; }
} else {
if (tier > player.tankTier) { player.tankTier = tier; msg = `New bag: ${TANK_TIERS[tier].name}, holds ${TANK_TIERS[tier].cap.toLocaleString('en-US')}.`; emit({ t: 'upgrade', kind: 'tank', tier }); }
else { msg = 'Your tank is already as good. The casting broke into scrap, which went into your bag.'; }
else { msg = 'Your tank is already as good; returned as scrap.'; }
}
// a casting you could not use breaks into scrap, which goes to your bag
if (msg.startsWith('New')) for (const i of cav) setCell(i, EMPTY, AMB[(i / W) | 0], 0);
else clearCavity(cav);
const u = unplugMold(mo);
unplugMold(mo);
mo.state = 'empty'; stats.cast++;
return msg + knockNote(0, 0, u.chipped, u.raked) + furnaceJunkHint(mo);
return msg;
}
function removeMold(mo) {
undoNote({ t: 'mold-', kind: mo.kind, bx: mo.x / BS, by: mo.y / BS, wall: mo.wall });
+2 -2
View File
@@ -42,11 +42,11 @@ function toolDig(cx, cy, r) {
stats.dug += dug;
let status = '';
void 0;
if (far && !dug && !tooHard) status = `Out of reach. Your ${tier.name.toLowerCase()} reaches ${drillReach()} grains from the bot.`;
if (far && !dug && !tooHard) status = `Out of reach; your bit reaches ${drillReach()} grains.`;
if (tooHard && !dug) {
const need = BIT_TIERS.find(b => b.hard >= HARD[tooHard]);
const nb = need ? need.name.toLowerCase() : 'better bit';
status = HARD[tooHard] > 6 ? `${MD[tooHard].name} cannot be drilled.` : `${MD[tooHard].name} is too hard for your ${tier.name.toLowerCase()}. Needs ${/^[aeiou]/.test(nb) ? 'an' : 'a'} ${nb}.`;
status = HARD[tooHard] > 6 ? `${MD[tooHard].name} cannot be drilled.` : `${MD[tooHard].name} is too hard; needs ${/^[aeiou]/.test(nb) ? 'an' : 'a'} ${nb}.`;
}
return { dug, status, tooHard };
}
+3 -3
View File
@@ -17,8 +17,8 @@ function undoLast() {
const rec = undoStack.pop();
if (!rec) return 'Nothing to undo.';
const prev = undoRec; undoRec = null;
let fails = 0, why = '';
const fail = (r) => { fails++; if (r && !why) why = r; };
let fails = 0;
const fail = () => { fails++; };
for (let k = rec.ops.length - 1; k >= 0; k--) {
const o = rec.ops[k];
if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); }
@@ -38,7 +38,7 @@ function undoLast() {
undoRec = prev;
powerDirty = true;
const n = rec.ops.length;
return fails ? `Undid ${rec.label}, but ${fails === n ? 'none' : fails} of it could not be put back${why ? ': ' + why.replace(/\.$/, '').toLowerCase() : ''}.` : `Undid ${rec.label}.`;
return fails ? `Undo incomplete: ${fails} of ${n} changes could not be put back.` : `Undid ${rec.label}.`;
}
function clearAllGhosts() { ghostBlocks.clear(); ghostMachines = []; ghostMolds = []; ghostBlocked.clear(); }
let ghostMissing = 0, ghostIter = null;
+3 -3
View File
@@ -28,7 +28,7 @@ function toolVacuum(cx, cy, r, pick) {
if (got & 1) emit({ t: 'suck', x, y, m });
if (--budget <= 0) return false;
});
return { got, full, status: full ? 'Bag full. Blow something out or cast a Tank mold.' : '' };
return { got, full, status: full ? 'Bag full; blow material out or cast a bigger tank.' : '' };
}
// Blow: grains leave the bot's nozzle and fly to the cursor, where they drop.
// Liquids need the Flask. Bars are thrown, one at a time.
@@ -45,11 +45,11 @@ function toolBlow(cx, cy, r, m, mix) {
let pickM = null;
if (mix && mix.length) {
const avail = mix.filter(k => bag[k] > 0 && PH[k] !== PH_ITEM && (PH[k] !== PH_LIQUID || player.flask));
if (!avail.length) return { placed: 0, status: 'Nothing ticked left to mix. Tick materials in your bag.' };
if (!avail.length) return { placed: 0, status: 'Tick materials in your bag to mix.' };
pickM = () => { for (let k = 0; k < avail.length; k++) { const c = avail[(blowRR++) % avail.length]; if (bag[c] > 0) return c; } return 0; };
m = avail[0];
}
if (!m) return { placed: 0, status: 'Pick something to blow out. Q and E cycle, or click it in your bag.' };
if (!m) return { placed: 0, status: 'Pick what to blow out: Q/E, or click it in your bag.' };
if (bag[m] <= 0) return { placed: 0, status: `No ${MD[m].name.toLowerCase()} left.` };
const ph = PH[m];
if (ph === PH_LIQUID && !player.flask) return { placed: 0, status: 'You need the Flask to carry liquids.' };