Files

161 lines
12 KiB
JavaScript

// Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load.
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
flatWorld(run, 240, 160, 100);
run('player.tankTier = 3; player.flask = false;');
// 1. drill breaks stone loose; nothing goes into the bag
run('bot.x = 116; bot.y = 92; for (let k=0;k<80;k++) toolDig(120, 104, 3)');
check('drill leaves bag empty', run('bagTotal') === 0, run('bagTotal'));
const rub = count(run, run('RSTONE'));
check('drill turns stone into loose rubble', rub > 20, rub);
ticks(run, 60);
// 2. vacuum with a filter: only rubble
rect(run, 100, 96, 104, 99, run('COAL'));
ticks(run, 30);
const pickup = run('toolVacuum(120, 104, 6, m => m === RSTONE)');
const got = pickup.got;
check('vacuum reports the first material picked up', pickup.m === run('RSTONE'));
check('vacuum picks only the filtered material', run('bag[RSTONE]') > 0 && run('bag[COAL]') === 0, `${run('bag[RSTONE]')} rubble, ${run('bag[COAL]')} coal`);
run('toolVacuum(102, 98, 4, m => true)');
check('empty vacuum reports no first material', run('toolVacuum(10, 10, 1, () => true).m') === 0);
check('unfiltered vacuum picks coal', run('bag[COAL]') > 0, run('bag[COAL]'));
// 3. drop: grains fall
const c0 = run('bag[COAL]');
run('bot.x = 100; bot.y = 60;');
run('for (let k=0;k<5;k++) { toolBlow(80, 70, 1, COAL); simTick(); }');
check('blown coal flies out of the bot', run('flyers.length') > 0, run('flyers.length'));
ticks(run, 120);
check('blown coal leaves the bag and lands on the ground', run('bag[COAL]') < c0 && run('flyers.length') === 0 && count(run, run('COAL'), 60, 80, 100, 99) > 0, `${c0}->${run('bag[COAL]')} landed ${count(run, run('COAL'), 60, 80, 100, 99)}`);
check('no selection explains how to select', run('toolBlow(80, 70, 1, 0).status') === 'Nothing selected: click or Q/E in the bag.');
check('unavailable blow mix explains none are in the bag', run('toolBlow(80, 70, 1, COAL, [CUORE, BARCU]).status') === 'None of the selected materials are in the bag.');
// bot flies through built walls but not through natural rock
run('bag[RSTONE] += 64; bagTotal += 64; placeBlocks(BP_WALL, RSTONE, blockRect(30, 14, 30, 17))');
check('bot can overlap a built wall', !run('botOverlapSolid(121, 58)'));
check('bot cannot overlap natural rock', run('botOverlapSolid(120, 98)'));
// trees: water passes in front of a background trunk; cutting the base drops the tree
run('for (let y = 80; y < 100; y++) for (let x = 140; x < 142; x++) bgm[y*W+x] = WOOD; for (let y = 72; y < 80; y++) for (let x = 136; x < 146; x++) bgm[y*W+x] = LEAVES;');
rect(run, 130, 96, 138, 99, run('WATER'));
ticks(run, 120);
check('water flows past a tree trunk', count(run, run('WATER'), 141, 90, 160, 99) > 0, count(run, run('WATER'), 141, 90, 160, 99));
run('bot.x = 134; bot.y = 88; for (let k=0;k<120;k++) toolDig(140, 99, 1)');
ticks(run, 5);
check('cutting the trunk base brings the tree down as wood chunks', run('bgm[85*W+140]') === 0 && count(run, run('LWOOD')) > 20, count(run, run('LWOOD')));
// 4. wall blocks cost rubble and snap to the grid
run('bag[RSTONE] = 200; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
const r1 = run("JSON.stringify(placeBlocks(BP_WALL, RSTONE, blockLine(25, 20, 29, 20)))");
check('stone wall line placed', JSON.parse(r1).n === 5 && run('bag[RSTONE]') === 200 - 80, r1 + ' bag=' + run('bag[RSTONE]'));
check('wall cells are solid stone at grid positions', run('mat[80*W+100]') === run('STONE') && run('mat[83*W+119]') === run('STONE'));
const rm = run("removePieceAt(101, 81)");
check('taking a wall block back refunds its rubble', run('bag[RSTONE]') === 200 - 64, rm + ' ' + run('bag[RSTONE]'));
// 5. ingot mold: pour molten copper, bars drop below
run('bag[RSTONE] = 200; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
const mo = run("placeMoldAt('ingot', 32, 22, RSTONE)");
check('ingot mold placed', mo === '' && run('molds.length') === 1, mo);
run('for (const cav of molds[0].cavs) for (const i of cav) setCell(i, MCU, 1160, 0)');
ticks(run, 900);
check('ingot mold sets and drops three bars', run('ents.length === 3 && molds[0].state === "short"'), run('molds[0].state'));
check('bars spawn below the mold', run('ents.every(e => e.m === BARCU && e.iy >= molds[0].y + molds[0].h)'));
ticks(run, 200);
check('bars fall and come to rest', run('ents.every(e => Math.abs(e.vy) < 0.01)'), run('JSON.stringify(ents.map(e=>[e.ix,e.iy]))'));
check('bars count as built-up copper', run('stats.bars') === 3);
// 6. vacuum bars
const bx = run('ents[0].ix + 2'), by = run('ents[0].iy + 1');
run(`toolVacuum(${bx}, ${by}, 4, m => m === BARCU)`);
check('vacuum picks up bars', run('bag[BARCU]') >= 1 && run('ents.length') < 3, run('bag[BARCU]'));
// 7. belt carries a bar: belt from block (40,24) to (46,24), powered by direct supply
run('bot.x = 165; bot.y = 70; bag[BARCU] = 20; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
const rb = run("JSON.stringify(placeBlocks(BP_BELT, 1, blockLine(40, 24, 46, 24)))");
check('belt blocks placed for $12 each', JSON.parse(rb).n === 7 && run('money') === 250 - 84, rb + ' money ' + run('money'));
run('rebuildPower(); nets[netId[96*W+161]].prods.push({out: 50});');
run('spawnBar(BARCU, 162, 90, 0, 0)');
ticks(run, 20);
const x0 = run('ents[ents.length-1].ix');
ticks(run, 60);
const x1 = run('ents[ents.length-1].ix');
check('powered belt moves the bar right', x1 > x0 + 10, `${x0} -> ${x1}`);
// 8. bar melts when heated
run('const e = ents[ents.length-1]; for (let dy=-2; dy<4; dy++) for (let dx=-2; dx<6; dx++) temp[(e.iy+dy)*W + e.ix+dx] = 1300; e.T = 1100;');
ticks(run, 3);
check('hot bar melts into molten copper', count(run, run('MCU')) > 0, count(run, run('MCU')));
// 9. machines snap to blocks
run('bag[LWOOD] = 100; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
check('water wheel placed on block grid', run("placeMachineAt('wheel', 0, 10, 10)") === '' && run('machines[1].x') === 40);
// 10. blueprint copy + ghost fill
run('bot.x = 100; bot.y = 60;');
run('bag[RSTONE] = 400; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
run("placeBlocks(BP_WALL, RSTONE, blockRect(20, 18, 22, 19))");
const bp = run("JSON.stringify(copyRegion(80, 72, 91, 79))");
check('blueprint captures 6 wall blocks', JSON.parse(bp).blocks.length === 6, bp.slice(0, 80));
run(`pasteGhost(${bp}, 34, 14)`);
ticks(run, 20);
check('ghosts are built from the bag', run('ghostBlocks.size') === 0 && run('blockP[14*BW+34]') === run('BP_WALL'), run('ghostBlocks.size'));
// 11. save / load round trip
const before = 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)})');
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));
// Rock in the bag: solids go in as what drilling them gives, and older saves holding solids load
// that way too, so the bag never holds something its list can't show.
flatWorld(run, 120, 80, 60);
check('putting rock in the bag stores its rubble', run('(() => { bag.fill(0); bagTotal = 0; bagAdd(BASALT, 10, true); bagAdd(CLAY, 5, true); return bag[BASALT] === 0 && bag[RBAS] === 10 && bag[CLAY] === 0 && bag[LCLAY] === 5 && bagTotal === 15; })()'));
check('a save holding solid rock loads it as rubble with the same total', run(`(() => {
bag.fill(0); bagTotal = 0; const s = serializeWorld(); s.meta.bag[BASALT] = 1000; s.meta.bag[CLAY] = 200; s.meta.bag[MSLAG] = 7;
deserializeWorld(s.meta, s.buffers);
return bag[BASALT] === 0 && bag[RBAS] === 1000 && bag[CLAY] === 0 && bag[LCLAY] === 200 && bag[MSLAG] === 7 && bagTotal === 1207;
})()`));
done();