152 lines
12 KiB
JavaScript
152 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));
|
|
done();
|