Drill: keep pass-through; restore the original loose-grain nudge, drop dust and push (t92)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 19:15:41 -04:00
co-authored by Claude Opus 5.5
parent 807217b22c
commit 6ac5f463aa
8 changed files with 88 additions and 148 deletions
+8
View File
@@ -561,3 +561,11 @@ above, including:
- **Upgrade ideas for the drill**: an amplifier, a tuning fork, harmonics. Also a Grav pick - **Upgrade ideas for the drill**: an amplifier, a tuning fork, harmonics. Also a Grav pick
upgrade tool and mining charges. See `docs/systems/dig-tools.md`. upgrade tool and mining charges. See `docs/systems/dig-tools.md`.
- The owner's ideas for later are tasks tagged `idea` in the Backlog of the task board. - The owner's ideas for later are tasks tagged `idea` in the Backlog of the task board.
- **Restore the original loose-grain nudge (t92 final owner feedback, 2026-10-28).** After
playing, the owner rejected dust (never visible in game), the strong surface-only push
toward the bot, and fluidising kicks. These supersede the two t92 decisions above. Keep
3% loose-material loss per grain and all damping per distance, with rock bores retuned to
the old feel. Restore the original advancing-front nudge: 30% times ray energy per
half-grain sample, straight back toward the bot, then down or up if blocked. Scale that
chance with sampling distance; do not keep shaking the travelled path.
+2 -2
View File
@@ -327,7 +327,7 @@ Tool helpers: reach, brushes, and plain digging.
- functions: setBuildRange, inBuildRange, inReach, forBrush, drillReach, inDrillReach, toolDig - functions: setBuildRange, inBuildRange, inReach, forBrush, drillReach, inDrillReach, toolDig
- variables: BUILD_MAX, buildBox - variables: BUILD_MAX, buildBox
### src/sim/drill.js (202 lines) ### src/sim/drill.js (183 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, pingRadius, resonancePing - functions: drillOrigin, pulseRay, builtCell, toolPulse, crumbleAround, crumbly, shatterCell, pulseTick, pingRadius, resonancePing
- variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF, PING_VEINS - variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF, PING_VEINS
@@ -482,7 +482,7 @@ Scale colour without changing the smooth-look class.
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, drawSiloWindow, drawRackBlocks - functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks
### src/render/overlays.js (384 lines) ### src/render/overlays.js (370 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, drawJoinMark, drawJoinMarks, flowLoaded, drawFlowRates, pingImage, drawPing, drawHoveredVein, drawPingRecharge, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles - functions: spawnFloater, drawRain, drawFlowArrows, drawJoinMark, drawJoinMarks, flowLoaded, drawFlowRates, pingImage, drawPing, drawHoveredVein, drawPingRecharge, drawRunOutline, drawFilterTags, drawFloaters, moldLabel, drawMolds, drawBot, spawnDebris, spawnSpark, spawnSuck, drawParticles
- variables: PING_SPEED, PING_HOLD, PING_FADE, PING_BANDS, PING_GLINTS, GLINT_MS - variables: PING_SPEED, PING_HOLD, PING_FADE, PING_BANDS, PING_GLINTS, GLINT_MS
+34
View File
@@ -5,6 +5,40 @@ note or request it was for, what changed, the files touched, the tests run, and
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-28: t92 keep pass-through, restore original loose-grain behaviour (Codex)
- Read the 7293787 then dadf62a t92 history and original drill in 90f2d3f. Restored the
original advancing-front nudge and escape order verbatim, except loose damping stays
0.97 per grain and chance is `1 - (1 - 0.3 * e) ** (2 * step)`, preserving the original
30% times energy at the default half-grain sample. Removed travelled-path pushing,
fluidising kicks, dust events and dust-specific event/render plumbing. Normal debris stays.
- Removed dust/channel/fluidising tests. Original ore-draining check was already present
and retained. Added back/down/up/fully-blocked escape checks and a front-only check;
three escape checks failed before restoration, all pass after. Pass-through still opens
78 basalt cells behind 35 rubble grains; 115 blocks breaking and 118 blocks all damage.
- Restoring the nudge initially failed narrow opened-area and follow-face checks. Retuned
narrow/medium damping 0.76/0.69 -> 0.75/0.67; wide stays 0.6084. Unchanged baseline
bore assertions pass: narrow 24/198 versus original 22/181, medium 13/197 versus 13/186,
wide 6/134 versus 6/134 (depth/opened grains; all within 10%). Updated the attenuation
constant expectation for the intentional medium retune; no bore expectations weakened.
- Focused drill suite passes (61 checks). Initial `python3 test/run_all.py --browser`
passed all 33 headless suites, but seven browser suites failed with unavailable WebGPU
in the sandbox (startup timeouts/undefined GPU device). Same full command rerunning
with host GPU access passed all 33 headless and eight browser suites (361 gameplay,
28 smooth, 18 console, 7 caps, 14 GPU, 28 goldens, 71 light, 2 Firefox). Evidence:
`/tmp/t92-restore-browser.log`, `/tmp/t92-restore-browser-host.log`.
- Fixed-powder microbenchmark (median of five after warmup, 20,000 rays): previous
travelled-path code 11.88 ms, restored front-only code 9.82 ms. Fixture stamps powder
to isolate traversal, not moving heaps; not a whole-game profile. Script: `/tmp/t92-restore-bench.js`.
- Captured and inspected `shots/t92-restore-pixel.png` and `shots/t92-restore-smooth.png`:
rubble heap beside a basalt face with a drilled notch, no dust puffs. Scene:
`/tmp/t92-restore-scene.js`, 120 held-medium ticks, zoom 8, lighting off, frozen frame.
- Files: `src/sim/drill.js`, `src/render/overlays.js`, `src/ui/events.js`, `test/t_drill.js`,
`test/t_gpu.py`, generated `docs/PROJECT_MAP.md`, `docs/DESIGN.md`, t92 task and this log.
Recorded the superseding owner decision and final checked feedback subtask; status Review.
No commit. Owner playtesting and whole-game performance remain unverified.
## 2026-10-28: t93 loose faces slump without sticky-step stalls (Codex) ## 2026-10-28: t93 loose faces slump without sticky-step stalls (Codex)
- Inspected the owner's screenshot and traced `powder`, `enterFree`, `move`, `moveChunk`, - Inspected the owner's screenshot and traced `powder`, `enterFree`, `move`, `moveChunk`,
+2 -16
View File
@@ -331,13 +331,8 @@ function drawBot() {
ctx.beginPath(); ctx.moveTo(cx - bot.face * zp, sy + zp * 0.6 + bob); ctx.lineTo(cx - bot.face * zp * 1.6, sy - zp * 0.6 + bob); ctx.stroke(); ctx.beginPath(); ctx.moveTo(cx - bot.face * zp, sy + zp * 0.6 + bob); ctx.lineTo(cx - bot.face * zp * 1.6, sy - zp * 0.6 + bob); ctx.stroke();
ctx.restore(); ctx.restore();
} }
function spawnDebris(x, y, m, dust = false) { function spawnDebris(x, y, m) {
if (!SET.particles || R.particles.length > 400) return; if (!SET.particles || R.particles.length > 400) return;
if (dust) {
R.particles.push({ x: x + 0.5, y: y + 0.5, vx: (Math.random() - 0.5) * 0.12,
vy: -0.06 - Math.random() * 0.06, life: 36, r: 205, g: 190, b: 165, dust: true });
return;
}
for (let k = 0; k < 2; k++) R.particles.push({ x: x + Math.random(), y: y + Math.random(), vx: (Math.random() - 0.5) * 0.5, vy: -Math.random() * 0.5, life: 20 + Math.random() * 20, r: COLR[m], g: COLG[m], b: COLB[m] }); for (let k = 0; k < 2; k++) R.particles.push({ x: x + Math.random(), y: y + Math.random(), vx: (Math.random() - 0.5) * 0.5, vy: -Math.random() * 0.5, life: 20 + Math.random() * 20, r: COLR[m], g: COLG[m], b: COLB[m] });
} }
function spawnSpark(x, y) { function spawnSpark(x, y) {
@@ -363,20 +358,11 @@ function drawParticles() {
p.x += dx / d * sp; p.y += dy / d * sp; p.life--; p.x += dx / d * sp; p.y += dy / d * sp; p.life--;
if (p.life <= 0) { ps.splice(k, 1); continue; } if (p.life <= 0) { ps.splice(k, 1); continue; }
} else { } else {
p.x += p.vx; p.y += p.vy; p.vy += p.dust ? 0 : 0.04; p.life--; p.x += p.vx; p.y += p.vy; p.vy += 0.04; p.life--;
if (p.life <= 0) { ps.splice(k, 1); continue; } if (p.life <= 0) { ps.splice(k, 1); continue; }
} }
if (SET.fog && fogSeen(p.x, p.y) < 64) continue; if (SET.fog && fogSeen(p.x, p.y) < 64) continue;
const s = worldToScreenPx(p.x, p.y); const s = worldToScreenPx(p.x, p.y);
if (p.dust) {
const radius = zp * (1 + (36 - p.life) * 0.035);
const puff = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, radius);
puff.addColorStop(0, `rgba(${p.r},${p.g},${p.b},${0.32 * p.life / 36})`);
puff.addColorStop(1, `rgba(${p.r},${p.g},${p.b},0)`);
ctx.fillStyle = puff;
ctx.fillRect(s.x - radius, s.y - radius, radius * 2, radius * 2);
continue;
}
ctx.fillStyle = `rgba(${p.r | 0},${p.g | 0},${p.b | 0},${Math.min(1, p.life / 10)})`; ctx.fillStyle = `rgba(${p.r | 0},${p.g | 0},${p.b | 0},${Math.min(1, p.life / 10)})`;
const sz = p.spark ? Math.max(1, zp * 0.5) : p.suck ? zp : Math.max(1, zp * 0.7); const sz = p.spark ? Math.max(1, zp * 0.5) : p.suck ? zp : Math.max(1, zp * 0.7);
ctx.fillRect(s.x, s.y, sz, sz); ctx.fillRect(s.x, s.y, sz, sz);
+16 -35
View File
@@ -8,14 +8,14 @@
// medium: a cone, the all-rounder // medium: a cone, the all-rounder
// wide: a broad fan that clears a big face close to the bot, but reaches only a short way // wide: a broad fan that clears a big face close to the bot, but reaches only a short way
// Tuned to one material, a pulse passes through everything else and shatters only that, and // Tuned to one material, a pulse passes through everything else and shatters only that, and
// loose grains it passes are shaken into short hops, so piles flow and pockets drain. // loose grains it passes are jostled toward the bot, so a shattered vein pours out of the wall.
// Built blocks, machines and molds soak pulses up unharmed. // Built blocks, machines and molds soak pulses up unharmed.
// half: half the spread angle; mouth: half the width of the fan where it leaves the bot; // half: half the spread angle; mouth: half the width of the fan where it leaves the bot;
// en: energy per ray; damp: energy kept per grain (higher bores deeper); reach: x bit reach // en: energy per ray; damp: energy kept per grain (higher bores deeper); reach: x bit reach
// Narrow/medium compensate for rubble now passing pulses; wide keeps its old squared loss. // Narrow/medium compensate for rubble now passing pulses; wide keeps its old squared loss.
const PULSE_W = [ const PULSE_W = [
{ name: 'narrow', half: 0.03, mouth: 3.2, rays: 13, en: 1.75, damp: 0.76, reach: 1.45 }, { name: 'narrow', half: 0.03, mouth: 3.2, rays: 13, en: 1.75, damp: 0.75, reach: 1.45 },
{ name: 'medium', half: 0.24, mouth: 3, rays: 23, en: 1.4, damp: 0.69, reach: 1 }, { name: 'medium', half: 0.24, mouth: 3, rays: 23, en: 1.4, damp: 0.67, reach: 1 },
{ name: 'wide', half: 0.62, mouth: 3.4, rays: 41, en: 1.32, damp: 0.6084, reach: 0.7 }, { name: 'wide', half: 0.62, mouth: 3.4, rays: 41, en: 1.32, damp: 0.6084, reach: 0.7 },
]; ];
const PULSE_EVERY = 8, PULSE_SPEED = 2.2, PULSE_K = 240; // ticks between pulses, cells per tick, shake per unit of energy const PULSE_EVERY = 8, PULSE_SPEED = 2.2, PULSE_K = 240; // ticks between pulses, cells per tick, shake per unit of energy
@@ -105,7 +105,6 @@ function pulseTick(step = 0.5) {
const loose = 0.97 ** step, liquid = 0.970225 ** step, built = 0.0625 ** step; const loose = 0.97 ** step, liquid = 0.970225 ** step, built = 0.0625 ** step;
const skip = 0.986049 ** step, hard = 0.1225 ** step, tuned = 0.9216 ** step; const skip = 0.986049 ** step, hard = 0.1225 ** step, tuned = 0.9216 ** step;
const wood = 0.85 ** step, leaves = 0.97 ** step; const wood = 0.85 ** step, leaves = 0.97 ** step;
let dust = 0;
// cells inside the bot's body are not touched (it flies in its own little space) // cells inside the bot's body are not touched (it flies in its own little space)
const bx0 = Math.floor(bot.x), by0 = Math.floor(bot.y), bx1 = Math.ceil(bot.x + bot.w) - 1, by1 = Math.ceil(bot.y + bot.h) - 1; const bx0 = Math.floor(bot.x), by0 = Math.floor(bot.y), bx1 = Math.ceil(bot.x + bot.w) - 1, by1 = Math.ceil(bot.y + bot.h) - 1;
for (const p of drillPulses) { for (const p of drillPulses) {
@@ -115,37 +114,6 @@ function pulseTick(step = 0.5) {
let e = p.en[k]; let e = p.en[k];
if (e < 0.04) continue; if (e < 0.04) continue;
const ray = pulseRay(p.ox, p.oy, p.ang, Wd, -1 + 2 * (k + p.jit) / p.n), ux = ray.ux, uy = ray.uy; const ray = pulseRay(p.ox, p.oy, p.ang, Wd, -1 + 2 * (k + p.jit) / p.n), ux = ray.ux, uy = ray.uy;
// Keep shaking the travelled path while the wave advances, including the pile interior.
let shake = Wd.en, kicks = 0;
for (let d = 0.5; d < lim && shake >= 0.04 && kicks < 3; d++) {
const x = Math.floor(ray.x + ux * d), y = Math.floor(ray.y + uy * d);
if (x < 0 || y < 0 || x >= W || y >= H) break;
if (x >= bx0 && x <= bx1 && y >= by0 && y <= by1) continue;
const i = y * W + x, m = mat[i], ph = PH[m];
if (ph === PH_SOLID) break;
if (ph === PH_LIQUID) { shake *= 0.970225; continue; }
if (ph !== PH_POWDER) continue;
shake *= 0.97;
if (stamp[i] !== tickStamp && !occ[i] && !part[i] && rnd() < 0.55 * shake) {
// Lead with the reverse ray; blocked grains can lift or spill to either side.
const backX = Math.abs(ux) > 0.38 ? -Math.sign(ux) : 0;
const backY = Math.abs(uy) > 0.38 ? -Math.sign(uy) : 0;
const side = rnd() < 0.5 ? -1 : 1;
for (let hop = 0; hop < 5; hop++) {
const mx = hop < 2 ? backX : hop < 4 ? side : -side;
const my = hop === 0 ? backY : hop === 1 ? -1 : -(hop & 1);
const nx = x + mx, ny = y + my;
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const j = ny * W + nx;
if (mat[j] !== EMPTY && PH[mat[j]] !== PH_GAS) continue;
if (occ[j] || part[j] || stamp[j] === tickStamp || !enter(m, ph, j, mx, my)) continue;
if (my && CORNER_SEAL && PH[mat[i + mx]] === PH_SOLID && PH[mat[i + my * W]] === PH_SOLID) continue;
move(i, j, x, y, nx, ny); kicks++;
if (dust < 4) { emit({ t: 'debris', x, y, m, dust: true }); dust++; }
break;
}
}
}
let sd = p.s[k]; let sd = p.s[k];
while (sd < lim && e >= 0.04) { while (sd < lim && e >= 0.04) {
sd += step; sd += step;
@@ -160,7 +128,20 @@ function pulseTick(step = 0.5) {
} }
if (ph === PH_LIQUID) { e *= liquid; continue; } if (ph === PH_LIQUID) { e *= liquid; continue; }
if (ph === PH_POWDER) { if (ph === PH_POWDER) {
// loose grains are jostled back toward the bot, so a shattered pocket drains out of the
// wall and a bore clears itself: straight back if there is room, else down or up a step
e *= loose; e *= loose;
// Preserve the old half-grain chance when the sampling distance changes.
if (stamp[i] !== tickStamp && rnd() < 1 - (1 - 0.3 * e) ** (2 * step)) {
const sx = Math.abs(ux) > 0.3 ? -Math.sign(ux) : 0, sy = Math.abs(uy) > 0.3 ? -Math.sign(uy) : 0;
for (const [mx, my] of [[sx, sy], [sx, 1], [sx, -1], [0, sy]]) {
if (!mx && !my) continue;
const nx = x + mx, ny = y + my;
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
const j = ny * W + nx;
if ((mat[j] === EMPTY || PH[mat[j]] === PH_GAS) && !occ[j] && !part[j]) { move(i, j, x, y, nx, ny); break; }
}
}
continue; continue;
} }
if (builtCell(i, x, y)) { e *= built; continue; } if (builtCell(i, x, y)) { e *= built; continue; }
+1 -1
View File
@@ -17,7 +17,7 @@ function drainEvents() {
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',
body: [e.kind === 'bit' ? `Digs hardness ${BIT_TIERS[e.tier].hard} and below.` : `Holds ${TANK_TIERS[e.tier].cap.toLocaleString('en-US')} grains.`] }); body: [e.kind === 'bit' ? `Digs hardness ${BIT_TIERS[e.tier].hard} and below.` : `Holds ${TANK_TIERS[e.tier].cap.toLocaleString('en-US')} grains.`] });
hudDirty = true; hudDirty = true;
} else if (e.t === 'debris') spawnDebris(e.x, e.y, e.m, e.dust); } 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 === '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));
+23 -79
View File
@@ -178,7 +178,7 @@ check('a mold and what sets inside it stay safe from the drill', run(`(() => {
for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) if (crumbly(i)) return false; for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) if (crumbly(i)) return false;
return true; return true;
})()`)); })()`));
// No gravity or kicks here: measure absorption through a fixed thickness, beyond normal reach // No gravity or nudges here: measure absorption through a fixed thickness, beyond normal reach
// for the 115-grain case. All real medium rays still cross the powder before reaching basalt. // for the 115-grain case. All real medium rays still cross the powder before reaching basalt.
function rubbleShield(thickness) { function rubbleShield(thickness) {
flatWorld(run, 240, 160, 150); flatWorld(run, 240, 160, 150);
@@ -206,36 +206,7 @@ check('115 grains of rubble prevent basalt breaking during sustained drilling',
const through118 = rubbleShield(118); const through118 = rubbleShield(118);
check('118 grains extinguish the ray before it can even shake basalt', through118 === 0 && !run('digp.some(d => d > 0)'), `${through118} basalt opened`); check('118 grains extinguish the ray before it can even shake basalt', through118 === 0 && !run('digp.some(d => d > 0)'), `${through118} basalt opened`);
// A 35-grain-deep pile against basalt: compare with gravity alone, tracking only rubble. // Absorption over a fixed distance, isolated from gravity, nudges and cell breakage.
function channelPile(fire) {
flatWorld(run, 240, 160, 110);
return run(`(() => {
RS=12345; tick=0; lastPulseTick=-99; drillPulses=[]; player.bitTier=3;
bot.x=45; bot.y=88; EV.length=0;
for(let y=85;y<110;y++) {
for(let x=55;x<90;x++) setCell(y*W+x,RSTONE,20,0);
for(let x=90;x<150;x++) setCell(y*W+x,BASALT,20,F_NAT);
}
const measure=()=>{let n=0,sx=0,empty=0;for(let i=0;i<N;i++) if(mat[i]===RSTONE){n++;sx+=i%W;}
for(let x=55;x<90;x++) if(mat[90*W+x]===EMPTY) empty++;
return {n,cx:sx/n,empty};};
const before=measure(); let dust=0;
for(let t=0;t<480;t++) {
${fire ? 'toolPulse(130,90,1,0);' : ''} simTick();
dust+=EV.filter(e=>e.t==='debris' && e.dust).length; EV.length=0;
}
return {before,after:measure(),dust};
})()`);
}
const channelRest=channelPile(false), channel=channelPile(true);
check('medium pulses open most of the centre ray through a 35-grain pile within 480 ticks',
channel.after.empty>=28 && channel.after.empty>channelRest.after.empty+15,JSON.stringify(channel));
check('the pile streams back toward the bot rather than just bouncing on its surface',
channel.after.cx<channel.before.cx-5 && channel.after.cx<channelRest.after.cx-5,JSON.stringify(channelRest));
check('channel drilling retains every rubble grain',channel.after.n===channel.before.n,`${channel.before.n}->${channel.after.n}`);
check('shaking loose grains emits dust puffs',channel.dust>0,`${channel.dust} puffs`);
// Absorption over a fixed distance, isolated from gravity, kicks and cell breakage.
function transmission(material, distance, step, tune = 0) { function transmission(material, distance, step, tune = 0) {
flatWorld(run, 240, 160, 150); flatWorld(run, 240, 160, 150);
return run(`(() => { return run(`(() => {
@@ -249,7 +220,7 @@ function transmission(material, distance, step, tune = 0) {
for (const [m, distance, factor, tune] of [ for (const [m, distance, factor, tune] of [
['RSTONE',35,0.97,0], ['SAND',35,0.97,0], ['FEORE',35,0.97,0], ['COAL',35,0.97,0], ['RSTONE',35,0.97,0], ['SAND',35,0.97,0], ['FEORE',35,0.97,0], ['COAL',35,0.97,0],
['WATER',10,0.985**2,0], ['GRANITE',1,0.35**2,0], ['STONE',10,0.993**2,'CUORE'], ['WATER',10,0.985**2,0], ['GRANITE',1,0.35**2,0], ['STONE',10,0.993**2,'CUORE'],
['STONE',1,0.69,0], ['STONE',1,0.96**2,'RSTONE'], ['BELTL',1,0.25**2,0], ['STONE',1,0.67,0], ['STONE',1,0.96**2,'RSTONE'], ['BELTL',1,0.25**2,0],
]) { ]) {
const energies = [0.25,0.5,1].map(step => transmission(m,distance,step,tune)); const energies = [0.25,0.5,1].map(step => transmission(m,distance,step,tune));
const expected = 1.4 * factor ** distance; const expected = 1.4 * factor ** distance;
@@ -257,57 +228,30 @@ for (const [m, distance, factor, tune] of [
energies.every(e => Math.abs(e-expected) < 0.00001), energies.map(e=>e.toFixed(5)).join(' / ')); energies.every(e => Math.abs(e-expected) < 0.00001), energies.map(e=>e.toFixed(5)).join(' / '));
} }
// A settled heap must slump further than an undrilled control, conserving all its grains. // The original nudge visits only the wave front: back first, then down, then up.
function shakenPile(pulse) { for (const [blocked, expectedY] of [[0,80], [1,81], [2,79], [3,80]]) {
flatWorld(run,240,160,110);
run(`RS=12345; tick=0; lastPulseTick=-99; drillPulses=[]; player.bitTier=3; bot.x=40; bot.y=88;
for(let x=65;x<116;x++) for(let y=85+Math.abs(x-90);y<110;y++) setCell(y*W+x,RSTONE,20,0);
for(let t=0;t<240;t++) simTick();`);
const height = () => run(`(() => {let top=110;for(let i=0;i<110*W;i++) if(mat[i]===RSTONE) top=Math.min(top,(i/W)|0);return 110-top;})()`);
const before=height(), grains=count(run,run('RSTONE'));
run(`for(let t=0;t<480;t++) {${pulse?'toolPulse(90,94,1,0);':''}simTick();}`);
return {before,after:height(),grains,left:count(run,run('RSTONE'))};
}
const restingPile=shakenPile(false), fluidPile=shakenPile(true);
check('pulses fluidise a settled rubble pile so its height drops',
fluidPile.after <= fluidPile.before-3 && fluidPile.after < restingPile.after-3,
`resting ${restingPile.before}->${restingPile.after}, shaken ${fluidPile.before}->${fluidPile.after}`);
check('shaking conserves the pile material',fluidPile.grains===fluidPile.left,`${fluidPile.grains}->${fluidPile.left}`);
check('repeating a seeded pile gives the same result',JSON.stringify(shakenPile(true))===JSON.stringify(fluidPile));
// A wave must still shake loose grains far behind its advancing front, throughout the path.
flatWorld(run,240,160,150); flatWorld(run,240,160,150);
const travelled=run(`(() => { const got=run(`(() => {
RS=12345; bot.x=0;bot.y=0;tickStamp=1;EV.length=0; bot.x=0;bot.y=0;tickStamp=1;RS=12345;
for(const x of [25,45,65]) setCell(80*W+x,RSTONE,20,0); setCell(80*W+25,RSTONE,20,0);
const p={ox:19.5,oy:80.5,ang:0,w:1,jit:0.5,n:1,damp:PULSE_W[1].damp, if (${blocked}>=1) setCell(80*W+24,STONE,20,F_NAT);
en:new Float32Array([1.4]),s:new Float32Array([60]),r:60,range:100,tune:0}; if (${blocked}>=2) setCell(81*W+24,STONE,20,F_NAT);
if (${blocked}>=3) setCell(79*W+24,STONE,20,F_NAT);
const p={ox:24.5,oy:80.5,ang:0,w:1,jit:0.5,n:1,damp:PULSE_W[1].damp,
en:new Float32Array([1.4]),s:new Float32Array(1),r:0,range:1,tune:0};
drillPulses=[p];pulseTick(); drillPulses=[p];pulseTick();
return [25,45,65].filter(x=>mat[80*W+x-1]===RSTONE).length; return mat[${expectedY}*W+${blocked===3?25:24}]===RSTONE;
})()`); })()`);
check('the travelled ray path pushes interior grains one cell toward the bot',travelled>=2,`${travelled}/3 botward hops (probabilistic kicks)`); check(`original loose-grain nudge with ${blocked} blocked exits`,got);
}
// One artificially long ray crosses many exposed grains in a single tick, exercising the caps.
flatWorld(run,240,160,150); flatWorld(run,240,160,150);
const kicks = run(`(() => { check('a pulse leaves grains behind its advancing front alone',run(`(() => {
RS=12345; bot.x=0;bot.y=0;tickStamp=1; EV.length=0; bot.x=0;bot.y=0;tickStamp=1;RS=12345;
for(let x=20;x<120;x+=2) setCell(80*W+x,RSTONE,20,0); setCell(80*W+25,RSTONE,20,0);
// Neither a covered grain nor a grain already moved this tick may be kicked. drillPulses=[{ox:19.5,oy:80.5,ang:0,w:1,jit:0.5,n:1,damp:PULSE_W[1].damp,
occ[80*W+20]=1; part[80*W+22]=P_PIPE; stamp[80*W+24]=tickStamp; en:new Float32Array([1.4]),s:new Float32Array([60]),r:60,range:100,tune:0}];
// An open but owned destination and one stamped this tick are also off limits. pulseTick();return mat[80*W+25]===RSTONE;
part[80*W+25]=P_PIPE; occ[80*W+27]=1; stamp[80*W+29]=tickStamp; })()`));
const p={ox:19.5,oy:80.5,ang:0,w:1,jit:0.5,n:8,damp:PULSE_W[1].damp,
en:new Float32Array(8).fill(1.4),s:new Float32Array(8),r:200,range:100,tune:0};
// Zero spread gives eight overlapping rays, testing the per-tick dust cap as well.
const half=PULSE_W[1].half,mouth=PULSE_W[1].mouth; PULSE_W[1].half=0;PULSE_W[1].mouth=0;
drillPulses=[p]; pulseTick(); PULSE_W[1].half=half;PULSE_W[1].mouth=mouth;
let moved=0; for(let i=0;i<N;i++) if(mat[i]===RSTONE && stamp[i]===tickStamp) moved++;
return {moved:moved-1,dust:EV.filter(e=>e.t==='debris').length,
protected:mat[80*W+20]===RSTONE && mat[80*W+22]===RSTONE && mat[80*W+24]===RSTONE,
blocked:mat[80*W+25]===EMPTY && mat[80*W+27]===EMPTY && mat[80*W+29]===EMPTY};
})()`);
check('kicks are bounded to three per ray and dust to four puffs per tick',kicks.moved>0 && kicks.moved<=24 && kicks.dust>0 && kicks.dust<=4,JSON.stringify(kicks));
check('shaking respects occupied, owned and stamped cells',kicks.protected && kicks.blocked,JSON.stringify(kicks));
// Resonance ping (t50): finds natural veins within reach of the bit, under fog too, and // Resonance ping (t50): finds natural veins within reach of the bit, under fog too, and
// nothing else. // nothing else.
-13
View File
@@ -22,19 +22,6 @@ with launch(viewport={'width':960,'height':600}) as (_,pg,url):
}""") }""")
assert startup['warm']>=0 assert startup['warm']>=0
print('PASS both looks submit behind loading screen; first visible frame ms',startup) print('PASS both looks submit behind loading screen; first visible frame ms',startup)
dust=pg.evaluate("""()=>{
const particles=SET.particles,fogOn=SET.fog;SET.particles=true;SET.fog=false;
R.particles=[];EV.length=0;emit({t:'debris',x:bot.x,y:bot.y,m:RSTONE,dust:true});drainEvents();
const spawned=R.particles.length===1 && R.particles[0].dust===true;
R.ctx.clearRect(0,0,R.canvas.width,R.canvas.height);drawParticles();
const data=R.ctx.getImageData(0,0,R.canvas.width,R.canvas.height).data;
let visible=0,soft=0;for(let i=3;i<data.length;i+=4)if(data[i]){visible++;if(data[i]<100)soft++;}
for(let n=0;n<36;n++)drawParticles();const expired=R.particles.length===0;
SET.particles=false;spawnDebris(bot.x,bot.y,RSTONE,true);const disabled=R.particles.length===0;
SET.particles=particles;SET.fog=fogOn;return {spawned,visible,soft,expired,disabled};
}""")
assert dust['spawned'] and dust['visible']>4 and dust['soft']==dust['visible'] and dust['expired'] and dust['disabled'],dust
print('PASS pulse dust events draw a visible soft puff, expire and respect particles setting',dust)
pg.add_script_tag(path=str(root/'tools/perf-scenes.js')) pg.add_script_tag(path=str(root/'tools/perf-scenes.js'))
pg.evaluate("async()=>{perfScene('factory',1024,640);await startThreads();simPool.force=true;SET.look='pixel';SET.zoom=2;resizeRender();snapCamera();drawWorld();}") pg.evaluate("async()=>{perfScene('factory',1024,640);await startThreads();simPool.force=true;SET.look='pixel';SET.zoom=2;resizeRender();snapCamera();drawWorld();}")
result=pg.evaluate('''async () => { result=pg.evaluate('''async () => {