The nozzle blows no further than it sucks (t34)

With the cursor outside the reach circle, blown grains aim at the circle's edge in the cursor's
direction instead of flying all the way to the cursor, and the blow preview stops there too.
New test/t_nozzle.js and a play.py preview check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 16:35:38 -04:00
co-authored by Claude Opus 5.5
parent 64ff8310f3
commit 5ce44f842e
6 changed files with 71 additions and 6 deletions
+17 -1
View File
@@ -434,7 +434,23 @@ with sync_playwright() as p:
}
})()"""))
# 3. right button blows the selection; exhausted materials stay selected
check('out-of-reach blow preview sits at the capped nozzle target', ev("""(() => {
const aim = GAME.aim, consumed = GAME.rmbConsumed, arc = R.ctx.arc;
const arcs = [];
try {
GAME.aim = { x: bot.x + bot.w / 2 + 3 * REACH, y: bot.y + bot.h / 2 };
GAME.rmb = true; GAME.rmbConsumed = false;
R.ctx.arc = function (x, y, r, ...rest) { arcs.push({ x, y, r }); return arc.call(this, x, y, r, ...rest); };
drawPreviews();
const nz = nozzle(Math.floor(GAME.aim.x) + 0.5, Math.floor(GAME.aim.y) + 0.5);
const target = worldToScreenPx(nz.ox + nz.dx * nz.dist, nz.oy + nz.dy * nz.dist);
return arcs.some(p => Math.abs(p.x - target.x) < 0.01 && Math.abs(p.y - target.y) < 0.01 && Math.abs(p.r - (GAME.brush[1] + 0.5) * R.zp) < 0.01);
} finally {
R.ctx.arc = arc; GAME.aim = aim; GAME.rmb = false; GAME.rmbConsumed = consumed;
}
})()"""))
# 3. right button blows the selection; exhausted materials are deselected
refill('bag[COAL] = 120; bag[RSTONE] = 600; bag[CUORE] = 60; bag[LCLAY] = 200; bag[LWOOD] = 120')
ok = select('2', 'GAME.blow.size === 1 && GAME.blow.has(COAL)')
check('Q/E chooses coal to blow', ok, ev('[...GAME.blow]'))
+32
View File
@@ -0,0 +1,32 @@
// Blowing aims within the nozzle's reach, with room for the brush and settling.
const { load, suite, flatWorld, ticks, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
function blowSand(distance, radius) {
flatWorld(run, 640, 160, 100);
run('RS = 12345; bot.x = 300 - bot.w / 2; bot.y = 80 - bot.h / 2; bag[SAND] = 40; bagTotal = 40;');
run(`for (let k = 0; k < 40 && bag[SAND]; k++) { toolBlow(300 + ${distance}, 99, ${radius}, SAND); simTick(); }`);
ticks(run, 400);
check('all blown sand lands and stays out of the bag', count(run, run('SAND')) === 40 && run('flyers.length === 0 && bag[SAND] === 0'));
return run(`(() => {
let farthest = 0, cursorDistance = 0;
for (let i = 0; i < N; i++) if (mat[i] === SAND) {
const x = i % W + 0.5, y = Math.floor(i / W) + 0.5;
farthest = Math.max(farthest, Math.hypot(x - 300, y - 80));
cursorDistance = Math.max(cursorDistance, Math.hypot(x - (300 + ${distance} + 0.5), y - 99.5));
}
return { farthest, cursorDistance };
})()`);
}
// Widest brush, 40 grains on flat ground: measured 14.95 grains past REACH.
const BRUSH_AND_SETTLING_MARGIN = 16;
for (const distance of [run('3 * REACH'), run('5 * REACH')]) {
const landing = blowSand(distance, 6);
check('aiming far away lands sand within reach plus brush and settling', landing.farthest <= run('REACH') + BRUSH_AND_SETTLING_MARGIN, landing.farthest.toFixed(2));
}
const nearby = blowSand(30, 0);
check('aiming inside reach still lands sand around the cursor', nearby.cursorDistance <= BRUSH_AND_SETTLING_MARGIN, nearby.cursorDistance.toFixed(2));
done();