Files
deepgrain-foundry/test/t_basics.js
T
44r0n7andClaude Opus 5.5 cd483dd546 Vacuum: one selection for pick-up and blow, full list while Ctrl is held (t20)
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>
2026-10-24 13:57:43 -04:00

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();