Silo contents at a glance (t38); drill aimed past its reach stays calm (t41)

Silos show a wide fill window in the material's colour in both looks,
and the cursor label names the contents anywhere over the outline.
Aiming the drill beyond its reach keeps normal colours, marks where the
pulse stops, and drops the red circle and the out-of-reach toast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 18:40:05 -04:00
co-authored by Claude Opus 5.5
parent 9bc5a6c3af
commit b74a9173eb
12 changed files with 197 additions and 44 deletions
+7
View File
@@ -98,6 +98,13 @@ Details and numbers live in the code and in `docs/systems/`.
## Decisions ## Decisions
- **Distant drill aim is normal (t41, 2026-10-26).** The owner normally points beyond the
drill's reach and expects players to do the same. Keep the pulse colour and light crosshair,
mark the pulse's far edge, and omit distance warnings for the drill. Other tools still warn.
- **Silo contents are visible without opening the gate (t38, 2026-10-26).** The owner wants
contents beside the cursor anywhere over the outline and a material-coloured fill window at
every zoom in both looks, while keeping the real steel silo shape.
- **Square corners hold grains (t39, 2026-10-26).** The owner wants square wall staircases to - **Square corners hold grains (t39, 2026-10-26).** The owner wants square wall staircases to
hold material instead of leaking through each touching corner. Powder, liquid and gas cannot hold material instead of leaking through each touching corner. Powder, liquid and gas cannot
step diagonally between two solid neighbours; powder neighbours still allow free piles to step diagonally between two solid neighbours; powder neighbours still allow free piles to
+7 -6
View File
@@ -287,7 +287,7 @@ Bars and other loose objects (ents), and the bag: adding, taking, costs and refu
Tool helpers: reach, brushes, and plain digging. Tool helpers: reach, brushes, and plain digging.
- functions: inReach, forBrush, drillReach, inDrillReach, toolDig - functions: inReach, forBrush, drillReach, inDrillReach, toolDig
### src/sim/drill.js (161 lines) ### src/sim/drill.js (160 lines)
The resonance drill: tuned pulses that shatter one material. The resonance drill: tuned pulses that shatter one material.
- functions: drillOrigin, pulseRay, builtCell, toolPulse, crumbleAround, crumbly, shatterCell, pulseTick - functions: drillOrigin, pulseRay, builtCell, toolPulse, crumbleAround, crumbly, shatterCell, pulseTick
- variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF - variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF
@@ -366,9 +366,9 @@ Scale colour without changing the smooth-look class.
- functions: fogColor, diagonalTubeJoins, diagonalTubeShape, diagonalTubeBlock, drawWorld - functions: fogColor, diagonalTubeJoins, diagonalTubeShape, diagonalTubeBlock, drawWorld
- variables: TUBE_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap - variables: TUBE_CLASS, diagonalJoins, diagonalAcross, diagonalAlong, diagonalCap
### src/render/machines.js (242 lines) ### src/render/machines.js (256 lines)
Machine overlays: fans, engines, silo and the rest, drawn on top of the world. Machine overlays: fans, engines, silo and the rest, drawn on top of the world.
- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawRackBlocks - functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks
### src/render/overlays.js (229 lines) ### src/render/overlays.js (229 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.
@@ -457,7 +457,7 @@ Selection panels: current tool card and bag materials.
- functions: renderSelCard, renderBag - functions: renderSelCard, renderBag
- variables: lastBagKey - variables: lastBagKey
### src/ui/info.js (189 lines) ### src/ui/info.js (195 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, pipeContentsText - functions: showCard, howText, discoveryCard, reactionsOf, hoverInfo, renderInfo, drawCursorLabel, pipeContentsText
@@ -476,11 +476,11 @@ Gamepad input.
- functions: padPressed, pollPad - functions: padPressed, pollPad
- variables: PAD - variables: PAD
### src/ui/previews.js (237 lines) ### src/ui/previews.js (234 lines)
Cursor previews: shared grid helpers, air streaks and tool preview dispatch. Cursor previews: shared grid helpers, air streaks and tool preview dispatch.
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock - functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews, previewBlock
### src/ui/preview-drill.js (104 lines) ### src/ui/preview-drill.js (107 lines)
Drill previews: shaken rock cracks, pulse wavefronts and aiming fan. Rock the drill has shaken but not yet broken shows cracks, more and finer as it nears breaking. Each crack is a contour line of a noise field (marching squares, cell by cell), so the lines run on unbroken from one cell to the next and look like real fractures, not a pattern. Drill previews: shaken rock cracks, pulse wavefronts and aiming fan. Rock the drill has shaken but not yet broken shows cracks, more and finer as it nears breaking. Each crack is a contour line of a noise field (marching squares, cell by cell), so the lines run on unbroken from one cell to the next and look like real fractures, not a pattern.
- functions: crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim - functions: crackSeg, drawCracks, pulseRGB, fanPoints, drawDrillPulses, drawDrillAim
- variables: CRACK_N, CRACK_LV - variables: CRACK_N, CRACK_LV
@@ -527,6 +527,7 @@ The frame loop and boot.
- `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way. - `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way.
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore. - `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation. - `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
- `test/t_silo_contents.js`: Stored snow keeps its identity and remains in the world's water budget.
- `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly. - `test/t_sim_optimizations.js`: Optimized paths must match the full roof scan and the original heat loop exactly.
- `test/t_slope_belt_art.js`: Drawing geometry against real parts, without changing transport. - `test/t_slope_belt_art.js`: Drawing geometry against real parts, without changing transport.
- `test/t_slope_belts.js`: Sloped belts: transport, joins, sorting, persistence and the actual drag controls. - `test/t_slope_belts.js`: Sloped belts: transport, joins, sorting, persistence and the actual drag controls.
+30
View File
@@ -4,6 +4,36 @@ 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-26: t38: show silo contents at the cursor and in a wide window (Codex)
- `src/ui/info.js`: contents were confined to the bottom info plate (hidden below 720 px),
omitted from cursor labels, and cone gaps lacked mach ownership. Hover now resolves the
full 12x24 outline and shows contents beside the cursor; physical footprints stay unchanged.
- `src/render/machines.js`: shared framed three-grain window in both looks, fixed pixel grain
noise, visible near-empty row, lifted dark fuel colours, and stencils fitted beside the glass.
`tools/scenes/silo.js` adds snow and one grain of coal. All 31 accepted powders checked.
- `test/t_silo_contents.js`: 5 checks confirm snow intake, identity, water accounting, warm
storage and release. `test/play.py` checks hover/label at five outline points empty/filled,
narrow HUD, fill pixels at zooms 2-12 and stable drawing for all powders in both looks.
- Opened and inspected `shots/t38-silos-{pixel,smooth}-z{2,4,8}.png`. Updated t38 and DESIGN;
regenerated PROJECT_MAP. Final `python3 test/run_all.py --browser`: ALL PASSED (26 headless
suites, 352 play checks, 15 smooth checks). Task moved to Review; owner playtest remains.
## 2026-10-26: t41: distant resonance-drill aim is normal (Codex)
- `src/ui/preview-drill.js`, `previews.js`: keep tuned fan and light crosshair, add a solid far
edge in pulse colour, remove the drill-only red reach circle. `info.js` omits drill distance
warnings; other tools retain their warning, including beside silo contents.
- `src/sim/drill.js`: removed only the distance status; too-hard/cannot-be-drilled wording and
`toolDig` are unchanged. Notebook drill text remains accurate; gamepad code untouched.
- `test/t_drill.js`: new distance regression failed before fix, now passes; two former warning
expectations updated intentionally. `test/play.py`: held mouse aim makes no toast/warning,
hard granite still toasts, no red reach circle, gamepad aim stays within reach.
- Opened and inspected `shots/t41-drill-beyond-reach.png`. Focused browser checks pass.
Initial full browser run exposed test timing/paused-input fixture mistakes, corrected without
changing gameplay. Final `python3 test/run_all.py --browser`: ALL PASSED (26 headless
suites, 352 play checks, 15 smooth checks). Updated t41 and DESIGN; moved to Review. No commit.
## 2026-10-26: t39: seal touching square-block corners (Codex) ## 2026-10-26: t39: seal touching square-block corners (Codex)
- `cells.js`: six inline checks reject diagonal moves between two PH_SOLID neighbours, with - `cells.js`: six inline checks reject diagonal moves between two PH_SOLID neighbours, with
+29 -15
View File
@@ -119,7 +119,7 @@ function drawProcBlocks() {
// glass that shows how full it is, a roof with the loading hatch. All in grain units from its corner. // glass that shows how full it is, a roof with the loading hatch. All in grain units from its corner.
function drawSilo(ctx, mm, sx, sy, zp) { function drawSilo(ctx, mm, sx, sy, zp) {
if (!R.smoothOn) { drawSiloPixel(ctx, mm, sx, sy, zp); return; } if (!R.smoothOn) { drawSiloPixel(ctx, mm, sx, sy, zp); return; }
const X = u => sx + u * zp, Y = u => sy + u * zp, full = Math.min(1, (mm.sn || 0) / SILO_CAP); const X = u => sx + u * zp, Y = u => sy + u * zp;
const steel = (x0, x1) => { const g = ctx.createLinearGradient(X(x0), 0, X(x1), 0); g.addColorStop(0, '#3b4249'); g.addColorStop(0.22, '#a9b3bb'); g.addColorStop(0.5, '#87919a'); g.addColorStop(1, '#353c43'); return g; }; const steel = (x0, x1) => { const g = ctx.createLinearGradient(X(x0), 0, X(x1), 0); g.addColorStop(0, '#3b4249'); g.addColorStop(0.22, '#a9b3bb'); g.addColorStop(0.5, '#87919a'); g.addColorStop(1, '#353c43'); return g; };
ctx.save(); ctx.save();
// legs, with short struts to the cone // legs, with short struts to the cone
@@ -146,22 +146,16 @@ function drawSilo(ctx, mm, sx, sy, zp) {
ctx.beginPath(); ctx.moveTo(X(1.3), Y(2)); ctx.lineTo(X(1.3), Y(19.8)); ctx.moveTo(X(2.3), Y(2)); ctx.lineTo(X(2.3), Y(19.8)); ctx.beginPath(); ctx.moveTo(X(1.3), Y(2)); ctx.lineTo(X(1.3), Y(19.8)); ctx.moveTo(X(2.3), Y(2)); ctx.lineTo(X(2.3), Y(19.8));
for (let yy = 2.6; yy < 19.8; yy += 1.1) { ctx.moveTo(X(1.3), Y(yy)); ctx.lineTo(X(2.3), Y(yy)); } for (let yy = 2.6; yy < 19.8; yy += 1.1) { ctx.moveTo(X(1.3), Y(yy)); ctx.lineTo(X(2.3), Y(yy)); }
ctx.stroke(); ctx.stroke();
// the sight glass: fills from the bottom in the colour of what is inside drawSiloWindow(ctx, mm, sx, sy, zp, false);
const gx = 9.1, gy0 = 3, gy1 = 18.8, gh = gy1 - gy0;
ctx.fillStyle = '#14181c'; ctx.fillRect(X(gx), Y(gy0), zp * 1.1, zp * gh);
if (mm.sn > 0) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(gx + 0.15), Y(gy1 - gh * full), zp * 0.8, zp * gh * full); }
ctx.strokeStyle = '#c9d1d8'; ctx.lineWidth = Math.max(1, zp * 0.1);
ctx.strokeRect(X(gx), Y(gy0), zp * 1.1, zp * gh);
for (let k = 1; k < 4; k++) { const yy = Y(gy1 - gh * k / 4); ctx.beginPath(); ctx.moveTo(X(gx - 0.35), yy); ctx.lineTo(X(gx), yy); ctx.stroke(); }
// what it holds, stencilled on the drum // what it holds, stencilled on the drum
if (zp >= 3) { if (zp >= 3) {
renderMonoFont(); renderMonoFont();
const fs = Math.max(8, Math.round(zp * 1.15)); const fs = Math.max(8, Math.round(zp * 1.15));
ctx.font = `700 ${fs}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.font = `700 ${fs}px ${R.monoFont}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(20,24,28,0.75)'; ctx.fillStyle = 'rgba(20,24,28,0.75)';
ctx.fillText('SILO', X(5.6), Y(5)); ctx.fillText('SILO', X(4.5), Y(5), 5 * zp);
ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`; ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`;
ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', X(5.6), Y(16.8)); ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', X(4.5), Y(16.8), 5 * zp);
if (mm.sn) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(4.4), Y(17.9), zp * 2.4, zp * 0.55); } if (mm.sn) { ctx.fillStyle = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; ctx.fillRect(X(4.4), Y(17.9), zp * 2.4, zp * 0.55); }
} }
// roof with a rim, and the loading hatch (its lid stands open) // roof with a rim, and the loading hatch (its lid stands open)
@@ -173,7 +167,7 @@ function drawSilo(ctx, mm, sx, sy, zp) {
ctx.restore(); ctx.restore();
} }
function drawSiloPixel(ctx, mm, sx, sy, zp) { function drawSiloPixel(ctx, mm, sx, sy, zp) {
const full = Math.min(1, (mm.sn || 0) / SILO_CAP), col = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`; const col = `rgb(${COLR[mm.sm]},${COLG[mm.sm]},${COLB[mm.sm]})`;
const grain = (x, y, c) => { ctx.fillStyle = c; ctx.fillRect(sx + x * zp, sy + y * zp, zp, zp); }; const grain = (x, y, c) => { ctx.fillStyle = c; ctx.fillRect(sx + x * zp, sy + y * zp, zp, zp); };
// Steel shaded by column (light on the left, dark at the right edge), darker rib rows, and fixed // Steel shaded by column (light on the left, dark at the right edge), darker rib rows, and fixed
// grain noise so it never shimmers. The drum is rows 2-19, the cone 20-22 narrowing to the gate. // grain noise so it never shimmers. The drum is rows 2-19, the cone 20-22 narrowing to the gate.
@@ -191,8 +185,7 @@ function drawSiloPixel(ctx, mm, sx, sy, zp) {
grain(1, 21, '#2f353b'); grain(10, 21, '#2f353b'); grain(1, 21, '#2f353b'); grain(10, 21, '#2f353b');
for (let x = 4; x < 8; x++) grain(x, 23, '#1c1f23'); for (let x = 4; x < 8; x++) grain(x, 23, '#1c1f23');
if (mm.open) for (let x = 8; x < 11; x++) grain(x, 23, '#c99a3c'); else for (let x = 3; x < 9; x++) grain(x, 23, '#6b737b'); if (mm.open) for (let x = 8; x < 11; x++) grain(x, 23, '#c99a3c'); else for (let x = 3; x < 9; x++) grain(x, 23, '#6b737b');
// the sight glass fills from the bottom in the colour of what is inside drawSiloWindow(ctx, mm, sx, sy, zp, true);
for (let y = 3; y < 19; y++) grain(9, y, mm.sn > 0 && y >= 19 - Math.max(1, Math.round(16 * full)) ? col : '#14181c');
// roof: a rim, shoulders, the dark hatch and its lid standing open // roof: a rim, shoulders, the dark hatch and its lid standing open
for (let x = 0; x < 12; x++) { for (let x = 0; x < 12; x++) {
grain(x, 1, x >= 3 && x < 9 ? '#15181b' : '#5b646c'); grain(x, 1, x >= 3 && x < 9 ? '#15181b' : '#5b646c');
@@ -204,14 +197,35 @@ function drawSiloPixel(ctx, mm, sx, sy, zp) {
renderMonoFont(); renderMonoFont();
ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'rgba(20,24,28,0.75)'; ctx.save(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = 'rgba(20,24,28,0.75)';
ctx.font = `700 ${Math.max(8, Math.round(zp * 1.15))}px ${R.monoFont}`; ctx.font = `700 ${Math.max(8, Math.round(zp * 1.15))}px ${R.monoFont}`;
ctx.fillText('SILO', sx + 5.6 * zp, sy + 5 * zp); ctx.fillText('SILO', sx + 4.5 * zp, sy + 5 * zp, 5 * zp);
ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`; ctx.font = `600 ${Math.max(8, Math.round(zp * 0.95))}px ${R.monoFont}`;
ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', sx + 5.6 * zp, sy + 16.8 * zp); ctx.fillText(mm.sn ? mm.sn.toLocaleString('en-US') : 'EMPTY', sx + 4.5 * zp, sy + 16.8 * zp, 5 * zp);
if (mm.sn) { grain(4, 18, col); grain(5, 18, col); grain(6, 18, col); } if (mm.sn) { grain(4, 18, col); grain(5, 18, col); grain(6, 18, col); }
ctx.restore(); ctx.restore();
} }
} }
// The same framed, three-grain window in both looks; one grain remains visible near empty.
function drawSiloWindow(ctx, mm, sx, sy, zp, pixel) {
const height = mm.sn > 0 ? Math.max(1, Math.round(16 * Math.min(1, mm.sn / SILO_CAP))) : 0;
ctx.fillStyle = '#c9d1d8'; ctx.fillRect(sx + 7 * zp, sy + 2 * zp, 5 * zp, 18 * zp);
ctx.fillStyle = '#14181c'; ctx.fillRect(sx + 8 * zp, sy + 3 * zp, 3 * zp, 16 * zp);
if (!height) return;
// Lift dark fuel slightly so coal stays distinct from the empty glass.
const lift = Math.max(COLR[mm.sm], COLG[mm.sm], COLB[mm.sm]) < 80 ? 32 : 0;
if (pixel) {
for (let y = 19 - height; y < 19; y++) for (let x = 8; x < 11; x++) {
const h = Math.imul(mm.x + x, 374761393) ^ Math.imul(mm.y + y, 668265263);
const n = lift + (((h ^ h >>> 13) >>> 0) % 3 - 1) * 8;
ctx.fillStyle = `rgb(${COLR[mm.sm] + n},${COLG[mm.sm] + n},${COLB[mm.sm] + n})`;
ctx.fillRect(sx + x * zp, sy + y * zp, zp, zp);
}
} else {
ctx.fillStyle = `rgb(${COLR[mm.sm] + lift},${COLG[mm.sm] + lift},${COLB[mm.sm] + lift})`;
ctx.fillRect(sx + 8 * zp, sy + (19 - height) * zp, 3 * zp, height * zp);
}
}
// Shelves show each block's fill and materials; the bottom gate belongs to the column. // Shelves show each block's fill and materials; the bottom gate belongs to the column.
function drawRackBlocks() { function drawRackBlocks() {
const ctx = R.ctx; const ctx = R.ctx;
+1 -2
View File
@@ -43,7 +43,7 @@ function toolPulse(ax, ay, width, tune) {
// every cell in the cone is hit, with no untouched stripes between the rays // every cell in the cone is hit, with no untouched stripes between the rays
drillPulses.push({ ox: o.x, oy: o.y, ang, w: drillWidth, jit: rnd(), n: Wd.rays, damp: Wd.damp, en: new Float32Array(Wd.rays).fill(Wd.en), s: new Float32Array(Wd.rays), r: 0, range, tune: tune || 0 }); drillPulses.push({ ox: o.x, oy: o.y, ang, w: drillWidth, jit: rnd(), n: Wd.rays, damp: Wd.damp, en: new Float32Array(Wd.rays).fill(Wd.en), s: new Float32Array(Wd.rays), r: 0, range, tune: tune || 0 });
lastPulseTick = tick; lastPulseTick = tick;
// say why nothing breaks: the first solid on the centre line is too hard, or out of reach // say why nothing breaks: the first solid on the centre line is too hard
let status = ''; let status = '';
for (let k = 3; k < reach; k++) { for (let k = 3; k < reach; k++) {
const x = Math.floor(o.x + dx / d * k), y = Math.floor(o.y + dy / d * k); const x = Math.floor(o.x + dx / d * k), y = Math.floor(o.y + dy / d * k);
@@ -57,7 +57,6 @@ function toolPulse(ax, ay, width, tune) {
} }
break; break;
} }
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;
+17 -11
View File
@@ -81,17 +81,23 @@ function hoverInfo(ax, ay) {
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return { t1: '', t2: ['Outside the world'], warn: '', air: true }; if (ax < 0 || ay < 0 || ax >= W || ay >= H) return { t1: '', t2: ['Outside the world'], warn: '', air: true };
const i = ay * W + ax, m = mat[i]; const i = ay * W + ax, m = mat[i];
let warn = ''; let warn = '';
let t1 = MD[m].name, t2 = []; let t1 = MD[m].name, t2 = [], contents = '';
const p = part[i], id = mach[i]; const p = part[i];
// Cone gaps have no machine cells; hover still belongs to the silo's whole outline.
const silo = machines.find(mm => mm && mm.type === 'silo' && ax >= mm.x && ax < mm.x + mm.w && ay >= mm.y && ay < mm.y + mm.h);
const id = silo ? silo.id : mach[i];
const b = blockP[((ay / BS) | 0) * BW + ((ax / BS) | 0)]; const b = blockP[((ay / BS) | 0) * BW + ((ax / BS) | 0)];
const bar = occ[i] ? ents.find(e => ax >= e.ix && ax < e.ix + BAR_W && ay >= e.iy && ay < e.iy + BAR_H) : null; const bar = occ[i] ? ents.find(e => ax >= e.ix && ax < e.ix + BAR_W && ay >= e.iy && ay < e.iy + BAR_H) : null;
if (bar) { t1 = MD[bar.m].name; t2.push(fmtT(bar.T)); } if (bar && !silo) { t1 = MD[bar.m].name; t2.push(fmtT(bar.T)); }
else if (id && machines[id]) { else if (id && machines[id]) {
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.hot > 760) { t2.push(`${mm.hot}°C · melts at 1085°C`); warn = 'too hot'; } } 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') {
contents = mm.sn ? `${MD[mm.sm].name.toLowerCase()} ${mm.sn.toLocaleString('en-US')} / ${SILO_CAP.toLocaleString('en-US')}` : 'empty';
t2.push(mm.sn ? `holds ${mm.sn.toLocaleString('en-US')} / ${SILO_CAP.toLocaleString('en-US')} ${MD[mm.sm].name.toLowerCase()}` : `empty (holds ${SILO_CAP.toLocaleString('en-US')} grains of one material)`, mm.open ? 'gate open · F shuts it' : 'gate shut · F opens it');
}
} else if (b === BP_RACK) { } else if (b === BP_RACK) {
const top = rackTop((ay >> 2) * BW + (ax >> 2)), counts = new Map(); const top = rackTop((ay >> 2) * BW + (ax >> 2)), counts = new Map();
let n = 0, capacity = 0; let n = 0, capacity = 0;
@@ -135,9 +141,9 @@ function hoverInfo(ax, ay) {
if (!ok && toolId() === 'drill') warn = HARD[m] > 6 ? 'cannot be drilled' : 'too hard'; if (!ok && toolId() === 'drill') warn = HARD[m] > 6 ? 'cannot be drilled' : 'too hard';
} }
if (!bar && PH[m] === PH_POWDER) t2.push('loose'); if (!bar && PH[m] === PH_POWDER) t2.push('loose');
if (!bar && m === EMPTY && bgm[i]) { t1 = bgm[i] === WOOD ? 'Tree trunk' : 'Leaves'; t2.unshift(bgm[i] === WOOD ? 'drill it for wood chunks' : 'water and sand pass through'); } if (!id && !bar && m === EMPTY && bgm[i]) { t1 = bgm[i] === WOOD ? 'Tree trunk' : 'Leaves'; t2.unshift(bgm[i] === WOOD ? 'drill it for wood chunks' : 'water and sand pass through'); }
if (flags[i] & F_BURN) t2.push('burning'); if (flags[i] & F_BURN) t2.push('burning');
const mo = moldAt(ax, ay); const mo = silo ? null : 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` : '100% set · press F') : mo.state === 'empty' ? 'empty' : `${mo.state === 'short' ? 'not full' : mo.state} ${Math.round(mo.fill * 100)}%`]; 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)}%`];
@@ -146,12 +152,12 @@ function hoverInfo(ax, ay) {
const hint = furnaceJunkHint(mo).trim(); if (hint) t2.push(hint); 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' || inReach(ax, ay);
if (!reachOk) { t2.push('out of reach'); warn = 'out of reach'; } if (!reachOk) { t2.push('out of reach'); warn = 'out of reach'; }
// what it does, unless the cursor is on a machine or mold, which have their own details // what it does, unless the cursor is on a machine or mold, which have their own details
const what = bar ? bar.m : !mo && !id && !(p && p !== P_GRATE && p !== P_PIPE) && m !== BELTL && m !== BELTR && m !== PIPE ? (m === EMPTY && bgm[i] ? bgm[i] : m) : 0; const what = bar ? bar.m : !mo && !id && !(p && p !== P_GRATE && p !== P_PIPE) && m !== BELTL && m !== BELTR && m !== PIPE ? (m === EMPTY && bgm[i] ? bgm[i] : m) : 0;
const t3 = what ? reactionsOf(what) : []; const t3 = what ? reactionsOf(what) : [];
return { t1, t2, t3, warn, air: m === EMPTY && !bgm[i] && !bar && !mo && !id && !p && !b }; return { t1, t2, t3, warn, contents, air: m === EMPTY && !bgm[i] && !bar && !mo && !id && !p && !b };
} }
function renderInfo() { function renderInfo() {
const r = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)); const r = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
@@ -167,8 +173,8 @@ function drawCursorLabel(px, py, r) {
const ctx = R.ctx, dpr = R.canvas.width / window.innerWidth; const ctx = R.ctx, dpr = R.canvas.width / window.innerWidth;
const fs = Math.round(12 * dpr * SET.uiScale); const fs = Math.round(12 * dpr * SET.uiScale);
if (!R.uiFont) R.uiFont = getComputedStyle(document.documentElement).getPropertyValue('--font-ui').trim() || 'sans-serif'; if (!R.uiFont) R.uiFont = getComputedStyle(document.documentElement).getPropertyValue('--font-ui').trim() || 'sans-serif';
const main = info.t1 + (player.lens && R.lens ? '' : ''); const main = info.t1 + (info.contents && info.warn ? ` ${info.contents}` : '');
const sub = info.warn || (R.lens ? info.t2.find(t => /°C$/.test(t)) || '' : ''); const sub = info.warn || info.contents || (R.lens ? info.t2.find(t => /°C$/.test(t)) || '' : '');
ctx.save(); ctx.save();
ctx.font = `600 ${fs}px ${R.uiFont}`; ctx.font = `600 ${fs}px ${R.uiFont}`;
const w1 = ctx.measureText(main).width; const w1 = ctx.measureText(main).width;
@@ -182,7 +188,7 @@ function drawCursorLabel(px, py, r) {
ctx.beginPath(); ctx.roundRect(x, y, w1 + w2 + padX * 2, h, 4 * dpr); ctx.fill(); ctx.beginPath(); ctx.roundRect(x, y, w1 + w2 + padX * 2, h, 4 * dpr); ctx.fill();
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.font = `600 ${fs}px ${R.uiFont}`; ctx.fillStyle = '#efe6d8'; ctx.fillText(main, x + padX, y + h / 2); ctx.font = `600 ${fs}px ${R.uiFont}`; ctx.fillStyle = '#efe6d8'; ctx.fillText(main, x + padX, y + h / 2);
if (sub) { ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`; ctx.fillStyle = info.warn ? '#ef6a55' : '#ffd27a'; ctx.fillText(sub, x + padX + w1 + fs * 0.6, y + h / 2); } if (sub) { ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`; ctx.fillStyle = info.warn ? '#ef6a55' : info.contents ? '#efe6d8' : '#ffd27a'; ctx.fillText(sub, x + padX + w1 + fs * 0.6, y + h / 2); }
ctx.restore(); ctx.restore();
} }
+5 -2
View File
@@ -74,7 +74,7 @@ function drawDrillAim(ax, ay) {
const o = drillOrigin(), c = worldToScreenPx(o.x, o.y), p = worldToScreenPx(ax + 0.5, ay + 0.5); const o = drillOrigin(), c = worldToScreenPx(o.x, o.y), p = worldToScreenPx(ax + 0.5, ay + 0.5);
const d = Math.hypot(ax + 0.5 - o.x, ay + 0.5 - o.y), ang = Math.atan2(ay + 0.5 - o.y, ax + 0.5 - o.x); const d = Math.hypot(ax + 0.5 - o.x, ay + 0.5 - o.y), ang = Math.atan2(ay + 0.5 - o.y, ax + 0.5 - o.x);
const Wd = PULSE_W[GAME.drillW], range = Math.max(3, Math.min(drillReach(GAME.drillW), d + 3)); const Wd = PULSE_W[GAME.drillW], range = Math.max(3, Math.min(drillReach(GAME.drillW), d + 3));
const ok = inDrillReach(ax, ay, GAME.drillW), col = ok ? pulseRGB(GAME.drillTune) : '239,106,85'; const col = pulseRGB(GAME.drillTune);
const mouth = fanPoints(o.x, o.y, ang, Wd, 2.5, 1), far = fanPoints(o.x, o.y, ang, Wd, range, 10); const mouth = fanPoints(o.x, o.y, ang, Wd, 2.5, 1), far = fanPoints(o.x, o.y, ang, Wd, range, 10);
ctx.save(); ctx.save();
// the outline gets a dark under-stroke so it reads on bright sky as well as dark rock // the outline gets a dark under-stroke so it reads on bright sky as well as dark rock
@@ -84,6 +84,9 @@ function drawDrillAim(ax, ay) {
ctx.strokeStyle = 'rgba(10,14,20,0.28)'; ctx.lineWidth = Math.max(2, zp * 0.3); ctx.stroke(); ctx.strokeStyle = 'rgba(10,14,20,0.28)'; ctx.lineWidth = Math.max(2, zp * 0.3); ctx.stroke();
ctx.strokeStyle = `rgba(${col},0.6)`; ctx.lineWidth = Math.max(1, zp * 0.14); ctx.stroke(); ctx.strokeStyle = `rgba(${col},0.6)`; ctx.lineWidth = Math.max(1, zp * 0.14); ctx.stroke();
ctx.setLineDash([]); ctx.lineDashOffset = 0; ctx.setLineDash([]); ctx.lineDashOffset = 0;
// A solid far edge marks where the pulse stops, even with a distant cursor.
ctx.beginPath(); far.forEach((pt, k) => k ? ctx.lineTo(pt.x, pt.y) : ctx.moveTo(pt.x, pt.y));
ctx.strokeStyle = `rgba(${col},0.85)`; ctx.lineWidth = Math.max(1, zp * 0.22); ctx.stroke();
// the emitter glows across the bot's face while it fires // the emitter glows across the bot's face while it fires
const hot = Math.max(0, 1 - (tick - lastPulseTick) / PULSE_EVERY); const hot = Math.max(0, 1 - (tick - lastPulseTick) / PULSE_EVERY);
if (GAME.lmb && hot > 0) { if (GAME.lmb && hot > 0) {
@@ -95,7 +98,7 @@ function drawDrillAim(ax, ay) {
ctx.fillStyle = g; ctx.fillRect(c.x - zp * 3.5, c.y - zp * 3.5, zp * 7, zp * 7); ctx.fillStyle = g; ctx.fillRect(c.x - zp * 3.5, c.y - zp * 3.5, zp * 7, zp * 7);
ctx.globalCompositeOperation = 'source-over'; ctx.globalCompositeOperation = 'source-over';
} }
ctx.strokeStyle = ok ? 'rgba(239,230,216,0.85)' : 'rgba(239,106,85,0.85)'; ctx.lineWidth = Math.max(1, zp * 0.2); ctx.strokeStyle = 'rgba(239,230,216,0.85)'; ctx.lineWidth = Math.max(1, zp * 0.2);
const k = zp * 0.9; const k = zp * 0.9;
ctx.beginPath(); ctx.moveTo(p.x - k * 1.6, p.y); ctx.lineTo(p.x - k * 0.5, p.y); ctx.moveTo(p.x + k * 0.5, p.y); ctx.lineTo(p.x + k * 1.6, p.y); ctx.beginPath(); ctx.moveTo(p.x - k * 1.6, p.y); ctx.lineTo(p.x - k * 0.5, p.y); ctx.moveTo(p.x + k * 0.5, p.y); ctx.lineTo(p.x + k * 1.6, p.y);
ctx.moveTo(p.x, p.y - k * 1.6); ctx.lineTo(p.x, p.y - k * 0.5); ctx.moveTo(p.x, p.y + k * 0.5); ctx.lineTo(p.x, p.y + k * 1.6); ctx.stroke(); ctx.moveTo(p.x, p.y - k * 1.6); ctx.lineTo(p.x, p.y - k * 0.5); ctx.moveTo(p.x, p.y + k * 0.5); ctx.lineTo(p.x, p.y + k * 1.6); ctx.stroke();
-3
View File
@@ -70,9 +70,6 @@ function drawPreviews() {
if (!reachOk && t !== 'drill' && !sel) { if (!reachOk && t !== 'drill' && !sel) {
drawReachCircle(REACH, 0.35); drawReachCircle(REACH, 0.35);
} }
if (t === 'drill' && !sel && !pasting && !inDrillReach(ax, ay, GAME.drillW)) {
drawReachCircle(drillReach(GAME.drillW), 0.45);
}
drawDrillPulses(); drawDrillPulses();
if (t === 'drill' && !sel && !pasting) drawDrillAim(ax, ay); if (t === 'drill' && !sel && !pasting) drawDrillAim(ax, ay);
if (sel) drawSelection(sel, ax, ay); if (sel) drawSelection(sel, ax, ay);
+72
View File
@@ -1134,6 +1134,7 @@ with sync_playwright() as p:
settle(1500) settle(1500)
ev("__pad.buttons[7].pressed = false; __pad.buttons[7].value = 0;") ev("__pad.buttons[7].pressed = false; __pad.buttons[7].value = 0;")
settle(200) settle(200)
check('gamepad drill aim stays within its reach', ev('Math.hypot(GAME.padAim.x, GAME.padAim.y) <= drillReach(GAME.drillW)'))
check('gamepad RT digs where the right stick aims', ev('stats.dug') > d0, f"{d0}->{ev('stats.dug')}") check('gamepad RT digs where the right stick aims', ev('stats.dug') > d0, f"{d0}->{ev('stats.dug')}")
ev("__pad.buttons[5].pressed = true;"); settle(100); ev("__pad.buttons[5].pressed = false;"); settle(100) ev("__pad.buttons[5].pressed = true;"); settle(100); ev("__pad.buttons[5].pressed = false;"); settle(100)
check('gamepad RB switches to the vacuum', ev('GAME.tool') == 1) check('gamepad RB switches to the vacuum', ev('GAME.tool') == 1)
@@ -1185,6 +1186,77 @@ with sync_playwright() as p:
fp.goto(url) fp.goto(url)
fp.evaluate('startNewWorld(4242, 512, 384)') fp.evaluate('startNewWorld(4242, 512, 384)')
fp.wait_for_function('GAME.running') fp.wait_for_function('GAME.running')
# Silo feedback uses real pointer positions; drawing checks sample an isolated canvas.
fp.evaluate("""() => {
GAME.paused = true; SET.fog = false; SET.look = 'pixel'; SET.zoom = 8; SET.cursorLabel = true;
SET.cameraLead = false; SET.cameraSmooth = 0; resizeRender();
for (let y = 80; y < 140; y++) for (let x = 80; x < 160; x++) { setCell(y * W + x, EMPTY, 20, 0); bgm[y * W + x] = 0; }
window.testSilo = addMachine('silo', 28, 25, 0);
bot.x = 104; bot.y = 106; snapCamera(); $('cards').replaceChildren();
}""")
for amount in [0, 2000]:
fp.evaluate(f'testSilo.sm = SAND; testSilo.sn = {amount}')
for name, dx, dy in [('drum', 5, 8), ('hatch with snow', 5, 0), ('cone gap', 1, 22), ('leg', 0, 23), ('gate', 5, 23)]:
fp.evaluate('setCell((testSilo.y) * W + testSilo.x + 5, SNOW, -10, 0)')
point = fp.evaluate(f'(() => {{ const p = worldToScreenPx(testSilo.x + {dx} + 0.5, testSilo.y + {dy} + 0.5); return [p.x * innerWidth / R.canvas.width, p.y * innerWidth / R.canvas.width]; }})()')
fp.mouse.move(*point); fp.wait_for_timeout(200)
want = 'sand 2,000 / 4,000' if amount else 'empty'
check(f'silo {name} hover shows {want}', fp.evaluate(f"hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)).t1 === 'Silo' && hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)).contents === '{want}' && $('info').textContent.includes('{('holds 2,000 / 4,000 sand' if amount else 'empty')}')"))
check(f'silo {name} cursor label draws {want} in normal colour', fp.evaluate("""() => {
const calls = [], ctx = R.ctx, text = ctx.fillText;
ctx.fillText = function(t) { calls.push([t, this.fillStyle]); };
try { drawCursorLabel(500, 300, 1); } finally { ctx.fillText = text; }
const info = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
return calls.some(([t, c]) => t === info.contents && c === '#efe6d8');
}"""))
check('snow in silo is named snow', fp.evaluate("testSilo.sm = SNOW; hoverInfo(testSilo.x + 5, testSilo.y + 8).contents === 'snow 2,000 / 4,000'"))
for look in ['pixel', 'smooth']:
check(f'{look} silo fill pixels show empty, near empty, half and full at zooms 2 through 12', fp.evaluate("""pixel => {
const canvas = document.createElement('canvas'); canvas.width = 160; canvas.height = 300;
const ctx = canvas.getContext('2d'), mm = { x: 112, y: 100, sm: SAND };
const at = (x, y, z) => Array.from(ctx.getImageData(Math.floor(x * z), Math.floor(y * z), 1, 1).data).slice(0, 3).join(',');
for (let z = 2; z <= 12; z++) for (const [n, h] of [[0, 0], [1, 1], [2000, 8], [4000, 16]]) {
mm.sn = n; drawSiloWindow(ctx, mm, 0, 0, z, pixel);
for (let y = 3; y < 19; y++) if ((at(9.5, y + 0.5, z) !== '20,24,28') !== (y >= 19 - h)) return false;
}
// Every powder siloTick accepts must stand apart from dark glass, including coal.
for (let m = 1; m < NMAT; m++) if (PH[m] === PH_POWDER) {
mm.sm = m; mm.sn = 2000; drawSiloWindow(ctx, mm, 0, 0, 2, pixel);
const filled = at(9.5, 18.5, 2).split(',').map(Number);
if (Math.max(...filled.map((c, k) => Math.abs(c - [20, 24, 28][k]))) < 25) return false;
const image = ctx.getImageData(0, 0, 24, 48).data.slice().join(',');
drawSiloWindow(ctx, mm, 0, 0, 2, pixel);
if (ctx.getImageData(0, 0, 24, 48).data.join(',') !== image) return false;
}
return true;
}""", look == 'pixel'))
fp.set_viewport_size({'width': 700, 'height': 800})
check('silo contents remain available when the narrow HUD hides the info plate', fp.evaluate("getComputedStyle($('info')).display === 'none' && hoverInfo(testSilo.x + 5, testSilo.y + 8).contents === 'snow 2,000 / 4,000'"))
fp.set_viewport_size({'width': 1280, 'height': 800})
check('other tools still warn beyond reach', fp.evaluate("GAME.tool = 1; hoverInfo(testSilo.x + 80, testSilo.y).warn === 'out of reach'"))
# Holding the drill with a distant pointer is ordinary aim, but hard rock still warns.
fp.evaluate('bot.y = 130; snapCamera()')
fp.keyboard.press('1')
fp.mouse.move(1000, 400); fp.wait_for_timeout(100)
fp.evaluate("$('toasts').replaceChildren(); lastPulseTick = -99; player.bitTier = 0; GAME.drillTune = 0")
fp.evaluate('GAME.paused = false')
fp.mouse.down()
fp.evaluate('GAME.paused = true; for (let k = 0; k < 24; k++) { tick++; toolTick(); }')
check('held drill beyond reach makes no toast or distance label', fp.evaluate("GAME.lmb && !inDrillReach(GAME.aim.x, GAME.aim.y, GAME.drillW) && !$('toasts').children.length && !hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)).warn && !hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y)).t2.includes('out of reach')"))
check('distant drill fan and crosshair stay normal with no red reach circle', fp.evaluate("""() => {
const ctx = R.ctx, stroke = ctx.stroke, circle = drawReachCircle, colours = []; let circles = 0;
ctx.stroke = function() { colours.push(this.strokeStyle); };
drawReachCircle = () => circles++;
try { drawPreviews(); } finally { ctx.stroke = stroke; drawReachCircle = circle; }
return circles === 0 && !colours.some(c => c.includes('239, 106, 85'));
}"""))
fp.evaluate("""() => {
const o = drillOrigin(), dx = GAME.aim.x + 0.5 - o.x, dy = GAME.aim.y + 0.5 - o.y, d = Math.hypot(dx, dy);
for (let k = 6; k < 10; k++) setCell(Math.floor(o.y + dy / d * k) * W + Math.floor(o.x + dx / d * k), GRANITE, 20, F_NAT);
tick += 20; toolTick();
}""")
check('held drill still toasts too hard with a distant pointer', fp.evaluate("$('toasts').textContent.includes('Granite is too hard; needs an iron bit.')"))
fp.mouse.up()
fp.evaluate("""() => { fp.evaluate("""() => {
GAME.paused = true; SET.fog = true; SET.look = 'pixel'; SET.zoom = 6; SET.lighting = 'off'; GAME.paused = true; SET.fog = true; SET.look = 'pixel'; SET.zoom = 6; SET.lighting = 'off';
SET.cameraLead = false; SET.cameraSmooth = 0; resizeRender(); SET.cameraLead = false; SET.cameraSmooth = 0; resizeRender();
+7 -2
View File
@@ -16,6 +16,11 @@ const med = { d: depth(), n: opened() };
check('held on a stone wall, medium pulses dig a hole and drop rubble', med.d >= 6 && run('stats.dug') > 30 && count(run, run('RSTONE')) > 30, `${med.d} deep, ${med.n} opened, ${count(run, run('RSTONE'))} rubble`); check('held on a stone wall, medium pulses dig a hole and drop rubble', med.d >= 6 && run('stats.dug') > 30 && count(run, run('RSTONE')) > 30, `${med.d} deep, ${med.n} opened, ${count(run, run('RSTONE'))} rubble`);
wall(); hold(150, 78, 1, 0, 0.3); wall(); hold(150, 78, 1, 0, 0.3);
check('rock that is shaking but has not broken yet shows it', run('(() => { let n = 0; for (let i = 0; i < N; i++) if (digp[i] > 0 && PH[mat[i]] === PH_SOLID) n++; return n; })()') > 5); check('rock that is shaking but has not broken yet shows it', run('(() => { let n = 0; for (let i = 0; i < N; i++) if (digp[i] > 0 && PH[mat[i]] === PH_SOLID) n++; return n; })()') > 5);
// A distant cursor chooses direction; the pulse still ends at its reach.
flatWorld(run, 240, 160, 100);
run('bot.x = 40; bot.y = 40; player.bitTier = 0; lastPulseTick = -99;');
check('aiming past reach fires without a distance warning', run("toolPulse(220, 42, 1, 0).status") === '');
check('a distant aim still caps the pulse at its reach', run('drillPulses[drillPulses.length - 1].range === drillReach(1)'));
// the widths, with a copper bit so the reach is not what stops them // the widths, with a copper bit so the reach is not what stops them
const wall2 = () => { wall(); run('player.bitTier = 1;'); }; const wall2 = () => { wall(); run('player.bitTier = 1;'); };
wall2(); hold(150, 78, 1, 0, 2); wall2(); hold(150, 78, 1, 0, 2);
@@ -81,10 +86,10 @@ check('and the granite stays whole', count(run, run('GRANITE')) === 116 * 40, co
wall(); wall();
run('drillPulses = []; tick += 20;'); run('drillPulses = []; tick += 20;');
const far = JSON.parse(run('JSON.stringify(toolPulse(200, 78, 1, 0))')); const far = JSON.parse(run('JSON.stringify(toolPulse(200, 78, 1, 0))'));
check('aiming past the reach says how far the bit reaches', /Out of reach/.test(far.status), far.status); check('aiming past the reach fires without a warning', far.fired && !far.status, far.status);
run('drillPulses = []; tick += 20;'); run('drillPulses = []; tick += 20;');
const farW = JSON.parse(run('JSON.stringify(toolPulse(125, 78, 2, 0))')), nearM = JSON.parse(run('tick += 20; JSON.stringify(toolPulse(125, 78, 1, 0))')); const farW = JSON.parse(run('JSON.stringify(toolPulse(125, 78, 2, 0))')), nearM = JSON.parse(run('tick += 20; JSON.stringify(toolPulse(125, 78, 1, 0))'));
check('a spot a medium pulse reaches is out of reach for a wide one, and it says so', /Out of reach/.test(farW.status) && /wide/.test(farW.status) && !nearM.status, `${farW.status} | ${nearM.status}`); check('aiming beyond a wide pulse also fires without a warning', farW.fired && !farW.status && nearM.fired && !nearM.status);
// stray bits: a lump of rock left hanging when the rock round it breaks falls apart // stray bits: a lump of rock left hanging when the rock round it breaks falls apart
const hanging = (m) => { const hanging = (m) => {
+17
View File
@@ -0,0 +1,17 @@
// Stored snow keeps its identity and remains in the world's water budget.
const { load, suite, flatWorld, ticks } = require('./harness');
const { run } = load();
const { check, done } = suite();
flatWorld(run, 240, 160, 120);
run("globalThis.snowSilo = addMachine('silo', 20, 20, 0); setCell(80 * W + 85, SNOW, -10, 0); stamp[80 * W + 85] = 0; tickStamp = 1;");
const before = run('worldWater()');
run('siloTick(snowSilo)');
check('silo accepts snow and keeps its name', run('snowSilo.sm === SNOW && snowSilo.sn === 1'));
check('snow inside still counts as water', run('worldWater()') === before);
run('for (let y = 80; y < 104; y++) for (let x = 80; x < 92; x++) temp[y * W + x] = 100;');
ticks(run, 180);
check('stored snow does not melt in a warm silo', run('snowSilo.sm === SNOW && snowSilo.sn === 1'));
run('snowSilo.open = true; siloTick(snowSilo)');
check('opening the gate releases snow, then the silo is empty', run('snowSilo.sn === 0 && snowSilo.sm === EMPTY && mat[103 * W + 84 + (tick & 3)] === SNOW'));
check('releasing snow preserves the water count', run('worldWater()') === before);
done();
+5 -3
View File
@@ -1,9 +1,11 @@
// Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge. // Run after clear-sky.js; leave paused so the open gate keeps its demonstration charge.
for (let k = 0; k < 3; k++) { for (let k = 0; k < 5; k++) {
const bx = BX + 10 + k * 7, by = BY - 5; const bx = BX + 5 + k * 6, by = BY - 5;
placeMachineAt('silo', 0, bx, by); placeMachineAt('silo', 0, bx, by);
const mm = machines.find(m => m && m.type === 'silo' && m.x === bx * 4 && m.y === by * 4); const mm = machines.find(m => m && m.type === 'silo' && m.x === bx * 4 && m.y === by * 4);
mm.sm = k === 2 ? CUORE : SAND; mm.sn = k === 0 ? 0 : Math.floor(SILO_CAP * (k === 1 ? 0.5 : 0.95)); mm.sm = k === 2 ? CUORE : k === 3 ? SNOW : COAL;
if (k === 1) mm.sm = SAND;
mm.sn = [0, 2000, 3800, 2800, 1][k];
mm.open = k === 2; mm.open = k === 2;
} }
window.CAM = [(BX + 18.5) * 4, (BY - 2) * 4]; window.CAM = [(BX + 18.5) * 4, (BY - 2) * 4];