Drill: visible dust; pulses push loose material back and open a channel (t92)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -542,6 +542,12 @@ above, including:
|
||||
flow, pockets drain), with a little dust. All damping is counted per grain travelled
|
||||
(attenuation is per distance in real physics), retuned so rock bores feel the same as before.
|
||||
|
||||
- **Pulses stream loose grains back toward the bot (t92 follow-up, 2026-10-28).** Owner
|
||||
playtesting found that surface bouncing did not open a channel. Keep shaking the travelled
|
||||
ray path while the pulse is in flight, bias short hops back toward the bot, and keep sideways
|
||||
and upward escapes when blocked. Grains fall normally after each hop. Dust must show as a few
|
||||
soft puffs where grains move; the previous rubble-coloured debris squares were not visible.
|
||||
|
||||
## Ideas not built yet
|
||||
|
||||
- **Concrete**, as a stronger wall material from rubble.
|
||||
|
||||
+2
-2
@@ -327,7 +327,7 @@ Tool helpers: reach, brushes, and plain digging.
|
||||
- functions: setBuildRange, inBuildRange, inReach, forBrush, drillReach, inDrillReach, toolDig
|
||||
- variables: BUILD_MAX, buildBox
|
||||
|
||||
### src/sim/drill.js (187 lines)
|
||||
### src/sim/drill.js (202 lines)
|
||||
The resonance drill: tuned pulses that shatter one material.
|
||||
- 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
|
||||
@@ -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.
|
||||
- functions: cellRect, drawHazardRims, drawMachines, drawProcBlocks, drawSilo, drawSiloPixel, drawSiloWindow, drawRackBlocks
|
||||
|
||||
### src/render/overlays.js (370 lines)
|
||||
### src/render/overlays.js (384 lines)
|
||||
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
|
||||
- variables: PING_SPEED, PING_HOLD, PING_FADE, PING_BANDS, PING_GLINTS, GLINT_MS
|
||||
|
||||
@@ -4,6 +4,46 @@ 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
|
||||
open or not verified. Keep each entry short; the git history has the details.
|
||||
|
||||
|
||||
## 2026-10-28: t92 follow-up, visible dust and a botward channel through rubble (Codex)
|
||||
|
||||
- Traced shake events through `drainEvents` to `spawnDebris` and `drawParticles`. Events were
|
||||
handled above the world, but rendered only two opaque, rubble-coloured 0.7-grain squares.
|
||||
Added a dust flag to the existing debris event/particle pipeline: one pale radial puff,
|
||||
gently rising/expanding and fading over 36 frames. Retained particles setting and particle
|
||||
limit; at most four shake dust events per tick. Normal shatter debris/sparks unchanged.
|
||||
- Kicks previously visited only the advancing 2.2-grain wave front, with random sideways/up
|
||||
directions. Now each in-flight ray keeps shaking its travelled path, with local energy
|
||||
attenuated through powder/liquid; stops at solid. Tries the reverse ray first, then lift and
|
||||
side escapes. Retains seeded rnd(), one-grain move/enter hops, occupied/part/stamp guards,
|
||||
sealed corners and three moves/ray/tick; gravity settles grains normally. No damping retune.
|
||||
- Added steel-medium 35-grain-deep pile/rock regression and gravity-only control. At 480 ticks:
|
||||
33/35 centre-line cells empty versus 9/35; rubble centre x 72 -> 56.168 versus 70.68;
|
||||
all 875 rubble grains retained. The original pulse code gives only 10/35 empty cells and
|
||||
centre x 68.341, failing channel, transport, dust and travelled-path checks. An isolated
|
||||
advanced-front ray checks interior botward movement; existing movement/dust caps and guards,
|
||||
attenuation, repeatability and heap-fluidising tests retained. New browser check exercises
|
||||
event -> visible soft pixels, expiry and particles-off.
|
||||
- Stone depth/opened cells against original t92 baseline: narrow 22/181 -> 24/195,
|
||||
medium 13/186 -> 13/198, wide 6/134 -> 6/133; all within 10%, including depth.
|
||||
Small 480-tick pile benchmark median, five measured samples after warmup: 24.18 -> 33.88 ms
|
||||
(about 0.02 ms/tick extra). This is a small fixture, not a whole-game performance profile.
|
||||
- Full `python3 test/run_all.py --browser` exited 0, ALL PASSED: 32 headless suites and
|
||||
gameplay 361, smooth 28, console 18, caps 7, GPU 15, goldens 28, light 71, Firefox 2.
|
||||
Final pile fixture was moved into the real steel reach and its conservation assertion added
|
||||
after that run; the final `python3 test/run_all.py` passes all 32 suites, drill 66 checks.
|
||||
Inspected final `shots/t92b-channel.png` and `shots/t92b-channel-smooth.png`: lowered
|
||||
channel, basalt notch and soft dust over the streaming rubble. First recapture timed out
|
||||
starting the world; host-GPU recaptures passed. Caps confirmed NVIDIA Blackwell; no claim
|
||||
that the timeout was caused by unavailable hardware. Screenshot setup: `/tmp/t92b-channel.js`,
|
||||
`tools/shot.py --setup /tmp/t92b-channel.js --zoom 8 --no-light --freeze` (add smooth look).
|
||||
Evidence: `/tmp/t92b-red-final.log`, `/tmp/t92b-drill.log`, `/tmp/t92b-all-tests.log`,
|
||||
`/tmp/t92b-headless-final.log`. No page errors on final captures.
|
||||
- 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`, owner decision in `docs/DESIGN.md`,
|
||||
t92 tracking and this log. Both feedback items ticked; t92 moved to Review. No commit.
|
||||
Owner playtesting and performance in a large active world remain unverified.
|
||||
|
||||
## 2026-10-28: t92 drill pulses pass through rubble and fluidise it (Codex)
|
||||
|
||||
- Converted every pulse loss to a per-grain factor, applied with `factor ** step` outside
|
||||
|
||||
+16
-2
@@ -331,8 +331,13 @@ 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.restore();
|
||||
}
|
||||
function spawnDebris(x, y, m) {
|
||||
function spawnDebris(x, y, m, dust = false) {
|
||||
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] });
|
||||
}
|
||||
function spawnSpark(x, y) {
|
||||
@@ -358,11 +363,20 @@ function drawParticles() {
|
||||
p.x += dx / d * sp; p.y += dy / d * sp; p.life--;
|
||||
if (p.life <= 0) { ps.splice(k, 1); continue; }
|
||||
} else {
|
||||
p.x += p.vx; p.y += p.vy; p.vy += 0.04; p.life--;
|
||||
p.x += p.vx; p.y += p.vy; p.vy += p.dust ? 0 : 0.04; p.life--;
|
||||
if (p.life <= 0) { ps.splice(k, 1); continue; }
|
||||
}
|
||||
if (SET.fog && fogSeen(p.x, p.y) < 64) continue;
|
||||
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)})`;
|
||||
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);
|
||||
|
||||
+32
-17
@@ -115,7 +115,38 @@ function pulseTick(step = 0.5) {
|
||||
let e = p.en[k];
|
||||
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;
|
||||
let sd = p.s[k], kicks = 0;
|
||||
// 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];
|
||||
while (sd < lim && e >= 0.04) {
|
||||
sd += step;
|
||||
const x = Math.floor(ray.x + ux * sd), y = Math.floor(ray.y + uy * sd);
|
||||
@@ -130,22 +161,6 @@ function pulseTick(step = 0.5) {
|
||||
if (ph === PH_LIQUID) { e *= liquid; continue; }
|
||||
if (ph === PH_POWDER) {
|
||||
e *= loose;
|
||||
// At most three one-cell hops per ray/tick; gravity settles the shaken grains.
|
||||
if (kicks < 3 && stamp[i] !== tickStamp && !occ[i] && !part[i] && rnd() < 1.1 * e * step) {
|
||||
const side = rnd() < 0.5 ? -1 : 1;
|
||||
for (let hop = 0; hop < 4; hop++) {
|
||||
const mx = hop < 2 ? side : -side, my = -(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 - W]] === PH_SOLID) continue;
|
||||
move(i, j, x, y, nx, ny); kicks++;
|
||||
if (dust < 4) { emit({ t: 'debris', x, y, m }); dust++; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
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',
|
||||
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;
|
||||
} else if (e.t === 'debris') spawnDebris(e.x, e.y, e.m);
|
||||
} else if (e.t === 'debris') spawnDebris(e.x, e.y, e.m, e.dust);
|
||||
else if (e.t === 'suck') spawnSuck(e.x, e.y, e.m);
|
||||
else if (e.t === 'warn') spawnFloater(e.x, e.y, e.text, '#ff8a72');
|
||||
else if (e.t === 'sold') spawnFloater(e.x, e.y, '+' + fmtMoney(e.v));
|
||||
|
||||
@@ -206,6 +206,35 @@ check('115 grains of rubble prevent basalt breaking during sustained drilling',
|
||||
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`);
|
||||
|
||||
// A 35-grain-deep pile against basalt: compare with gravity alone, tracking only rubble.
|
||||
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) {
|
||||
flatWorld(run, 240, 160, 150);
|
||||
@@ -246,6 +275,18 @@ check('pulses fluidise a settled rubble pile so its height drops',
|
||||
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);
|
||||
const travelled=run(`(() => {
|
||||
RS=12345; bot.x=0;bot.y=0;tickStamp=1;EV.length=0;
|
||||
for(const x of [25,45,65]) setCell(80*W+x,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,
|
||||
en:new Float32Array([1.4]),s:new Float32Array([60]),r:60,range:100,tune:0};
|
||||
drillPulses=[p];pulseTick();
|
||||
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(`(() => {
|
||||
|
||||
@@ -22,6 +22,19 @@ with launch(viewport={'width':960,'height':600}) as (_,pg,url):
|
||||
}""")
|
||||
assert startup['warm']>=0
|
||||
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.evaluate("async()=>{perfScene('factory',1024,640);await startThreads();simPool.force=true;SET.look='pixel';SET.zoom=2;resizeRender();snapCamera();drawWorld();}")
|
||||
result=pg.evaluate('''async () => {
|
||||
|
||||
Reference in New Issue
Block a user