With anything selected a drag takes only the selection; with nothing selected it takes what you click. Right click blows the selected materials in the bag. Holding Ctrl lists every material found so far under "Not in bag". Selections stay when materials run out; Clear button, clicking the only selected row, or gamepad B clears. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
7.1 KiB
JavaScript
100 lines
7.1 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);
|
|
done();
|