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:
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||||
|
|||||||
+25
-81
@@ -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);
|
flatWorld(run,240,160,150);
|
||||||
run(`RS=12345; tick=0; lastPulseTick=-99; drillPulses=[]; player.bitTier=3; bot.x=40; bot.y=88;
|
const got=run(`(() => {
|
||||||
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);
|
bot.x=0;bot.y=0;tickStamp=1;RS=12345;
|
||||||
for(let t=0;t<240;t++) simTick();`);
|
setCell(80*W+25,RSTONE,20,0);
|
||||||
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;})()`);
|
if (${blocked}>=1) setCell(80*W+24,STONE,20,F_NAT);
|
||||||
const before=height(), grains=count(run,run('RSTONE'));
|
if (${blocked}>=2) setCell(81*W+24,STONE,20,F_NAT);
|
||||||
run(`for(let t=0;t<480;t++) {${pulse?'toolPulse(90,94,1,0);':''}simTick();}`);
|
if (${blocked}>=3) setCell(79*W+24,STONE,20,F_NAT);
|
||||||
return {before,after:height(),grains,left:count(run,run('RSTONE'))};
|
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();
|
||||||
|
return mat[${expectedY}*W+${blocked===3?25:24}]===RSTONE;
|
||||||
|
})()`);
|
||||||
|
check(`original loose-grain nudge with ${blocked} blocked exits`,got);
|
||||||
}
|
}
|
||||||
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(`(() => {
|
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(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,
|
drillPulses=[{ox:19.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([60]),r:60,range:100,tune:0};
|
en:new Float32Array([1.4]),s:new Float32Array([60]),r:60,range:100,tune:0}];
|
||||||
drillPulses=[p];pulseTick();
|
pulseTick();return mat[80*W+25]===RSTONE;
|
||||||
return [25,45,65].filter(x=>mat[80*W+x-1]===RSTONE).length;
|
})()`));
|
||||||
})()`);
|
|
||||||
check('the travelled ray path pushes interior grains one cell toward the bot',travelled>=2,`${travelled}/3 botward hops (probabilistic kicks)`);
|
|
||||||
|
|
||||||
// One artificially long ray crosses many exposed grains in a single tick, exercising the caps.
|
|
||||||
flatWorld(run,240,160,150);
|
|
||||||
const kicks = run(`(() => {
|
|
||||||
RS=12345; bot.x=0;bot.y=0;tickStamp=1; EV.length=0;
|
|
||||||
for(let x=20;x<120;x+=2) setCell(80*W+x,RSTONE,20,0);
|
|
||||||
// Neither a covered grain nor a grain already moved this tick may be kicked.
|
|
||||||
occ[80*W+20]=1; part[80*W+22]=P_PIPE; stamp[80*W+24]=tickStamp;
|
|
||||||
// An open but owned destination and one stamped this tick are also off limits.
|
|
||||||
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.
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user