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:
+3
-2
@@ -297,7 +297,7 @@ Trees (background layer), acorns, saplings, and waking freezing water.
|
|||||||
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
|
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
|
||||||
- variables: treeBases, saplings, treeDraw
|
- variables: treeBases, saplings, treeDraw
|
||||||
|
|
||||||
### src/sim/vacuum.js (172 lines)
|
### src/sim/vacuum.js (176 lines)
|
||||||
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
|
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
|
||||||
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
|
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
|
||||||
- variables: blowRR, FLY_G, FLY_Q
|
- variables: blowRR, FLY_G, FLY_Q
|
||||||
@@ -467,7 +467,7 @@ Gamepad input.
|
|||||||
- functions: padPressed, pollPad
|
- functions: padPressed, pollPad
|
||||||
- variables: PAD
|
- variables: PAD
|
||||||
|
|
||||||
### src/ui/previews.js (190 lines)
|
### src/ui/previews.js (192 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
|
- functions: blockRectPx, drawGrid, previewColor, drawAirStreaks, drawReachCircle, drawPreviews
|
||||||
|
|
||||||
@@ -511,6 +511,7 @@ The frame loop and boot.
|
|||||||
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
||||||
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
||||||
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
||||||
|
- `test/t_nozzle.js`: Blowing aims within the nozzle's reach, with room for the brush and settling.
|
||||||
- `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.
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ 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-25: t34: the nozzle blows no further than it sucks (Claude Code, Codex)
|
||||||
|
|
||||||
|
- `toolBlow()` (`src/sim/vacuum.js`) aims at the capped point from `nozzle()` when the cursor is
|
||||||
|
out of reach (thrown bars already did); `drawPreviews()` draws the blow brush there, in the blow
|
||||||
|
colour rather than the out-of-reach red.
|
||||||
|
- `test/t_nozzle.js`: aiming at 3x and 5x the reach lands sand within reach + 16 grains (brush and
|
||||||
|
settling, measured 15); inside reach it still lands around the cursor. play.py: the preview
|
||||||
|
sits at the capped point. `run_all.py --browser` ALL PASSED. Screenshot
|
||||||
|
`shots/t34-blow-out-of-reach.png`.
|
||||||
|
|
||||||
## 2026-10-25: t21: a loaded world starts around the player first (Claude Code, Codex)
|
## 2026-10-25: t21: a loaded world starts around the player first (Claude Code, Codex)
|
||||||
|
|
||||||
- Owner: "lets speed up the first load speed. load in areas around the player first".
|
- Owner: "lets speed up the first load speed. load in areas around the player first".
|
||||||
|
|||||||
+6
-2
@@ -31,7 +31,7 @@ function toolVacuum(cx, cy, r, pick) {
|
|||||||
});
|
});
|
||||||
return { got, m: first, full, status: full ? 'Bag full; blow material out or cast a bigger tank.' : '' };
|
return { got, m: first, full, status: full ? 'Bag full; blow material out or cast a bigger tank.' : '' };
|
||||||
}
|
}
|
||||||
// Blow: grains leave the bot's nozzle and fly to the cursor, where they drop.
|
// Blow: grains leave the bot's nozzle and arc toward the cursor, within reach.
|
||||||
// Liquids need the Flask. Bars are thrown, one at a time.
|
// Liquids need the Flask. Bars are thrown, one at a time.
|
||||||
function nozzle(tx, ty) {
|
function nozzle(tx, ty) {
|
||||||
const cx = bot.x + bot.w / 2, cy = bot.y + bot.h / 2;
|
const cx = bot.x + bot.w / 2, cy = bot.y + bot.h / 2;
|
||||||
@@ -65,6 +65,10 @@ function toolBlow(cx, cy, r, m, mix) {
|
|||||||
if (!e) return { placed: 0, status: 'No room to throw a bar from here.' };
|
if (!e) return { placed: 0, status: 'No room to throw a bar from here.' };
|
||||||
bagTake(m, 1); return { placed: 1, status: '' };
|
bagTake(m, 1); return { placed: 1, status: '' };
|
||||||
}
|
}
|
||||||
|
if (!inReach(cx, cy)) {
|
||||||
|
cx = nz.ox + nz.dx * nz.dist - 0.5;
|
||||||
|
cy = nz.oy + nz.dy * nz.dist - 0.5;
|
||||||
|
}
|
||||||
if (flyers.length > 3000) return { placed: 0, status: '' };
|
if (flyers.length > 3000) return { placed: 0, status: '' };
|
||||||
// nozzle 0 is a trickle for careful work, the widest a torrent
|
// nozzle 0 is a trickle for careful work, the widest a torrent
|
||||||
const rate = 1 + r + ((r * r) >> 1);
|
const rate = 1 + r + ((r * r) >> 1);
|
||||||
@@ -73,7 +77,7 @@ function toolBlow(cx, cy, r, m, mix) {
|
|||||||
for (let k = 0; k < n; k++) {
|
for (let k = 0; k < n; k++) {
|
||||||
const gm = pickM ? pickM() : m;
|
const gm = pickM ? pickM() : m;
|
||||||
if (!gm) break;
|
if (!gm) break;
|
||||||
// aim each grain at a spot in the brush around the cursor, on an arc that passes
|
// aim each grain at a spot in the brush around the target, on an arc that passes
|
||||||
// through it; past that point gravity carries it on until it hits something
|
// through it; past that point gravity carries it on until it hits something
|
||||||
const a = rnd() * Math.PI * 2, rr = Math.sqrt(rnd()) * (r + 0.5);
|
const a = rnd() * Math.PI * 2, rr = Math.sqrt(rnd()) * (r + 0.5);
|
||||||
const tx = cx + 0.5 + Math.cos(a) * rr, ty = cy + 0.5 + Math.sin(a) * rr;
|
const tx = cx + 0.5 + Math.cos(a) * rr, ty = cy + 0.5 + Math.sin(a) * rr;
|
||||||
|
|||||||
+3
-1
@@ -80,7 +80,9 @@ function drawPreviews() {
|
|||||||
const r = t === 'ignite' ? 1 : GAME.brush[GAME.tool];
|
const r = t === 'ignite' ? 1 : GAME.brush[GAME.tool];
|
||||||
const reachOk = inReach(ax, ay);
|
const reachOk = inReach(ax, ay);
|
||||||
const blowing = t === 'vacuum' && GAME.rmb && !GAME.rmbConsumed;
|
const blowing = t === 'vacuum' && GAME.rmb && !GAME.rmbConsumed;
|
||||||
ctx.strokeStyle = !reachOk ? 'rgba(239,106,85,0.8)' : t === 'drill' ? 'rgba(239,230,216,0.8)' : blowing ? 'rgba(255,210,122,0.9)' : t === 'vacuum' ? 'rgba(92,186,166,0.95)' : 'rgba(255,154,60,0.9)';
|
const nz = blowing && !reachOk ? nozzle(ax + 0.5, ay + 0.5) : null;
|
||||||
|
const p = nz ? worldToScreenPx(nz.ox + nz.dx * nz.dist - 0.5, nz.oy + nz.dy * nz.dist - 0.5) : worldToScreenPx(ax, ay);
|
||||||
|
ctx.strokeStyle = !reachOk && !nz ? 'rgba(239,106,85,0.8)' : t === 'drill' ? 'rgba(239,230,216,0.8)' : blowing ? 'rgba(255,210,122,0.9)' : t === 'vacuum' ? 'rgba(92,186,166,0.95)' : 'rgba(255,154,60,0.9)';
|
||||||
ctx.lineWidth = Math.max(1, zp * 0.25);
|
ctx.lineWidth = Math.max(1, zp * 0.25);
|
||||||
if (t === 'vacuum') ctx.setLineDash([zp, zp * 0.6]);
|
if (t === 'vacuum') ctx.setLineDash([zp, zp * 0.6]);
|
||||||
ctx.beginPath(); ctx.arc(p.x + zp / 2, p.y + zp / 2, (r + 0.5) * zp, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
ctx.beginPath(); ctx.arc(p.x + zp / 2, p.y + zp / 2, (r + 0.5) * zp, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);
|
||||||
|
|||||||
+17
-1
@@ -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')
|
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)')
|
ok = select('2', 'GAME.blow.size === 1 && GAME.blow.has(COAL)')
|
||||||
check('Q/E chooses coal to blow', ok, ev('[...GAME.blow]'))
|
check('Q/E chooses coal to blow', ok, ev('[...GAME.blow]'))
|
||||||
|
|||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user