// 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)); // Factory launches must not consume the nozzle or weather allowances. flatWorld(run, 640, 512, 480); run(`bag[SAND] = 30000; bagTotal = 30000; placeBlock(BP_BELT, 1, 20, 20, true); for (let k = 0; k < 10000; k++) { const i = 79 * W + 83; setCell(i, SAND, 20, 0); tickStamp++; carryColumn(i, 1, null); }`); check('a belt chute launches ten thousand grains without a shared cap', run('flyers.length === 10000 && mat[79*W+83] === EMPTY')); check('busy chutes do not pause the nozzle', run('toolBlow(330, 400, 6, SAND).placed === 25')); check('busy chutes and nozzle do not pause rain', run('spawnRain(0, W, 10) === 10')); run(`flyers.length = 0; for (let k = 0; k < 3001; k++) toolBlow(330, 400, 0, SAND);`); check('a long hold continues past the old shared limit', run('toolBlow(330, 400, 0, SAND).placed === 1')); run(`flyers.length = 0; for (let k = 0; k < 3999; k++) toolBlow(330, 400, 0, SAND);`); check('wide nozzle stops exactly at its own raised allowance', run('toolBlow(330, 400, 6, SAND).placed === 1 && flyers.length === 4000')); check('nozzle limit explains the pause and keeps the bag intact', run(`(() => { const before = bag[SAND], r = toolBlow(330, 400, 6, SAND); return r.placed === 0 && r.status === 'Too much in the air.' && bag[SAND] === before; })()`)); check('nozzle allowance does not stop rain', run('spawnRain(0, W, 2501) === 2500')); check('rain stops at its own allowance', run('spawnRain(0, W, 1) === 0')); run('flyers.splice(0, 1)'); check('nozzle resumes as soon as one own grain leaves flight', run('toolBlow(330, 400, 6, SAND).placed === 1')); run('setCell(100*W+100, SAND, 20, 0); launchGrain(100*W+100, 100, 40, 0, 0)'); check('production continues with both allowances full', run('flyers.length === 6501 && !flyers[6500].bag && !flyers[6500].rain')); run(`flyers.length=0; spawnRain(0, W, 2500);`); check('full rain allowance does not block the nozzle', run('toolBlow(330, 400, 6, SAND).placed === 25')); run('flyers.length=0; spawnRain(0,W,2500); for(const f of flyers) f.age=900; flyTick();'); check('rain can resume after its grains land', run('spawnRain(0,W,1) === 1')); run('flyers.length=0; bag[SAND]=10000; bagTotal=10000;'); run(`for(let k=0;k<3999;k++) toolBlow(330,400,0,SAND); toolVacuum(330,450,100,()=>true);`); check('vacuum removal also frees nozzle allowance', run('toolBlow(330,400,6,SAND).placed > 1')); done();