diff --git a/src/sim/vacuum.js b/src/sim/vacuum.js index 1c472ce..891558d 100644 --- a/src/sim/vacuum.js +++ b/src/sim/vacuum.js @@ -2,11 +2,19 @@ // Material the vacuum would start on at the cursor: a bar, else the nearest loose grain. function vacuumTargetAt(cx, cy, r) { for (const e of entsInCircle(cx, cy, Math.max(1, r))) return e.m; + r = Math.max(1, r); let best = 0, bd = 1e9; forBrush(cx, cy, Math.max(1, r), (x, y, i) => { const m = mat[i], ph = PH[m]; if (ph === PH_POWDER || (ph === PH_LIQUID && player.flask)) { const d = (x - cx) * (x - cx) + (y - cy) * (y - cy); if (d < bd) { bd = d; best = m; } } }); + for (const f of flyers) { + if (f.rain) continue; // Rain belongs to the clouds; collecting it would create water. + const ph = PH[f.m]; + if (!(ph === PH_POWDER || (ph === PH_LIQUID && player.flask))) continue; + const x = Math.floor(f.x), y = Math.floor(f.y), dx = x - cx, dy = y - cy, d = dx * dx + dy * dy; + if (d <= (r + 0.5) * (r + 0.5) && d < bd && inReach(x, y)) { bd = d; best = f.m; } + } return best; } // Vacuum: picks up loose grains, liquids (with the Flask) and bars for which pick(m) is true. @@ -18,7 +26,19 @@ function toolVacuum(cx, cy, r, pick) { bagAdd(e.m, 1); removeEnt(e); got++; if (!first) first = e.m; emit({ t: 'suck', x: e.ix + 2, y: e.iy + 1, m: e.m }); } - if (!full) forBrush(cx, cy, r, (x, y, i) => { + if (!full) for (let k = flyers.length - 1; k >= 0; k--) { + const f = flyers[k], m = f.m, ph = PH[m]; + if (f.rain) continue; // Rain belongs to the clouds; collecting it would create water. + if (!(ph === PH_POWDER || (ph === PH_LIQUID && player.flask)) || !pick(m)) continue; + const x = Math.floor(f.x), y = Math.floor(f.y), dx = x - cx, dy = y - cy; + if (dx * dx + dy * dy > (r + 0.5) * (r + 0.5) || !inReach(x, y)) continue; + if (bagTotal >= bagCap()) { full = true; break; } + if (!first) first = m; + bagAdd(m, 1); flyers[k] = flyers[flyers.length - 1]; flyers.pop(); got++; + if (got & 1) emit({ t: 'suck', x, y, m }); + if (--budget <= 0) break; + } + if (!full && budget > 0) forBrush(cx, cy, r, (x, y, i) => { const m = mat[i], ph = PH[m]; if (!(ph === PH_POWDER || (ph === PH_LIQUID && player.flask))) return; if (!pick(m)) return; diff --git a/test/t_basics.js b/test/t_basics.js index 31589b8..4a35b75 100644 --- a/test/t_basics.js +++ b/test/t_basics.js @@ -96,4 +96,56 @@ const before = run('JSON.stringify({bag: bagTotal, ents: ents.length, molds: mol run('globalThis.__sv = serializeWorld(); deserializeWorld(__sv.meta, __sv.buffers);'); const after = run('JSON.stringify({bag: bagTotal, ents: ents.length, molds: molds.length, mach: machines.filter(m => m && m.type !== "dock").length, blocks: blockP.reduce((a,b)=>a+(b>0),0)})'); check('save/load keeps bag, bars, molds, machines and blocks', before === after, before + ' vs ' + after); +// 12. vacuum catches grains in flight, before loose cells, with the same limits. +flatWorld(run, 240, 160, 140); +run('bot.x = 98; bot.y = 58; player.flask = false; EV.length = 0;'); +run('flyers.push({x: 102.9, y: 62.9, m: SAND});'); +const airborne = run('toolVacuum(100, 60, 3, () => true)'); +check('vacuum takes airborne sand using its current cell', airborne.got === 1 && airborne.m === run('SAND') && run('bag[SAND] === 1 && flyers.length === 0')); +check('airborne pickup emits a suck at the grain cell', run('EV.some(e => e.t === "suck" && e.x === 102 && e.y === 62 && e.m === SAND)')); +run('flyers.push({x: 103, y: 63, m: SAND}, {x: 180, y: 60, m: SAND});'); +check('vacuum leaves flyers outside the circular brush', run('toolVacuum(100, 60, 3, () => true).got === 0 && flyers.length === 2')); +check('vacuum leaves flyers out of reach', run('toolVacuum(180, 60, 3, () => true).got === 0 && flyers.length === 2 && vacuumTargetAt(180, 60, 3) === 0')); +run('flyers = [{x: 100, y: 60, m: WATER}];'); +check('liquid flyers need the flask for pickup and targeting', run('toolVacuum(100, 60, 3, () => true).got === 0 && vacuumTargetAt(100, 60, 3) === 0 && flyers.length === 1')); +run('player.flask = true;'); +check('flask allows liquid flyer targeting', run('vacuumTargetAt(100, 60, 3) === WATER')); +check('flask allows liquid flyer pickup', run('toolVacuum(100, 60, 3, () => true).got === 1 && bag[WATER] === 1 && flyers.length === 0')); +run('flyers = [{x: 100, y: 60, m: WATER, rain: true}];'); +const moisture = run('weather.moist.reduce((a, b) => a + b, 0)'); +check('rain is neither targeted nor collected', run('vacuumTargetAt(100, 60, 3) === 0 && toolVacuum(100, 60, 3, () => true).got === 0 && flyers.length === 1 && bag[WATER] === 1') && moisture === run('weather.moist.reduce((a, b) => a + b, 0)')); +run('flyers = [{x: 100, y: 60, m: COAL}, {x: 101, y: 60, m: SAND, bag: true}];'); +check('pickup filter also applies to bag-blown flyers', run('toolVacuum(100, 60, 3, m => m === SAND).got === 1 && flyers.length === 1 && flyers[0].m === COAL && bag[COAL] === 0')); +check('targeting finds a material only in flight', run('vacuumTargetAt(100, 60, 3) === COAL')); +run('setCell(60*W+102, SAND, 20, 0);'); +check('nearer flyer wins over a loose cell', run('vacuumTargetAt(100, 60, 3) === COAL')); +run('flyers[0].x = 103;'); +check('nearer loose cell wins over a flyer', run('vacuumTargetAt(100, 60, 3) === SAND')); +run('spawnBar(BARCU, 100, 60, 0, 0);'); +check('bars still take targeting priority', run('vacuumTargetAt(100, 60, 3) === BARCU')); +run('ents = []; bag.fill(0); bag[SAND] = bagTotal = bagCap();'); +const fullPickup = run('toolVacuum(100, 60, 3, () => true)'); +check('full bag leaves both flyers and cells untouched', fullPickup.full && fullPickup.got === 0 && run('flyers.length === 1 && mat[60*W+102] === SAND && bagTotal === bagCap()')); +run('bagTake(SAND, 1);'); +const lastRoom = run('toolVacuum(100, 60, 3, () => true)'); +check('last bag space goes to the flyer before the cell', lastRoom.full && lastRoom.got === 1 && lastRoom.m === run('COAL') && run('flyers.length === 0 && mat[60*W+102] === SAND && bagTotal === bagCap()')); +run('bag.fill(0); bagTotal = 0; EV.length = 0; for (let k = 0; k < 50; k++) flyers.push({x: 100, y: 60, m: COAL});'); +check('flyers share the 48-grain budget and leave cells for later', run('toolVacuum(100, 60, 3, () => true).got === 48 && flyers.length === 2 && bag[COAL] === 48 && mat[60*W+102] === SAND')); +check('flyers emit suck on every other collected grain', run('EV.filter(e => e.t === "suck").length === 24')); +check('remaining budget collects both flyers and cells', run('toolVacuum(100, 60, 3, () => true).got === 3 && flyers.length === 0 && bag[COAL] === 50 && bag[SAND] === 1')); +// A real screw launches sand; hold the brush below its airborne arc, away from its mouth. +flatWorld(run, 240, 160, 140); +run('RS = 7; nextPowerBuild = 0; bot.x = 106; bot.y = 70; placeBlock(BP_SCREW, 1, 24, 15, true);'); +rect(run, 96, 60, 99, 63, run('SAND')); +const screwCatch = run(`(() => { + let airborne = 0, caught = 0; + for (let k = 0; k < 120; k++) { + simTick(); + const n = flyers.length; + const result = toolVacuum(106, 70, 3, m => m === SAND); + airborne += n - flyers.length; caught += result.got; + } + return {airborne, caught, bag: bag[SAND]}; +})()`); +check('vacuum held under a screw arc catches its launched sand', screwCatch.airborne === 16 && screwCatch.caught === 16 && screwCatch.bag === 16, JSON.stringify(screwCatch)); done();