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 43eb4e23fd
commit 4d4d94cbd8
25 changed files with 164 additions and 106 deletions
+2
View File
@@ -175,6 +175,8 @@ above, including:
- **Ingot molds need no F.** They push junk and mixed metal out of the bottom as loose grains with the bars, throw slag away and rake the grate. Removing any mold returns its set metal: whole bars as bars, the rest as scrap. The owner asked what F was for once bars dropped on their own, and chose this over keeping F for cleaning. - **Ingot molds need no F.** They push junk and mixed metal out of the bottom as loose grains with the bars, throw slag away and rake the grate. Removing any mold returns its set metal: whole bars as bars, the rest as scrap. The owner asked what F was for once bars dropped on their own, and chose this over keeping F for cleaning.
- **Fewer, smaller messages.** Molds show only a small percentage, red when blocked or spoiled and gold when a casting waits for F; details are in the hover panel. Bars dropping, blocked molds and set castings no longer pop up cards or toasts. Fan heat is a red marker on the fan; melted tubes and machines get a short red label where they were. Status lines are one short sentence. The owner found the messages too large and too many.
## Ideas not built yet ## Ideas not built yet
- **Concrete**, as a stronger wall material from rubble. - **Concrete**, as a stronger wall material from rubble.
+9 -8
View File
@@ -257,9 +257,9 @@ How belts, filters and screw tubes move what is on or in them. What rests on a b
Building blocks: placing, integrity, removing, lines and rectangles of blocks. Building blocks: placing, integrity, removing, lines and rectangles of blocks.
- functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks - functions: blockOf, blockCellList, pieceCost, canPlaceBlock, placeBlock, blockIntegrity, removeBlock, blockLine, blockRect, blockInReach, blockIs, placeBlocks
### src/sim/molds.js (328 lines) ### src/sim/molds.js (319 lines)
Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal. Molds: cavities for castings; ingot troughs empty themselves below. Removal returns set metal.
- functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, knockNote, tally, collectMold, removeMold - functions: moldCellsAt, canPlaceMold, newMoldRecord, addMold, castMix, cavityState, troughState, clearCavity, moldPlugCells, rakeAbove, furnaceJunkHint, unplugMold, ejectIngot, checkMolds, moldAt, tally, collectMold, removeMold
- variables: DROSS, grains, barWord - variables: DROSS, grains, barWord
### src/sim/items.js (134 lines) ### src/sim/items.js (134 lines)
@@ -341,13 +341,13 @@ drawWorld: the per-grain pixel buffer (pixel look), also the input of the smooth
- functions: drawWorld - functions: drawWorld
- variables: TUBE_CLASS - variables: TUBE_CLASS
### src/render/machines.js (152 lines) ### src/render/machines.js (157 lines)
Machine overlays: fans, engines, refiner, cutter, silo and the rest, drawn on top of the world. Machine overlays: fans, engines, refiner, cutter, silo and the rest, drawn on top of the world.
- functions: cellRect, drawHazardRims, drawMachines, drawSilo - functions: cellRect, drawHazardRims, drawMachines, drawSilo
### src/render/overlays.js (230 lines) ### src/render/overlays.js (228 lines)
Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles. Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
- functions: spawnFloater, drawRain, drawFlowArrows, drawRunOutline, drawFilterTags, drawFloaters, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles - functions: spawnFloater, drawRain, drawFlowArrows, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
### src/render/smooth.js (430 lines) ### src/render/smooth.js (430 lines)
Smooth look: a WebGL2 pass that turns the per-grain buffers drawWorld fills (colour + class, colour behind, trees, occupancy fields, light, glow) into rounded, outlined, softly lit shapes at screen resolution. The 2D canvas then draws this image as its world layer, and every overlay (machines, bot, previews, labels) goes on top as before. Smooth look: a WebGL2 pass that turns the per-grain buffers drawWorld fills (colour + class, colour behind, trees, occupancy fields, light, glow) into rounded, outlined, softly lit shapes at screen resolution. The 2D canvas then draws this image as its world layer, and every overlay (machines, bot, previews, labels) goes on top as before.
@@ -402,7 +402,7 @@ The selected tool and piece: R / Q / E, drill width, filter picker, the selectio
- functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleCollect, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag - functions: pieceUsesWall, setDrillWidth, tuneUnderCursor, cycleItem, cycleWall, rotate, toggleCollect, pickFilterUnderCursor, toggleFilterMat, toggleCollectUnderCursor, setBrush, pieceV, pieceExtra, filterChoices, matList, filterParts, filterSentence, applyFilterRun, openFilterPicker, filterBlockAt, chipCost, pieceCostNow, renderSelCard, renderBag
- variables: lastBagKey - variables: lastBagKey
### src/ui/info.js (167 lines) ### src/ui/info.js (170 lines)
Discovery cards, hover info (I) and the cursor label. Discovery cards, hover info (I) and the cursor label.
- functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel - functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel
@@ -411,7 +411,7 @@ Keyboard and mouse listeners.
- functions: isTyping - functions: isTyping
- variables: canvas - variables: canvas
### src/ui/actions.js (244 lines) ### src/ui/actions.js (242 lines)
What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge What input does: overlays, F, pick, copy / cut / paste, undo, building and tool use each frame. Which overlays are on, shown as a coloured frame round the screen and a named badge
- functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick - functions: updateOverlayUI, toggleLens, doInteract, pickUnderCursor, aimBlock, stampOrigin, selModeHeld, startPaste, stopSelectAndPaste, plural, piecesIn, doUndo, endRmbRecord, dragDirFor, blockLabel, primaryPress, primaryRelease, secondaryPress, toolTick
- variables: ovShown - variables: ovShown
@@ -426,7 +426,7 @@ Previews drawn under the cursor: build ghosts, drill aim and pulses, selections,
- functions: blockRectPx, drawGrid, previewColor, crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim, drawPreviews, drawTag, drawSelection, drawPastePreview - functions: blockRectPx, drawGrid, previewColor, crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim, drawPreviews, drawTag, drawSelection, drawPastePreview
- variables: CRACK_N, CRACK_LV, SEL_COL - variables: CRACK_N, CRACK_LV, SEL_COL
### src/ui/events.js (50 lines) ### src/ui/events.js (42 lines)
Events from the simulation (emit) turned into sounds, toasts and cards. Events from the simulation (emit) turned into sounds, toasts and cards.
- functions: drainEvents - functions: drainEvents
@@ -469,4 +469,5 @@ The frame loop and boot.
- `tools/experiments/worldgen.js`: World generation report: time to generate, how many cells of each material, where the bot starts, and what idle ticks and ticks after digging cost. - `tools/experiments/worldgen.js`: World generation report: time to generate, how many cells of each material, where the bot starts, and what idle ticks and ticks after digging cost.
- `tools/scenes/belt-heap.js`: A belt feeding an upward screw: sand underneath a deep heap of copper ore. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7 - `tools/scenes/belt-heap.js`: A belt feeding an upward screw: sand underneath a deep heap of copper ore. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7
- `tools/scenes/clear-sky.js`: A clear work area left of the bot's starting point (the trade dock is to its right): open sky 160 grains wide and 40 tall, over a flat stone floor. Money and a bag of building stuff to build with. Sets FLOOR (the first row of the floor), BX, BY (the leftmost block of the area that stands on the floor) and CAM (the middle of the area). - `tools/scenes/clear-sky.js`: A clear work area left of the bot's starting point (the trade dock is to its right): open sky 160 grains wide and 40 tall, over a flat stone floor. Money and a bag of building stuff to build with. Sets FLOOR (the first row of the floor), BX, BY (the leftmost block of the area that stands on the floor) and CAM (the middle of the area).
- `tools/scenes/mold-labels.js`: Run after clear-sky.js: filling, blocked, set, and hot machinery in one view.
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7 - `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
+22
View File
@@ -4,6 +4,28 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-23: fewer, smaller messages (Claude Code, with Codex)
Note: "lets change the engot mold and other status messages. they are way too large and just too
many messages…". The owner chose molds, machine warnings and shorter status lines.
- `src/render/overlays.js`: every mold always shows a small percentage (`moldLabel`), gold
`100% · F` for a set bit or tank, red when spoiled or when an ingot mold is blocked
(`mo.blocked`, set in `ejectIngot`, not saved). Details are in the hover panel (`info.js`),
which also carries the furnace-loading advice that F messages used to append.
- `src/ui/events.js`: no cards or toasts for bars dropping, blocked molds or set castings (the
sounds stay). Fan heat is a red ! marker on the fan (`mm.hot`, hover shows °C); melted tubes
and machines destroyed by heat get a short red floating label (`warn` event, coloured
`spawnFloater`) instead of a toast.
- Status lines shortened to one sentence across `src/sim` (molds, actions, blocks, blueprints,
drill, items, machines, tools, undo, vacuum) and `src/ui` (actions, selection).
- Tests: `play.py` records every status line and checks none is over 90 characters (longest
now 55); mold label and no-toast checks. `t_molds` (message lengths, `mo.blocked`), `t_build`
(hot fan: `mm.hot`, no toast), `t_world`. All 11 headless suites and `play.py` pass.
- Shots: `shots/messages-pixel.png`, `shots/messages-smooth.png` (filling, blocked, set bit,
hot fan).
- Open: the first-ever bar drop no longer shows an explaining card; the notebook still explains.
## 2026-10-23: ingot molds need no F; removing a mold returns its metal (Claude Code, with Codex) ## 2026-10-23: ingot molds need no F; removing a mold returns its metal (Claude Code, with Codex)
Note: "now that the ingot molds drop the ingots, what is the purpose of F…". The owner chose no F Note: "now that the ingot molds drop the ingots, what is the purpose of F…". The owner chose no F
+5
View File
@@ -52,6 +52,11 @@ function drawMachines() {
const a = mm.angle + k * Math.PI * 2 / 3; const a = mm.angle + k * Math.PI * 2 / 3;
ctx.beginPath(); ctx.ellipse(cx + Math.cos(a) * sw * 0.18, cy + Math.sin(a) * sw * 0.18, sw * 0.17, sw * 0.07, a, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(cx + Math.cos(a) * sw * 0.18, cy + Math.sin(a) * sw * 0.18, sw * 0.17, sw * 0.07, a, 0, Math.PI * 2); ctx.fill();
} }
if (mm.hot > 760) {
const sz = Math.max(10, zp * 2), x = sx + sw - sz * 0.6, y = sy - sz * 0.4;
ctx.save(); ctx.fillStyle = '#b84332'; ctx.beginPath(); ctx.roundRect(x, y, sz, sz, sz * 0.2); ctx.fill();
ctx.fillStyle = '#fff'; ctx.font = `700 ${sz * 0.85}px monospace`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('!', x + sz / 2, y + sz / 2); ctx.restore();
}
// direction arrow // direction arrow
const dx = [1, 0, -1, 0][mm.dir], dy = [0, 1, 0, -1][mm.dir]; const dx = [1, 0, -1, 0][mm.dir], dy = [0, 1, 0, -1][mm.dir];
ctx.strokeStyle = sat > 0.05 && mm.on ? 'rgba(160,220,255,0.7)' : 'rgba(160,170,180,0.35)'; ctx.lineWidth = Math.max(1, zp * 0.35); ctx.strokeStyle = sat > 0.05 && mm.on ? 'rgba(160,220,255,0.7)' : 'rgba(160,170,180,0.35)'; ctx.lineWidth = Math.max(1, zp * 0.35);
+12 -14
View File
@@ -1,5 +1,5 @@
// Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles. // Other overlays: floating text, rain and snow, flow arrows, filter tags, molds, the bot, particles.
function spawnFloater(x, y, text) { R.floaters = R.floaters || []; if (R.floaters.length < 30) R.floaters.push({ x, y, text, life: 70 }); } function spawnFloater(x, y, text, colour) { R.floaters = R.floaters || []; if (R.floaters.length < 30) R.floaters.push({ x, y, text, colour, life: 70 }); }
// Rain as thin falling streaks, snow as soft flakes // Rain as thin falling streaks, snow as soft flakes
function drawRain() { function drawRain() {
if (!flyers.length) return; if (!flyers.length) return;
@@ -133,33 +133,31 @@ function drawFloaters() {
if (f.life <= 0) { fl.splice(k, 1); continue; } if (f.life <= 0) { fl.splice(k, 1); continue; }
const s = worldToScreenPx(f.x, f.y), a = Math.min(1, f.life / 25); const s = worldToScreenPx(f.x, f.y), a = Math.min(1, f.life / 25);
ctx.fillStyle = `rgba(14,12,11,${0.6 * a})`; ctx.fillText(f.text, s.x + 1, s.y + 1); ctx.fillStyle = `rgba(14,12,11,${0.6 * a})`; ctx.fillText(f.text, s.x + 1, s.y + 1);
ctx.fillStyle = `rgba(255,210,122,${a})`; ctx.fillText(f.text, s.x, s.y); ctx.globalAlpha = a; ctx.fillStyle = f.colour || '#ffd27a'; ctx.fillText(f.text, s.x, s.y); ctx.globalAlpha = 1;
} }
ctx.restore(); ctx.restore();
} }
function drawMolds(hoverMold) { function moldLabel(mo) { return mo.state === 'ready' && mo.kind !== 'ingot' ? '100% · F' : Math.round(mo.fill * 100) + '%'; }
const ctx = R.ctx, zp = R.zp; function drawMolds() {
const ctx = R.ctx, zp = R.zp, fs = Math.max(9, Math.round(zp * 2));
ctx.save(); ctx.save();
if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace'; if (!R.monoFont) R.monoFont = getComputedStyle(document.documentElement).getPropertyValue('--font-mono').trim() || 'monospace';
ctx.font = `600 ${Math.max(10, Math.round(zp * 3.4))}px ${R.monoFont}`; ctx.font = `600 ${fs}px ${R.monoFont}`;
ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
for (const mo of molds) { for (const mo of molds) {
const [sx, sy, sw, sh] = cellRect(mo.x, mo.y, mo.w, mo.h); const [sx, sy, sw, sh] = cellRect(mo.x, mo.y, mo.w, mo.h);
if (sx > R.canvas.width || sy > R.canvas.height || sx + sw < 0 || sy + sh < 0) continue; if (sx > R.canvas.width || sy > R.canvas.height || sx + sw < 0 || sy + sh < 0) continue;
const ready = mo.state === 'ready', flawed = mo.state === 'flawed'; const ready = mo.state === 'ready' && mo.kind !== 'ingot', bad = mo.state === 'flawed' || mo.blocked;
ctx.setLineDash([zp, zp]); ctx.setLineDash([zp, zp]);
ctx.strokeStyle = ready ? 'rgba(255,200,90,0.95)' : flawed ? 'rgba(230,90,70,0.8)' : 'rgba(200,190,170,0.4)'; ctx.strokeStyle = bad ? '#ff8a72' : ready ? '#ffcf7a' : 'rgba(200,190,170,0.4)';
ctx.lineWidth = Math.max(1, zp * 0.3); ctx.lineWidth = Math.max(1, zp * 0.3);
ctx.strokeRect(sx - zp * 0.5, sy - zp * 0.5, sw + zp, sh + zp); ctx.strokeRect(sx - zp * 0.5, sy - zp * 0.5, sw + zp, sh + zp);
ctx.setLineDash([]); ctx.setLineDash([]);
if (ready || flawed || mo === hoverMold || mo.state === 'filling' || mo.state === 'short') { const label = moldLabel(mo), tw = ctx.measureText(label).width + fs * 0.6;
const label = ready ? `${mo.kind === 'ingot' ? (mo.ready.length === 1 ? 'Bar' : `${mo.ready.length} bars`) : mo.kind === 'bit' ? 'Bit' : 'Tank'} set${mo.kind === 'ingot' ? '' : ' · F'}` : flawed ? 'Flawed casting' : mo.state === 'short' ? 'Needs more metal' : mo.state === 'filling' ? `Filling ${Math.round(mo.fill * 100)}%` : MOLDS[mo.kind].name;
ctx.fillStyle = 'rgba(14,12,11,0.75)'; ctx.fillStyle = 'rgba(14,12,11,0.75)';
const tw = ctx.measureText(label).width + zp * 2; ctx.fillRect(sx + sw / 2 - tw / 2, sy - zp - fs * 1.3, tw, fs * 1.3);
ctx.fillRect(sx + sw / 2 - tw / 2, sy - zp * 5.2, tw, zp * 4.2); ctx.fillStyle = bad ? '#ff8a72' : ready ? '#ffcf7a' : '#e7dccb';
ctx.fillStyle = ready ? '#ffcf7a' : flawed ? '#ff8a72' : '#e7dccb'; ctx.fillText(label, sx + sw / 2, sy - zp - fs * 0.15);
ctx.fillText(label, sx + sw / 2, sy - zp * 1.4);
}
} }
ctx.restore(); ctx.restore();
} }
+3 -3
View File
@@ -3,7 +3,7 @@
function removePieceAt(x, y) { function removePieceAt(x, y) {
droppedLast = 0; droppedLast = 0;
const msg = removePieceAt0(x, y); 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) { function removePieceAt0(x, y) {
if (x < 0 || y < 0 || x >= W || y >= H) return ''; if (x < 0 || y < 0 || x >= W || y >= H) return '';
@@ -15,7 +15,7 @@ function removePieceAt0(x, y) {
if (mach[i] && machines[mach[i]]) { if (mach[i] && machines[mach[i]]) {
const P = PIECE_BY_ID[machines[mach[i]].type]; const P = PIECE_BY_ID[machines[mach[i]].type];
if (P.fixed) return ''; 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.`; removeMachine(mach[i], true, false); return `${P.name} taken back.`;
} }
const mo = moldAt(x, y); 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.'); 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]; 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.'; } 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 ''; return '';
} }
+1 -1
View File
@@ -8,7 +8,7 @@ function canPlaceBlock(bx, by) {
if (blockP[by * BW + bx]) return 'Something is already built there'; if (blockP[by * BW + bx]) return 'Something is already built there';
for (const i of blockCellList(bx, by)) { for (const i of blockCellList(bx, by)) {
if (mach[i] || part[i]) return 'Something is already built there'; 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'; if (occ[i]) return 'A bar is in the way';
} }
return ''; 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++) { 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'; if (x < 0 || y < 0 || x >= W || y >= H) return 'Out of bounds';
const i = y * W + x; 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 (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] && 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'; 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) { if (HARD[m] > tier.hard) {
const need = BIT_TIERS.find(b => b.hard >= HARD[m]); const need = BIT_TIERS.find(b => b.hard >= HARD[m]);
const nb = need ? need.name.toLowerCase() : 'better bit'; 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; 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 }; return { fired: true, status };
} }
const THIN_BONUS = 0.75, CRUMBLE_MAX = 16; 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 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 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 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; const sat = POWER_FREE ? 1 : net ? net.sat : 0;
mm.angle += sat * 0.5 * (mm.on ? 1 : 0); mm.angle += sat * 0.5 * (mm.on ? 1 : 0);
if (mm.on && sat > 0.05) fanBlow(mm, sat); 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) { if ((tick + mm.id) % 30 === 0) {
let hotT = 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; } 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 // 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 (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 (stamp[i] === tickStamp) continue;
if (temp[i] > 1000 && rnd() < 0.02) { 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; continue;
} }
const s = netSat(i); 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)) { for (const [cx, cy] of machineFootprint(type, bx * BS, by * BS)) {
const i = cy * W + cx; const i = cy * W + cx;
if (mach[i] || part[i]) return 'Something is already built there'; 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'; 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'; 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; 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 (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'; 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'; 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); 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'; if (moldAt(bx * BS + 1, by * BS + 1)) return 'There is already a mold there';
return null; return null;
} }
function newMoldRecord(kind, x, y, wall) { function newMoldRecord(kind, x, y, wall) {
const md = MOLDS[kind]; 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) { function addMold(kind, bx, by, wallLoose, free) {
const md = MOLDS[kind]; const md = MOLDS[kind];
@@ -132,7 +132,7 @@ function rakeAbove(mo) {
if (n) wakeXY(mo.x + (mo.w >> 1), mo.y - 8); if (n) wakeXY(mo.x + (mo.w >> 1), mo.y - 8);
return n; 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) { function furnaceJunkHint(mo) {
let ore = 0, fuel = 0, open = 0; 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++) { 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 grains = n => `${n} ${n === 1 ? 'grain' : 'grains'}`;
const barWord = (m, n) => m === GLASS ? (n === 1 ? 'glass brick' : 'glass bricks') : n === 1 ? 'bar' : 'bars'; const barWord = (m, n) => m === GLASS ? (n === 1 ? 'glass brick' : 'glass bricks') : n === 1 ? 'bar' : 'bars';
function ejectIngot(mo) { function ejectIngot(mo) {
mo.blocked = false;
let r = troughState(mo), n = 0; let r = troughState(mo), n = 0;
const metal = r.metal, x = mo.x + (mo.w >> 1), y = mo.y + mo.h; const metal = r.metal, x = mo.x + (mo.w >> 1), y = mo.y + mo.h;
if (r.st === 'ready') { if (r.st === 'ready') {
@@ -173,6 +174,7 @@ function ejectIngot(mo) {
free = true; free = true;
if (spawnBar(BAR_OF[metal], sx, y, (rnd() - 0.5) * 0.15, 0.2, T)) n++; if (spawnBar(BAR_OF[metal], sx, y, (rnd() - 0.5) * 0.15, 0.2, T)) n++;
} }
mo.blocked = !n && !free;
if (n) { if (n) {
// Take from the top so the short stub stays on the trough floor. // Take from the top so the short stub stays on the trough floor.
let take = n * GRAINS_PER_BAR; let take = n * GRAINS_PER_BAR;
@@ -203,7 +205,7 @@ function ejectIngot(mo) {
setCell(dest, loose, temp[i], 0); slot++; placed = true; break; 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++; 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; 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; 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) { function tally(got) {
let scrap = 0, other = 0; 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; } 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; const scrap = g.scrap + u.metal;
if (!scrap && !g.other && !g.dross && !u.chipped && !u.raked) return 'The mold is empty.'; if (!scrap && !g.other && !g.dross && !u.chipped && !u.raked) return 'The mold is empty.';
emit({ t: 'sfx', s: 'ui' }); 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 = ''; let msg = '';
if (mo.state === 'short' && !(mo.knockAsk != null && tick - mo.knockAsk >= 0 && tick - mo.knockAsk < 300)) { 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 // an unfinished casting can still be topped up, so the first press only says so
mo.knockAsk = tick; 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') { 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 // 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 = []; mo.state = 'empty'; mo.ready = [];
emit({ t: 'sfx', s: 'ui' }); emit({ t: 'sfx', s: 'ui' });
const what = mo.kind === 'bit' ? 'bit' : 'casting'; 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.`) return (short ? `Unfinished ${what} knocked out as scrap.` : `Spoiled ${what} knocked out as scrap.`);
+ knockNote(g.scrap + u.metal, g.other, g.dross + u.chipped, u.raked) + furnaceJunkHint(mo);
} }
const tier = METAL_TIER[mo.metal]; const tier = METAL_TIER[mo.metal];
const cav = mo.cavs[0]; 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') { 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 }); } 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 { } 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 }); } 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 // 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); if (msg.startsWith('New')) for (const i of cav) setCell(i, EMPTY, AMB[(i / W) | 0], 0);
else clearCavity(cav); else clearCavity(cav);
const u = unplugMold(mo); unplugMold(mo);
mo.state = 'empty'; stats.cast++; mo.state = 'empty'; stats.cast++;
return msg + knockNote(0, 0, u.chipped, u.raked) + furnaceJunkHint(mo); return msg;
} }
function removeMold(mo) { function removeMold(mo) {
undoNote({ t: 'mold-', kind: mo.kind, bx: mo.x / BS, by: mo.y / BS, wall: mo.wall }); 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; stats.dug += dug;
let status = ''; let status = '';
void 0; 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) { if (tooHard && !dug) {
const need = BIT_TIERS.find(b => b.hard >= HARD[tooHard]); const need = BIT_TIERS.find(b => b.hard >= HARD[tooHard]);
const nb = need ? need.name.toLowerCase() : 'better bit'; 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 }; return { dug, status, tooHard };
} }
+3 -3
View File
@@ -17,8 +17,8 @@ function undoLast() {
const rec = undoStack.pop(); const rec = undoStack.pop();
if (!rec) return 'Nothing to undo.'; if (!rec) return 'Nothing to undo.';
const prev = undoRec; undoRec = null; const prev = undoRec; undoRec = null;
let fails = 0, why = ''; let fails = 0;
const fail = (r) => { fails++; if (r && !why) why = r; }; const fail = () => { fails++; };
for (let k = rec.ops.length - 1; k >= 0; k--) { for (let k = rec.ops.length - 1; k >= 0; k--) {
const o = rec.ops[k]; const o = rec.ops[k];
if (o.t === 'block+') { if (blockP[o.by * BW + o.bx] === o.code) removeBlock(o.bx, o.by); } 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; undoRec = prev;
powerDirty = true; powerDirty = true;
const n = rec.ops.length; 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(); } function clearAllGhosts() { ghostBlocks.clear(); ghostMachines = []; ghostMolds = []; ghostBlocked.clear(); }
let ghostMissing = 0, ghostIter = null; 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 (got & 1) emit({ t: 'suck', x, y, m });
if (--budget <= 0) return false; 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. // 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. // 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; let pickM = null;
if (mix && mix.length) { if (mix && mix.length) {
const avail = mix.filter(k => bag[k] > 0 && PH[k] !== PH_ITEM && (PH[k] !== PH_LIQUID || player.flask)); 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; }; 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]; 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.` }; if (bag[m] <= 0) return { placed: 0, status: `No ${MD[m].name.toLowerCase()} left.` };
const ph = PH[m]; const ph = PH[m];
if (ph === PH_LIQUID && !player.flask) return { placed: 0, status: 'You need the Flask to carry liquids.' }; if (ph === PH_LIQUID && !player.flask) return { placed: 0, status: 'You need the Flask to carry liquids.' };
+14 -16
View File
@@ -30,7 +30,7 @@ function doInteract() {
openFilterPicker(Object.assign(filterInfo(fb), { dir: blockV[fb] & 128 ? 1 : 0 }), f => { openFilterPicker(Object.assign(filterInfo(fb), { dir: blockV[fb] & 128 ? 1 : 0 }), f => {
applyFilterRun(run, f); applyFilterRun(run, f);
GAME.filterCfg = { m: f.m.slice(), inv: f.inv }; GAME.pieceDir.filter = f.dir; GAME.filterCfg = { m: f.m.slice(), inv: f.inv }; GAME.pieceDir.filter = f.dir;
setStatus(`${run.length === 1 ? 'This filter' : `These ${run.length} filters`} now ${filterSentence(f)}, ${f.dir ? 'right' : 'left'}.`, 'info'); setStatus(`${run.length} ${run.length === 1 ? 'filter' : 'filters'} updated, carrying ${f.dir ? 'right' : 'left'}.`, 'info');
}, run.length > 1 ? `Filter (${run.length} in this row)` : 'Filter'); }, run.length > 1 ? `Filter (${run.length} in this row)` : 'Filter');
GAME.editRun = run; GAME.editRun = run;
return; return;
@@ -43,7 +43,7 @@ function doInteract() {
if (msg) break outer; if (msg) break outer;
} }
if (msg) { setStatus(msg, 'info'); if (!/^New (drill bit|bag)/.test(msg)) toast(msg); } if (msg) { setStatus(msg, 'info'); if (!/^New (drill bit|bag)/.test(msg)) toast(msg); }
else { setStatus('Nothing to use there. F works on gates, molds, fans and filters.', 'info'); } else { setStatus('Nothing to use there.', 'info'); }
} }
// Pick (middle mouse): the piece under the cursor becomes the one you build, with its // Pick (middle mouse): the piece under the cursor becomes the one you build, with its
// direction, wall material and filter settings. On loose material it picks that to blow out. // direction, wall material and filter settings. On loose material it picks that to blow out.
@@ -51,24 +51,22 @@ function pickUnderCursor() {
const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y); const ax = Math.floor(GAME.aim.x), ay = Math.floor(GAME.aim.y);
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return; if (ax < 0 || ay < 0 || ax >= W || ay >= H) return;
const i = ay * W + ax, m = mat[i], bi = ((ay / BS) | 0) * BW + ((ax / BS) | 0); const i = ay * W + ax, m = mat[i], bi = ((ay / BS) | 0) * BW + ((ax / BS) | 0);
let pid = null, note = ''; let pid = null;
const ghost = ghostBlocks.get(bi); const ghost = ghostBlocks.get(bi);
if (mach[i] && machines[mach[i]]) { const mm = machines[mach[i]]; pid = mm.type; if (PIECE_BY_ID[pid].dirs) GAME.pieceDir[pid] = mm.dir; } if (mach[i] && machines[mach[i]]) { const mm = machines[mach[i]]; pid = mm.type; if (PIECE_BY_ID[pid].dirs) GAME.pieceDir[pid] = mm.dir; }
else if (moldAt(ax, ay)) { const mo = moldAt(ax, ay); pid = mo.kind + 'mold'; GAME.wallMat = mo.wall; note = MD[WALL_OF[mo.wall]].name.toLowerCase(); } else if (moldAt(ax, ay)) { const mo = moldAt(ax, ay); pid = mo.kind + 'mold'; GAME.wallMat = mo.wall; }
else if (blockP[bi] || ghost) { else if (blockP[bi] || ghost) {
const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1]; const code = blockP[bi] || ghost[0], v = blockP[bi] ? blockV[bi] : ghost[1];
const P = PIECE_BY_CODE[code]; pid = P.id; const P = PIECE_BY_CODE[code]; pid = P.id;
if (P.code === BP_WALL) { GAME.wallMat = v; note = MD[WALL_OF[v]].name.toLowerCase(); } if (P.code === BP_WALL) { GAME.wallMat = v; }
else if (P.filter) { GAME.filterCfg = blockP[bi] ? filterInfo(bi) : cleanFilterCfg(ghost[2]); GAME.pieceDir[P.id] = v >> 7; note = filterSentence(GAME.filterCfg); } else if (P.filter) { GAME.filterCfg = blockP[bi] ? filterInfo(bi) : cleanFilterCfg(ghost[2]); GAME.pieceDir[P.id] = v >> 7; }
else if (P.dirs) GAME.pieceDir[P.id] = v & 3; else if (P.dirs) GAME.pieceDir[P.id] = v & 3;
if (P.dirs && !P.filter) note = P.dirs[GAME.pieceDir[P.id] || 0];
else if (P.filter) note = `${P.dirs[GAME.pieceDir[P.id] || 0]}, ${note}`;
} }
if (pid) { if (pid) {
const P = PIECE_BY_ID[pid]; const P = PIECE_BY_ID[pid];
if (P.hidden) { setStatus(`${P.name} cannot be built.`); playSfx('error'); return; } if (P.hidden) { setStatus(`${P.name} cannot be built.`); playSfx('error'); return; }
GAME.pieceIdx = P.index; GAME.buildCat = catOf(P.id); GAME.paste = false; selectTool(2); hudDirty = true; playSfx('ui'); GAME.pieceIdx = P.index; GAME.buildCat = catOf(P.id); GAME.paste = false; selectTool(2); hudDirty = true; playSfx('ui');
setStatus(`Picked: ${P.name}${note ? ` (${note})` : ''}.`, 'info'); setStatus(`Picked: ${P.name}.`, 'info');
return; return;
} }
const t = PH[m] === PH_POWDER || PH[m] === PH_LIQUID ? m : 0; const t = PH[m] === PH_POWDER || PH[m] === PH_LIQUID ? m : 0;
@@ -81,7 +79,7 @@ function stampOrigin(bw, bh) { const { bx, by } = aimBlock(); return { bx: bx -
// Copy and cut: hold C or X and drag a box with the left button; letting go does it. // Copy and cut: hold C or X and drag a box with the left button; letting go does it.
function selModeHeld() { return GAME.running && !anyModal() ? (keys.has('x') ? 'cut' : keys.has('c') ? 'copy' : null) : null; } function selModeHeld() { return GAME.running && !anyModal() ? (keys.has('x') ? 'cut' : keys.has('c') ? 'copy' : null) : null; }
function startPaste() { function startPaste() {
if (!GAME.clip) { setStatus('Nothing copied yet. Hold C and drag a box over something you built.', 'info'); playSfx('error'); return; } if (!GAME.clip) { setStatus('Hold C and drag over built pieces to copy.', 'info'); playSfx('error'); return; }
GAME.paste = true; GAME.drag = null; hudDirty = true; playSfx('ui'); GAME.paste = true; GAME.drag = null; hudDirty = true; playSfx('ui');
} }
function stopSelectAndPaste() { GAME.selDrag = null; GAME.paste = false; hudDirty = true; playSfx('ui'); } function stopSelectAndPaste() { GAME.selDrag = null; GAME.paste = false; hudDirty = true; playSfx('ui'); }
@@ -145,7 +143,7 @@ function primaryPress(shift) {
} }
if (!err) err = place(); if (!err) err = place();
}); });
if (err) { setStatus(err + (/already/.test(err) ? `${/[.!]$/.test(err) ? '' : '.'} Hold G to replace it.` : '')); playSfx('error'); } else { playSfx('place'); hudDirty = true; } if (err) { setStatus(err + (/already/.test(err) ? `; hold G to replace it.` : '')); playSfx('error'); } else { playSfx('place'); hudDirty = true; }
} }
} }
} }
@@ -156,11 +154,11 @@ function primaryRelease(shift) {
if (sd.mode === 'cut') { if (sd.mode === 'cut') {
let bp = null; let bp = null;
undoRecord(rec => `cutting ${plural(piecesIn(rec, 'block-') + piecesIn(rec, 'mach-') + piecesIn(rec, 'mold-'), 'piece')}`, () => { bp = cutRegion(sd.x, sd.y, ax, ay); }); undoRecord(rec => `cutting ${plural(piecesIn(rec, 'block-') + piecesIn(rec, 'mach-') + piecesIn(rec, 'mold-'), 'piece')}`, () => { bp = cutRegion(sd.x, sd.y, ax, ay); });
if (bp) { GAME.clip = bp; GAME.paste = true; playSfx('decon'); setStatus(`Cut ${bp.bw}×${bp.bh} blocks into your bag. Click to paste it somewhere; Ctrl+Z puts it back.`, 'info'); } if (bp) { GAME.clip = bp; GAME.paste = true; playSfx('decon'); setStatus(`Cut ${bp.bw}×${bp.bh} blocks; click to paste.`, 'info'); }
else { setStatus('Nothing you built is inside that box.'); playSfx('error'); } else { setStatus('Nothing you built is inside that box.'); playSfx('error'); }
} else { } else {
const bp = copyRegion(sd.x, sd.y, ax, ay); const bp = copyRegion(sd.x, sd.y, ax, ay);
if (bp) { GAME.clip = bp; GAME.paste = true; playSfx('cast'); setStatus(`Copied ${bp.bw}×${bp.bh} blocks. Click to place it.`, 'info'); } if (bp) { GAME.clip = bp; GAME.paste = true; playSfx('cast'); setStatus(`Copied ${bp.bw}×${bp.bh} blocks; click to place.`, 'info'); }
else { setStatus('Nothing you built is inside that box.'); playSfx('error'); } else { setStatus('Nothing you built is inside that box.'); playSfx('error'); }
} }
return; return;
@@ -185,8 +183,8 @@ function primaryRelease(shift) {
if (ghosts) undoNote(g); if (ghosts) undoNote(g);
return res; return res;
}); });
const hint = /already built/.test(r.status) ? `${/[.!]$/.test(r.status) ? '' : '.'} Hold G to replace it.` : ''; const hint = /already built/.test(r.status) ? `; hold G to replace it.` : '';
const gtxt = ghosts ? `${r.n ? `Placed ${plural(r.n, P.name.toLowerCase())}; ` : ''}${plural(ghosts, 'more')} out of reach ${ghosts === 1 ? 'is a ghost' : 'are ghosts'} your bot builds when it gets close.` : ''; const gtxt = ghosts ? `${r.n} placed; ${ghosts} ghosts waiting for the bot.` : '';
if (r.n || ghosts) { playSfx('place'); setStatus(gtxt || (r.status ? r.status + hint : ''), 'info'); } else { setStatus((r.status || 'Nothing placed.') + hint); playSfx('error'); } if (r.n || ghosts) { playSfx('place'); setStatus(gtxt || (r.status ? r.status + hint : ''), 'info'); } else { setStatus((r.status || 'Nothing placed.') + hint); playSfx('error'); }
GAME.drag = null; GAME.dragDir = -1; hudDirty = true; GAME.drag = null; GAME.dragDir = -1; hudDirty = true;
} }
@@ -219,7 +217,7 @@ function toolTick() {
if (r.status) setStatus(r.status); if (r.status) setStatus(r.status);
} }
} else if (t === 'ignite') { } else if (t === 'ignite') {
if (inReach(ax, ay)) { const r = toolIgnite(ax, ay); playSfx('spark'); if (Math.random() < 0.6) spawnSpark(ax + 0.5, ay + 0.5); if (!r.fuel) setStatus('Nothing here burns. Aim at coal or wood that touches air.'); } if (inReach(ax, ay)) { const r = toolIgnite(ax, ay); playSfx('spark'); if (Math.random() < 0.6) spawnSpark(ax + 0.5, ay + 0.5); if (!r.fuel) setStatus('Aim at coal or wood touching air to light it.'); }
else setStatus('Out of reach.'); else setStatus('Out of reach.');
} }
} }
+1 -9
View File
@@ -11,16 +11,7 @@ function drainEvents() {
const m = MILESTONES.find(x => x.id === e.id); const m = MILESTONES.find(x => x.id === e.id);
if (m && !['bit1', 'bit2', 'bit3', 'tank'].includes(m.id)) { showCard({ kind: 'Milestone', title: m.title, body: [m.msg, m.reward], accent: 'milestone' }); playSfx('milestone'); } if (m && !['bit1', 'bit2', 'bit3', 'tank'].includes(m.id)) { showCard({ kind: 'Milestone', title: m.title, body: [m.msg, m.reward], accent: 'milestone' }); playSfx('milestone'); }
} else if (e.t === 'cast_ready') { } else if (e.t === 'cast_ready') {
const metal = MD[e.metal] ? MD[e.metal].name : 'Metal';
const body = `The ${e.kind} casting has set. Press F on the mold to knock it out.`;
showCard({ kind: 'Casting ready', title: `${metal} ${e.kind}`, swatch: e.metal > 0 ? e.metal : 0, body: [body], accent: 'milestone' });
playSfx('cast'); playSfx('cast');
} else if (e.t === 'ingot_drop') {
const metal = MD[e.metal] ? MD[e.metal].name : 'Metal', body = `${e.bars} ${barWord(e.metal, e.bars)} dropped out of the mold.`;
if (e.first) showCard({ kind: 'Casting', title: e.metal === GLASS ? 'Glass bricks' : `${metal} bars`, swatch: e.metal, body: [body], accent: 'milestone' });
else toast(body);
} else if (e.t === 'ingot_blocked') {
toast('Bars are ready in a mold but there is no room under it. Leave one open block under the mold: bars drop out of the bottom.');
} else if (e.t === 'upgrade') { } else if (e.t === 'upgrade') {
playSfx('upgrade'); if (SET.shake) R.shake = 1.4; playSfx('upgrade'); if (SET.shake) R.shake = 1.4;
showCard({ kind: e.kind === 'bit' ? 'Drill bit upgraded' : 'Bag upgraded', title: e.kind === 'bit' ? BIT_TIERS[e.tier].name : TANK_TIERS[e.tier].name, accent: 'milestone', showCard({ kind: e.kind === 'bit' ? 'Drill bit upgraded' : 'Bag upgraded', title: e.kind === 'bit' ? BIT_TIERS[e.tier].name : TANK_TIERS[e.tier].name, accent: 'milestone',
@@ -28,6 +19,7 @@ function drainEvents() {
hudDirty = true; hudDirty = true;
} else if (e.t === 'debris') spawnDebris(e.x, e.y, e.m); } else if (e.t === 'debris') spawnDebris(e.x, e.y, e.m);
else if (e.t === 'suck') spawnSuck(e.x, e.y, e.m); else if (e.t === 'suck') spawnSuck(e.x, e.y, e.m);
else if (e.t === 'warn') spawnFloater(e.x, e.y, e.text, '#ff8a72');
else if (e.t === 'sold') spawnFloater(e.x, e.y, '+' + fmtMoney(e.v)); else if (e.t === 'sold') spawnFloater(e.x, e.y, '+' + fmtMoney(e.v));
else if (e.t === 'order_new') { else if (e.t === 'order_new') {
const o = e.o; const o = e.o;
+5 -2
View File
@@ -88,7 +88,7 @@ function hoverInfo(ax, ay) {
const mm = machines[id], P = PIECE_BY_ID[mm.type]; t1 = P.name; 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 === '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 === '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.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'); 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');
if (mm.type === 'cutter') t2.push(mm.blocked ? 'stopped: clear the space under it' : tick - (mm.busyT || -999) < 30 ? 'cutting' : 'idle: drop stone rubble in', `${(stats.bricks || 0).toLocaleString('en-US')} bricks cut`); if (mm.type === 'cutter') t2.push(mm.blocked ? 'stopped: clear the space under it' : tick - (mm.busyT || -999) < 30 ? 'cutting' : 'idle: drop stone rubble in', `${(stats.bricks || 0).toLocaleString('en-US')} bricks cut`);
if (mm.type === 'refiner') t2.push(mm.blocked ? 'stopped: clear the space under its chute' : tick - (mm.busyT || -999) < 30 ? 'grinding' : 'idle: drop rubble, gravel, sand or dirt in', `${(stats.refined || 0).toLocaleString('en-US')} ground, ${(stats.refinedOre || 0).toLocaleString('en-US')} ore found`); if (mm.type === 'refiner') t2.push(mm.blocked ? 'stopped: clear the space under its chute' : tick - (mm.busyT || -999) < 30 ? 'grinding' : 'idle: drop rubble, gravel, sand or dirt in', `${(stats.refined || 0).toLocaleString('en-US')} ground, ${(stats.refinedOre || 0).toLocaleString('en-US')} ore found`);
@@ -123,7 +123,10 @@ function hoverInfo(ax, ay) {
const mo = moldAt(ax, ay); const mo = moldAt(ax, ay);
if (mo) { if (mo) {
t1 = MOLDS[mo.kind].name; t1 = MOLDS[mo.kind].name;
t2 = [mo.state === 'ready' ? (mo.kind === 'ingot' ? `${mo.ready.length} ${mo.ready.length === 1 ? 'bar' : 'bars'} set · drop when there is room below` : `${MD[mo.metal].name} casting set · press F`) : mo.state === 'flawed' || mo.state === 'short' ? mo.flaw : mo.state === 'filling' ? `filling ${Math.round(mo.fill * 100)}%` : 'empty']; 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])); if (player.lens) t2.push(fmtT(temp[i]));
} }
const reachOk = toolId() === 'drill' ? inDrillReach(ax, ay, GAME.drillW) : inReach(ax, ay); const reachOk = toolId() === 'drill' ? inDrillReach(ax, ay, GAME.drillW) : inReach(ax, ay);
+4 -4
View File
@@ -4,7 +4,7 @@ function setDrillWidth(w) {
const n = Math.max(0, Math.min(PULSE_W.length - 1, w)); const n = Math.max(0, Math.min(PULSE_W.length - 1, w));
if (n === GAME.drillW) return; if (n === GAME.drillW) return;
GAME.drillW = n; drillWidth = n; hudDirty = true; playSfx('ui'); GAME.drillW = n; drillWidth = n; hudDirty = true; playSfx('ui');
const what = ['a beam as wide as the bot: bores a tunnel fast and reaches furthest', 'a cone: the all-rounder', 'a broad fan: clears the most rock, but only close to the bot'][n]; const what = ['long beam', 'balanced cone', 'broad fan'][n];
setStatus(`${PULSE_W[n].name[0].toUpperCase() + PULSE_W[n].name.slice(1)} pulse, ${what} (reach ${drillReach(n)}).`, 'info'); setStatus(`${PULSE_W[n].name[0].toUpperCase() + PULSE_W[n].name.slice(1)} pulse, ${what} (reach ${drillReach(n)}).`, 'info');
} }
// R with the drill: tune it to the material under the cursor, or back to everything // R with the drill: tune it to the material under the cursor, or back to everything
@@ -15,7 +15,7 @@ function tuneUnderCursor() {
const to = PH[m] === PH_SOLID ? YLD[m] : PH[m] === PH_POWDER ? m : 0; const to = PH[m] === PH_SOLID ? YLD[m] : PH[m] === PH_POWDER ? m : 0;
GAME.drillTune = !to || to === GAME.drillTune ? 0 : to; GAME.drillTune = !to || to === GAME.drillTune ? 0 : to;
hudDirty = true; playSfx('ui'); hudDirty = true; playSfx('ui');
setStatus(GAME.drillTune ? `Tuned to ${MD[GAME.drillTune].name.toLowerCase()}: only that shatters, and the pulses pass through everything else.` : 'Untuned: the drill shatters any rock it can.', 'info'); setStatus(GAME.drillTune ? `Tuned to ${MD[GAME.drillTune].name.toLowerCase()}: only it shatters.` : 'Untuned: the drill shatters any rock it can.', 'info');
} }
function cycleItem(d) { function cycleItem(d) {
const t = toolId(); const t = toolId();
@@ -74,13 +74,13 @@ function toggleFilterMat(m) {
if (k >= 0) f.m.splice(k, 1); else f.m.push(m); if (k >= 0) f.m.splice(k, 1); else f.m.push(m);
f.m.sort((a, b) => a - b); f.m.sort((a, b) => a - b);
hudDirty = true; lastBagKey = ''; playSfx('ui'); hudDirty = true; lastBagKey = ''; playSfx('ui');
setStatus(`New filters ${filterSentence(f)}.`, 'info'); setStatus(`New filter selection updated.`, 'info');
} }
function toggleCollectUnderCursor() { function toggleCollectUnderCursor() {
const m = grainAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)); const m = grainAt(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
if (!m) { setStatus('Aim at something loose to tick it.', 'info'); return; } if (!m) { setStatus('Aim at something loose to tick it.', 'info'); return; }
toggleCollect(m); toggleCollect(m);
setStatus(`${MD[m].name} ${GAME.collect.has(m) ? 'ticked. While anything is ticked, the vacuum takes only ticked materials' : GAME.collect.size ? 'unticked' : 'unticked. Nothing is ticked, so the vacuum takes what you start on'}.`, 'info'); setStatus(`${MD[m].name} ${GAME.collect.has(m) ? 'ticked for vacuuming' : 'unticked'}.`, 'info');
} }
function setBrush(d) { function setBrush(d) {
const t = toolId(); const t = toolId();
+19 -2
View File
@@ -25,6 +25,7 @@ with sync_playwright() as p:
pg.wait_for_function('GAME.running === true', timeout=20000) pg.wait_for_function('GAME.running === true', timeout=20000)
pg.wait_for_timeout(500) pg.wait_for_timeout(500)
ev = pg.evaluate ev = pg.evaluate
ev("window._maxStatus = ''; const _ss = setStatus; setStatus = function (s, k) { if (s && s.length > _maxStatus.length) _maxStatus = s; return _ss(s, k); }; 0")
look = sys.argv[2] if len(sys.argv) > 2 else 'pixel' look = sys.argv[2] if len(sys.argv) > 2 else 'pixel'
check('a new game starts fully zoomed in', ev('SET.zoom') == 12, ev('SET.zoom')) check('a new game starts fully zoomed in', ev('SET.zoom') == 12, ev('SET.zoom'))
# Ctrl+W closes the tab; with a world open the browser must ask first # Ctrl+W closes the tab; with a world open the browser must ask first
@@ -286,6 +287,8 @@ with sync_playwright() as p:
check('mold placed by click', ev('molds.length') == 1, ev('GAME.status')) check('mold placed by click', ev('molds.length') == 1, ev('GAME.status'))
ev('for (const cav of molds[0].cavs) for (const i of cav) setCell(i, MCU, 1160, 0)') ev('for (const cav of molds[0].cavs) for (const i of cav) setCell(i, MCU, 1160, 0)')
pg.wait_for_function("ents.filter(e => e.m === BARCU).length === 3", timeout=30000) pg.wait_for_function("ents.filter(e => e.m === BARCU).length === 3", timeout=30000)
check('mold label shows a percentage', ev(r'/^\d+%/.test(moldLabel(molds[0]))'))
check('bar drops make no toast', ev("![...$('toasts').children].some(e => /dropped/i.test(e.textContent))"))
check('bars set and drop on their own', ev("ents.length === 3 && molds[0].state === 'short' && molds[0].ready.length === 0"), ev('[molds[0].state, ents.length]')) check('bars set and drop on their own', ev("ents.length === 3 && molds[0].state === 'short' && molds[0].ready.length === 0"), ev('[molds[0].state, ents.length]'))
pg.screenshot(path=str(out / '05-mold-ready.png')) pg.screenshot(path=str(out / '05-mold-ready.png'))
check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])')) check('three bars drop below the mold without F', ev('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'), ev('ents.map(e => [e.ix, e.iy])'))
@@ -297,6 +300,19 @@ with sync_playwright() as p:
hold(*cell_to_screen(*e0), 900) hold(*cell_to_screen(*e0), 900)
check('vacuum picks up bars', ev('bag[BARCU]') >= 1, ev('bag[BARCU]')) check('vacuum picks up bars', ev('bag[BARCU]') >= 1, ev('bag[BARCU]'))
check('casting events stay quiet and damage labels are red', ev("""(() => {
const oldToast = toast, oldCard = showCard, oldSfx = playSfx, events = EV.splice(0), floaters = R.floaters;
let popups = 0, casts = 0;
toast = showCard = () => popups++; playSfx = s => { if (s === 'cast') casts++; };
R.floaters = [];
try {
emit({ t: 'ingot_drop', bars: 1, metal: COPPER, first: true });
emit({ t: 'ingot_blocked' }); emit({ t: 'cast_ready', kind: 'bit', metal: COPPER });
emit({ t: 'warn', x: 12, y: 34, text: 'melted' }); drainEvents();
return popups === 0 && casts === 1 && R.floaters.length === 1 && R.floaters[0].colour === '#ff8a72' && R.floaters[0].text === 'melted';
} finally { toast = oldToast; showCard = oldCard; playSfx = oldSfx; R.floaters = floaters; EV.push(...events); }
})()"""))
# 7. belt line by dragging, copied with a blueprint and pasted # 7. belt line by dragging, copied with a blueprint and pasted
refill('bag[BARCU] = 60'); ev('money = 1000') refill('bag[BARCU] = 60'); ev('money = 1000')
ok = select('3', "piece().id === 'belt'") ok = select('3', "piece().id === 'belt'")
@@ -344,7 +360,7 @@ with sync_playwright() as p:
ok = select('3', "piece().id === 'fan'") ok = select('3', "piece().id === 'fan'")
pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(400) pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(400)
pg.mouse.down(); pg.mouse.up(); settle(100) pg.mouse.down(); pg.mouse.up(); settle(100)
check('a fan on a belt says to hold G', 'Hold G' in ev('GAME.status'), ev('GAME.status')) check('a fan on a belt says to hold G', 'hold G' in ev('GAME.status'), ev('GAME.status'))
pg.keyboard.down('g'); settle(100); pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(200) pg.keyboard.down('g'); settle(100); pg.mouse.move(*blk(bbx - 4, bby + 1)); settle(200)
check('holding G shows replace mode', 'Replace · G' in ev("$('selcard').textContent")) check('holding G shows replace mode', 'Replace · G' in ev("$('selcard').textContent"))
pg.screenshot(path=str(out / '07g-replace-fan.png')) pg.screenshot(path=str(out / '07g-replace-fan.png'))
@@ -384,7 +400,7 @@ with sync_playwright() as p:
# two belts at the end of the line: plain dragging leaves them, G-dragging swaps them for filters # two belts at the end of the line: plain dragging leaves them, G-dragging swaps them for filters
ev(f"placeBlock(BP_BELT, 1, {bbx-2}, {fy}, true); placeBlock(BP_BELT, 1, {bbx-1}, {fy}, true);") ev(f"placeBlock(BP_BELT, 1, {bbx-2}, {fy}, true); placeBlock(BP_BELT, 1, {bbx-1}, {fy}, true);")
drag(lambda: blk(bbx - 2, fy), lambda: blk(bbx - 1, fy)) drag(lambda: blk(bbx - 2, fy), lambda: blk(bbx - 1, fy))
check('dragging over built belts leaves them and says how to replace', ev(f'blockP[{fy}*BW+{bbx-2}] === BP_BELT') and 'Hold G' in ev('GAME.status'), ev('GAME.status')) check('dragging over built belts leaves them and says how to replace', ev(f'blockP[{fy}*BW+{bbx-2}] === BP_BELT') and 'hold G' in ev('GAME.status'), ev('GAME.status'))
pg.keyboard.down('g'); settle(50) pg.keyboard.down('g'); settle(50)
pg.mouse.move(*blk(bbx - 2, fy)); settle(300) pg.mouse.move(*blk(bbx - 2, fy)); settle(300)
pg.screenshot(path=str(out / '07c-replace-preview.png')) pg.screenshot(path=str(out / '07c-replace-preview.png'))
@@ -527,6 +543,7 @@ with sync_playwright() as p:
pg2.screenshot(path=str(out / '11-deep-magma.png')) pg2.screenshot(path=str(out / '11-deep-magma.png'))
pg2.close() pg2.close()
print('\n'.join(logs) or 'no console errors') print('\n'.join(logs) or 'no console errors')
check('every status line is short (≤ 90 characters)', len(ev('_maxStatus')) <= 90, ev('_maxStatus'))
print('FAILS:', fails) print('FAILS:', fails)
b.close() b.close()
sys.exit(1 if fails else 0) sys.exit(1 if fails else 0)
+4 -2
View File
@@ -68,8 +68,10 @@ check('undo clears the ghosts and takes back what was built from them', belts()
fresh(); fresh();
run("placeMachineAt('fan', 0, 22, 20); EV.length = 0;"); run("placeMachineAt('fan', 0, 22, 20); EV.length = 0;");
run('for (let y = 80; y < 84; y++) for (let x = 88; x < 92; x++) temp[y * W + x] = 900; for (let k = 0; k < 40; k++) { for (let y = 80; y < 84; y++) for (let x = 88; x < 92; x++) temp[y * W + x] = 900; simTick(); }'); run('for (let y = 80; y < 84; y++) for (let x = 88; x < 92; x++) temp[y * W + x] = 900; for (let k = 0; k < 40; k++) { for (let y = 80; y < 84; y++) for (let x = 88; x < 92; x++) temp[y * W + x] = 900; simTick(); }');
const warn = run("EV.filter(e => e.t === 'toast' && /fan/i.test(e.text)).map(e => e.text).join(' | ')"); check('a hot fan stores its temperature without a toast', run("machines.some(m => m && m.type === 'fan' && m.hot > 760) && !EV.some(e => e.t === 'toast')"));
check('a fan at 900°C warns that it is about to melt', /1085/.test(warn) && fans() === 1, warn);
run("EV.length = 0; removeMachine(machines.find(m => m && m.type === 'fan').id, false, true);");
check('heat destruction emits a local warning', run("EV.some(e => e.t === 'warn' && e.text === 'Fan destroyed' && Number.isFinite(e.x) && Number.isFinite(e.y)) && !EV.some(e => e.t === 'toast')"));
// old saves: tiny specks of too-hard rock become the rock around them, once // old saves: tiny specks of too-hard rock become the rock around them, once
fresh(); fresh();
+13 -2
View File
@@ -5,6 +5,7 @@ const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const { run } = load(); const { run } = load();
const { check, done } = suite(); const { check, done } = suite();
function fresh() { flatWorld(run, 240, 160, 100); run('bot.x = 110; bot.y = 70; bag.fill(0); bagTotal = 0; for (const e of ents.slice()) removeEnt(e);'); } function fresh() { flatWorld(run, 240, 160, 100); run('bot.x = 110; bot.y = 70; bag.fill(0); bagTotal = 0; for (const e of ents.slice()) removeEnt(e);'); }
run("var moldMessages = []; const collectOriginal = collectMold; collectMold = function(mo) { const s = collectOriginal(mo); moldMessages.push(s); return s; }; 0");
const cavCount = (k, m) => run(`molds[${k}].cavs[0].filter(i => mat[i] === ${m}).length`); const cavCount = (k, m) => run(`molds[${k}].cavs[0].filter(i => mat[i] === ${m}).length`);
// automatic drops leave the short stub on the trough floor for the next firing // automatic drops leave the short stub on the trough floor for the next firing
@@ -20,11 +21,13 @@ check('five more grains top up the stub into another bar', run('ents.length ===
// blocked mouths retain their metal and warn once, including molds from old saves // blocked mouths retain their metal and warn once, including molds from old saves
fresh(); fresh();
run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; EV.length = 0; { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, STONE, 30, 0); } checkMolds(); checkMolds();'); run('bag[RSTONE] = 100; bagTotal = 100; placeMoldAt("ingot", 30, 22, RSTONE); bag[RSTONE] = 0; bagTotal = 0; EV.length = 0; { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, STONE, 30, 0); } checkMolds(); checkMolds();');
check('blocked bars have a warning flag', run('molds[0].blocked === true'));
check('blocked bars stay put and warn only once', run('ents.length === 0 && molds[0].state === "ready" && EV.filter(e => e.t === "ingot_blocked").length === 1') && cavCount(0, run('COPPER')) === 28); check('blocked bars stay put and warn only once', run('ents.length === 0 && molds[0].state === "ready" && EV.filter(e => e.t === "ingot_blocked").length === 1') && cavCount(0, run('COPPER')) === 28);
msg = run('collectMold(molds[0])'); msg = run('collectMold(molds[0])');
check('F on a blocked ingot mold changes nothing', run('ents.length === 0') && cavCount(0, run('COPPER')) === 28 && /empty themselves/.test(msg), msg); check('F on a blocked ingot mold changes nothing', run('ents.length === 0') && cavCount(0, run('COPPER')) === 28 && /empty themselves/.test(msg), msg);
run('{ const mo = molds[0]; for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, EMPTY, 30, 0); }'); run('{ const mo = molds[0]; for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, EMPTY, 30, 0); }');
run('checkMolds()'); run('checkMolds()');
check('dropping bars clears the warning flag', run('molds[0].blocked === false'));
check('the next check drops bars only below it', run('ents.length === 3 && ents.every(e => e.iy === molds[0].y + molds[0].h) && !molds[0].blockedSaid')); check('the next check drops bars only below it', run('ents.length === 3 && ents.every(e => e.iy === molds[0].y + molds[0].h) && !molds[0].blockedSaid'));
run('for (const e of ents.slice()) removeEnt(e); { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); for (let x = mo.x; x < mo.x + mo.w; x++) setCell((mo.y + mo.h) * W + x, STONE, 30, 0); } checkMolds();'); run('for (const e of ents.slice()) removeEnt(e); { const mo = molds[0]; for (const i of mo.cavs[0]) setCell(i, COPPER, 30, F_CAST); for (let x = mo.x; x < mo.x + mo.w; x++) setCell((mo.y + mo.h) * W + x, STONE, 30, 0); } checkMolds();');
check('a new blocked spell warns again', run('EV.filter(e => e.t === "ingot_blocked").length === 2')); check('a new blocked spell warns again', run('EV.filter(e => e.t === "ingot_blocked").length === 2'));
@@ -145,9 +148,9 @@ run('molds[0].cavs[0].forEach((i, k) => setCell(i, k & 1 ? COPPER : IRON, 30, F_
check('mixed metal all drops as scrap without bars', under('SCRCU') === 14 && under('SCRFE') === 14 && run('ents.length === 0 && molds[0].state === "empty" && bagTotal === 0')); check('mixed metal all drops as scrap without bars', under('SCRCU') === 14 && under('SCRFE') === 14 && run('ents.length === 0 && molds[0].state === "empty" && bagTotal === 0'));
ingot(); ingot();
run('setCell(molds[0].cavs[0][0], SAND, 30, 0); for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, STONE, 30, 0); checkMolds();'); run('setCell(molds[0].cavs[0][0], SAND, 30, 0); for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, STONE, 30, 0); checkMolds();');
check('blocked junk stays in the trough', cavCount(0, run('SAND')) === 1 && under('SAND') === 0); check('blocked junk stays in the trough', cavCount(0, run('SAND')) === 1 && under('SAND') === 0 && run('molds[0].blocked === true'));
run('for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, EMPTY, 30, 0); checkMolds();'); run('for (let y = 92; y < 96; y++) for (let x = 120; x < 136; x++) setCell(y * W + x, EMPTY, 30, 0); checkMolds();');
check('junk drops once there is room', cavCount(0, run('SAND')) === 0 && under('SAND') === 1); check('junk drops once there is room', cavCount(0, run('SAND')) === 0 && under('SAND') === 1 && run('molds[0].blocked === false'));
ingot(); ingot();
run('setCell(molds[0].cavs[0][0], SAND, 30, 0); setCell(molds[0].cavs[0][1], MCU, 1200, 0); checkMolds();'); run('setCell(molds[0].cavs[0][0], SAND, 30, 0); setCell(molds[0].cavs[0][1], MCU, 1200, 0); checkMolds();');
check('cleaning waits for molten metal in the cavity', cavCount(0, run('SAND')) === 1 && under('SAND') === 0); check('cleaning waits for molten metal in the cavity', cavCount(0, run('SAND')) === 1 && under('SAND') === 0);
@@ -184,4 +187,12 @@ check('metal wall refund does not count cavity metal twice', run('bag[BARCU] ===
fresh(); fresh();
run('addMold("bit", 30, 20, RSTONE, true); molds[0].cavs[0].slice(0, 20).forEach((i, k) => setCell(i, k & 1 ? IRON : STEEL, 30, F_CAST)); removeMold(molds[0]);'); run('addMold("bit", 30, 20, RSTONE, true); molds[0].cavs[0].slice(0, 20).forEach((i, k) => setCell(i, k & 1 ? IRON : STEEL, 30, F_CAST)); removeMold(molds[0]);');
check('compatible iron and steel return as iron bars and scrap', run('bag[BARFE] === 2 && bag[SCRFE] === 4 && bag[SCRST] === 0')); check('compatible iron and steel return as iron bars and scrap', run('bag[BARFE] === 2 && bag[SCRFE] === 4 && bag[SCRST] === 0'));
for (const kind of ['bit', 'tank']) for (const metal of ['COPPER', 'IRON', 'GLASS']) for (const upgraded of [false, true]) {
fresh();
run(`addMold("${kind}", 30, 20, RSTONE, true); player.bitTier = player.tankTier = ${upgraded ? 3 : 0}; molds[0].cavs[0].forEach(i => setCell(i, ${metal}, 30, F_CAST)); checkMolds(); collectMold(molds[0]);`);
}
for (const state of ['empty', 'filling']) {
fresh(); run(`addMold("bit", 30, 20, RSTONE, true); molds[0].state = "${state}"; collectMold(molds[0]);`);
}
check('every collected mold message is short', run('moldMessages.every(s => s.length <= 90)'), run('moldMessages.join(" | ")'));
done(); done();
+1 -1
View File
@@ -102,7 +102,7 @@ run('player.tankTier = 0; bag[RSTONE] = 16; bag[LDIRT] = 1500 - 16; bagTotal = 1
run('placeBlocks(BP_WALL, RSTONE, [[30, 18]])'); run('placeBlocks(BP_WALL, RSTONE, [[30, 18]])');
run('bag[LDIRT] = 1500; bagTotal = 1500;'); run('bag[LDIRT] = 1500; bagTotal = 1500;');
const msg = run('removePieceAt(121, 73)'); const msg = run('removePieceAt(121, 73)');
check('removing a block with a full bag drops its rubble in place', count(run, run('RSTONE'), 120, 72, 123, 75) === 16 && /bag is full/.test(msg), msg); check('removing a block with a full bag drops its rubble in place', count(run, run('RSTONE'), 120, 72, 123, 75) === 16 && /bag full/.test(msg), msg);
run('bag[BARCU] = 20; bag[LDIRT] = 1480; bagTotal = 1500; placeBlocks(BP_BELT, 1, [[32, 18]]); bag[LDIRT] += 1; bagTotal = 1500;'); run('bag[BARCU] = 20; bag[LDIRT] = 1480; bagTotal = 1500; placeBlocks(BP_BELT, 1, [[32, 18]]); bag[LDIRT] += 1; bagTotal = 1500;');
const m0 = run('money'); run('removePieceAt(129, 73)'); ticks(run, 60); const m0 = run('money'); run('removePieceAt(129, 73)'); ticks(run, 60);
check('a belt removed with a full bag still refunds its price as money', run('money') === m0 + 12, `${m0} -> ${run('money')}`); check('a belt removed with a full bag still refunds its price as money', run('money') === m0 + 12, `${m0} -> ${run('money')}`);
+16
View File
@@ -0,0 +1,16 @@
// Run after clear-sky.js: filling, blocked, set, and hot machinery in one view.
const my = BY - 4;
const filling = addMold('ingot', BX + 3, my, RSTONE, true);
filling.cavs[0].slice(0, 11).forEach(i => setCell(i, MCU, 1160, 0));
const blocked = addMold('ingot', BX + 12, my, RSTONE, true);
blocked.cavs[0].forEach(i => setCell(i, COPPER, 30, F_CAST));
for (let y = blocked.y + blocked.h; y < blocked.y + blocked.h + 2; y++) for (let x = blocked.x; x < blocked.x + blocked.w; x++) setCell(y * W + x, STONE, 30, 0);
const bit = addMold('bit', BX + 23, my - 1, RSTONE, true);
bit.cavs[0].forEach(i => setCell(i, COPPER, 30, F_CAST));
checkMolds();
const fan = addMachine('fan', BX + 30, my, 0);
for (let y = fan.y; y < fan.y + fan.h; y++) for (let x = fan.x; x < fan.x + fan.w; x++) temp[y * W + x] = 900;
tick = (30 - fan.id % 30) % 30; machinesTick();
if (filling.fill < 0.35 || filling.fill > 0.45 || !blocked.blocked || bit.state !== 'ready' || !(fan.hot > 760)) throw Error('Message scene did not reach its intended states');
if (!hoverInfo(blocked.x + 1, blocked.y + 1).t2.includes('blocked: leave one open block under it') || !hoverInfo(bit.x + 1, bit.y + 1).t2.includes('100% set · press F') || !hoverInfo(fan.x + 1, fan.y + 1).t2.includes('900°C · melts at 1085°C')) throw Error('Missing hover details');
CAM = [filling.x + 60, filling.y + 4];