SOURCE / PINNED RELEASE
Made of little things.
GRAV / RIFT
- Release
- 288aa261ce07…
- Author-recorded commit
- c549b5a4e17e…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-8a7361981e8
Archive hash verified: e52a01aff70810b3…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import assert from 'node:assert/strict';
import test from 'node:test';
import {readFileSync} from 'node:fs';
import {runInNewContext} from 'node:vm';
import ts from 'typescript';
const modules={};
function load(name){
if(modules[name])return modules[name];
const source=readFileSync(new URL(`../src/game/${name}.ts`,import.meta.url),'utf8');
const code=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
const module={exports:{}};runInNewContext(code,{module,exports:module.exports,Math,require:p=>load(p.replace('./','').replace('.js',''))});return modules[name]=module.exports;
}
const physics=load('physics'),{Game}=load('game'),{WORLD,TUG}=load('world');
const square=[{x:-14,y:-14},{x:14,y:-14},{x:14,y:14},{x:-14,y:14}];
const sprites=Array.from({length:17},()=>({hull:square,width:64,height:64}));
const advance=(g,seconds)=>{for(let i=0;i<seconds*120;i++)g.step(1/120);};
test('alpha-derived hull encloses opaque pixel boundaries and ignores transparency',()=>{
const pixels=new Uint8Array(4*4*4);pixels[(1*4+1)*4+3]=255;pixels[(1*4+2)*4+3]=255;
const hull=physics.hullFromPixels(pixels,4,4,2);
assert.equal(hull.length,4);assert.equal(Math.min(...hull.map(p=>p.x)),-2);assert.equal(Math.max(...hull.map(p=>p.x)),2);assert.equal(Math.max(...hull.map(p=>p.y)),0);
});
test('convex SAT excludes separated shapes and transfers collision momentum',()=>{
const a=physics.body('scrap',0,square,0,0),b=physics.body('scrap',0,square,26,0);
a.vx=150;const hit=physics.contact(a,b);assert.ok(hit);const impact=physics.resolve(a,b,hit);assert.ok(impact>140);assert.ok(b.vx>90);assert.ok(a.vx<60);
b.x=100;assert.equal(physics.contact(a,b),null);
});
test('off-center collision produces angular response without nonfinite values',()=>{
const a=physics.body('scrap',0,square,0,0),b=physics.body('scrap',0,square,25,17);a.vx=250;
physics.resolve(a,b,physics.contact(a,b));assert.ok(Math.abs(a.omega)+Math.abs(b.omega)>.1);
for(let i=0;i<1000;i++){physics.integrate(a,1/120,12);assert.ok(Number.isFinite(a.x+a.y+a.angle));}
});
test('all gravity modes capture debris and release a fast volley',()=>{
for(const mode of [0,1,2]){
const g=new Game(sprites,'orbit');g.start(true);g.setMode(mode);g.input.field=true;g.input.aim={x:1000,y:g.ship.y};advance(g,1.5);
assert.ok(g.held.length>0,`mode ${mode} must capture`);const held=[...g.held];g.input.fire=true;g.input.field=false;advance(g,.03);
assert.equal(g.held.length,0);assert.equal(g.shots,1);assert.ok(held.some(b=>b.vx>400),`mode ${mode} must throw`);
}
});
test('rip tether detaches real armor joints and exposes the core after four plates',()=>{
const g=new Game(sprites,'orbit');g.start(true);g.ship.x=WORLD.bossX-180;g.ship.y=WORLD.bossY;g.ship.vx=0;g.setMode(2);g.input.field=true;g.input.aim={x:WORLD.bossX,y:WORLD.bossY};
const initialHp=g.boss.hp;g.damage(g.boss,100);assert.equal(g.boss.hp,initialHp,'armor protects core');
advance(g,7);assert.ok(g.torn>=4,`detached ${g.torn}`);assert.ok(g.plates.length<=2);
g.damage(g.boss,300);assert.equal(g.phase,'won');assert.ok(g.score>=1500);
});
test('impact kill drops usable scrap and a volatile cell; explosions chain safely',()=>{
const g=new Game(sprites,'orbit',()=>.1);g.start(true);const enemy=g.bodies.find(b=>b.kind==='drone');const before=g.bodies.filter(b=>b.kind==='cell').length;
g.damage(enemy,100);assert.equal(g.kills,1);assert.equal(g.salvage,40);assert.equal(g.bodies.filter(b=>b.kind==='cell').length,before+1);
const a=g.spawn('cell',6,1100,300),b=g.spawn('cell',6,1130,300);g.destroy(a);assert.equal(b.dead,true);
});
test('salvage upgrades cost resources, increase field power/range and have a limit',()=>{
const g=new Game(sprites,'orbit');g.start();assert.equal(g.upgrade(),false);g.salvage=1000;
const power=g.vortexSpeed,range=g.range;for(let i=0;i<3;i++)assert.equal(g.upgrade(),true);
assert.ok(g.vortexSpeed>power);assert.ok(g.range>range);assert.equal(g.upgrade(),false);assert.equal(g.salvage,715);
});
test('pause freezes combat and retry clears progress; practice is survivable',()=>{
const g=new Game(sprites,'orbit');g.start();advance(g,1);g.pause();const time=g.time,x=g.ship.x;advance(g,3);assert.equal(g.time,time);assert.equal(g.ship.x,x);
g.start(true);g.hurt(500);assert.equal(g.ship.hp,25);assert.equal(g.phase,'playing');g.start(false);g.hurt(500);assert.equal(g.phase,'lost');
});
test('sustained combat retains finite bodies without a count cap',()=>{
const g=new Game(sprites,'orbit');g.start(true);
for(let i=0;i<15000;i++){
g.input.field=i%600<400;g.input.fire=i%600>=400;g.input.aim={x:g.ship.x+450,y:g.ship.y};g.setMode(Math.floor(i/1800)%3);g.step(1/120);
for(const b of g.bodies)assert.ok(Number.isFinite(b.x+b.y+b.vx+b.vy+b.angle));
if(g.phase==='won')break;
}
});
test('torn armor can kill the exposed boss through actual projectile collisions',()=>{
const g=new Game(sprites,'orbit');g.start(true);g.ship.x=WORLD.bossX-180;g.ship.y=WORLD.bossY;g.ship.vx=0;
g.bodies=g.bodies.filter(b=>b.attached||b.kind==='core');g.walkers=[];g.rigs=g.rigs.filter(r=>r.root===g.boss);g.setMode(2);g.input.aim={x:WORLD.bossX,y:WORLD.bossY};
g.input.field=true;advance(g,8);assert.ok(g.held.length>0);assert.ok(g.torn>=4);
g.input.fire=true;g.input.field=false;advance(g,3);
assert.ok(g.boss.hp<g.boss.maxHp,'armor volley must damage the core');
// Keep gathering the arena supply and throw until the reactor ruptures.
for(let i=0;i<12&&g.phase==='playing';i++){g.input.fire=false;g.input.field=true;advance(g,3);g.input.aim={x:g.boss.x,y:g.boss.y};g.input.fire=true;g.input.field=false;advance(g,1.5);}
assert.equal(g.phase,'won','a pilot must be able to complete the run by throwing, without direct damage calls');
});
function emptyArena(mode=0){
const g=new Game(sprites,'orbit');g.start(true);g.ship.x=WORLD.bossX-200;g.ship.y=WORLD.startY;g.ship.vx=0;g.bodies=[];g.walkers=[];g.rigs=[];
g.bossShot=999;g.setMode(mode);g.input.aim={x:g.ship.x+900,y:g.ship.y};g.updateCamera(1);return g;
}
test('field follows aim ahead of the tug and collects around that focus',()=>{
const g=emptyArena(),front=g.spawn('scrap',0,g.ship.x+340,g.ship.y),rear=g.spawn('scrap',0,g.ship.x-190,g.ship.y);
assert.equal(g.fieldCenter.x,g.ship.x+165);assert.equal(g.fieldCenter.y,g.ship.y);
g.input.field=true;advance(g,.025);assert.equal(front.held,true);assert.equal(rear.held,false);
g.input.aim={x:g.ship.x-500,y:g.ship.y};assert.ok(g.fieldCenter.x<g.ship.x);advance(g,.025);assert.equal(rear.held,true);
g.input.aim={x:g.ship.x,y:100};assert.equal(g.fieldCenter.x,g.ship.x);assert.ok(g.fieldCenter.y<g.ship.y);
g.input.aim={x:g.ship.x,y:g.ship.y};assert.ok(Math.hypot(g.fieldCenter.x-g.ship.x,g.fieldCenter.y-g.ship.y)>150,'aim at ship retains the last valid facing');
});
test('orbit physically spins fast around its forward focus, rather than the ship',()=>{
const g=emptyArena(),center=g.fieldCenter;
const b=g.spawn('scrap',0,center.x+80,center.y,1,.5);g.input.field=true;advance(g,1);
let angle=Math.atan2(b.y-g.fieldCenter.y,b.x-g.fieldCenter.x),turn=0,minRadius=Infinity,totalSpeed=0;
for(let i=0;i<120;i++){
g.step(1/120);const c=g.fieldCenter,next=Math.atan2(b.y-c.y,b.x-c.x);
turn+=Math.atan2(Math.sin(next-angle),Math.cos(next-angle));angle=next;
minRadius=Math.min(minRadius,Math.hypot(b.x-c.x,b.y-c.y));totalSpeed+=Math.hypot(b.vx,b.vy);
}
assert.ok(turn>11,`actual orbit ${turn.toFixed(2)} rad/s`);
assert.ok(minRadius>50,'strong attraction must not collapse the orbit into the focus');
assert.ok(totalSpeed/120>700,'the actual debris must move violently, not just the visual effect');
assert.ok(Math.abs(b.omega)>15,'individual parts tumble quickly');
});
test('all modes can hold and launch 256 pieces without truncation or backward spread',()=>{
for(const mode of [0,1,2]){
const g=emptyArena(mode),c=g.fieldCenter;
for(let i=0;i<256;i++)g.spawn('scrap',i%6,c.x+Math.cos(i*2.4)*120,c.y+Math.sin(i*2.4)*100,.7,.4);
g.input.field=true;advance(g,1);assert.equal(g.held.length,256,`mode ${mode} has no carry cap`);
g.input.fire=true;g.input.field=false;g.step(1/120);
assert.equal(g.bodies.length,256,'scene cleanup must not discard a large volley');
assert.ok(g.bodies.every(b=>b.thrown>0&&b.vx>500),'every projectile stays in the forward firing cone');
}
});
test('collision grouping preserves eligible contacts and skips whole payload groups',()=>{
const bodies=Array.from({length:60},(_,i)=>{
const b=physics.body('scrap',0,square,i%8*9,Math.floor(i/8)*9);
b.held=i%5===0;b.attached=i%5===1;b.thrown=i%5===2?2:0;b.volley=i%3;return b;
});
const expected=[];
for(let i=0;i<bodies.length;i++)for(let j=i+1;j<bodies.length;j++){
const a=bodies[i],b=bodies[j];
if((a.held&&b.held)||(a.attached&&b.attached)||((a.held||b.held)&&(a.attached||b.attached)))continue;
if(a.thrown>0&&b.thrown>0&&a.volley===b.volley)continue;
if(Math.hypot(a.x-b.x,a.y-b.y)<=a.radius+b.radius)expected.push([a.id,b.id].sort((a,b)=>a-b).join(':'));
}
const actual=[...physics.collisionPairs(bodies)].map(([a,b])=>[a.id,b.id].sort((a,b)=>a-b).join(':'));
assert.deepEqual(actual.sort(),expected.sort());
bodies.forEach(b=>{b.held=true;b.attached=false;});assert.equal([...physics.collisionPairs(bodies)].length,0);
});
test('expanded stage starts with smaller scrap distributed across its width and height',()=>{
const g=new Game(sprites,'orbit'),scrap=g.bodies.filter(b=>b.kind==='scrap'||b.kind==='cell');
assert.equal(scrap.length,224);assert.equal(WORLD.width,4800);
assert.ok(scrap.filter(b=>b.scale<=.56).length>200);
assert.ok(Math.max(...scrap.map(b=>b.x))-Math.min(...scrap.map(b=>b.x))>4000);
assert.ok(Math.max(...scrap.map(b=>b.y))-Math.min(...scrap.map(b=>b.y))>550);
assert.ok(scrap.every(b=>b.y-b.radius>WORLD.ceiling&&b.y+b.radius<WORLD.floor));
const drones=g.bodies.filter(b=>b.kind==='drone');
assert.ok(Math.max(...drones.map(b=>b.homeY))-Math.min(...drones.map(b=>b.homeY))>350);
assert.ok(g.boss.x>4000&&g.boss.y+180<WORLD.floor);
});
test('flight uses the expanded vertical space and a short viewport tracks it',()=>{
const g=emptyArena();g.viewHeight=700;g.input.y=-1;advance(g,3);
assert.equal(g.ship.y,WORLD.ceiling+TUG.radius);assert.ok(g.cameraY<1);
const top=g.ship.y;g.input.y=1;advance(g,5);
assert.equal(g.ship.y,WORLD.floor-TUG.radius);assert.ok(g.ship.y-top>650);
assert.ok(g.cameraY>299&&g.cameraY<=300);
g.viewHeight=1200;g.updateCamera(1);assert.equal(g.cameraY,-100);
g.viewHeight=1000;g.viewWidth=WORLD.width+500;g.updateCamera(1);
assert.equal(g.camera,0);assert.equal(g.cameraY,0);
});
test('replacement salvage appears at the current flight altitude and fragments stay small',()=>{
const g=emptyArena();g.ship.y=WORLD.floor-60;g.supplyTimer=6;g.step(1/120);
assert.equal(g.bodies.length,12);assert.ok(g.bodies.every(b=>Math.abs(b.y-g.ship.y)<150&&b.y<WORLD.floor));
const part=g.bodies[1],before=g.bodies.length;g.destroy(part);
const fragments=g.bodies.slice(before);assert.equal(fragments.length,2);
assert.ok(fragments.every(b=>b.scale<part.scale&&b.mass<=part.mass/2));
});
test('enemy cells are occasional drops with deterministic coverage of both outcomes',()=>{
for(const [roll,expected] of [[.1,1],[.9,0]]){
const g=new Game(sprites,'orbit',()=>roll);g.start(true);const target=g.bodies.find(b=>b.kind==='wheel');
const before=g.bodies.filter(b=>b.kind==='cell').length;g.damage(target,200);
assert.equal(g.bodies.filter(b=>b.kind==='cell').length-before,expected);
}
});
test('Foundry starts with floor scrap and stable structures; orbital scrap has no gravity',()=>{
const ground=new Game(sprites,'foundry');ground.start(true);
const scrap=ground.bodies.filter(b=>['scrap','cell'].includes(b.kind));
assert.equal(scrap.length,224);assert.ok(scrap.every(b=>b.y>WORLD.floor-65));
const attached=ground.bodies.filter(b=>b.kind==='structure'&&b.attached).length;
advance(ground,3);assert.equal(ground.demolished,0);assert.equal(ground.explosions,0);
assert.equal(ground.bodies.filter(b=>b.kind==='structure'&&b.attached).length,attached);
const g=emptyArena(),floating=g.spawn('scrap',0,g.ship.x+100,400);advance(g,2);
assert.equal(floating.y,400);assert.equal(floating.vy,0);
g.site='foundry';advance(g,2);assert.ok(floating.y>800);advance(g,2);
assert.ok(Math.abs(floating.y+physics.verticalExtent(floating).max-WORLD.floor)<.01);
assert.ok(Math.abs(floating.vy)<1,'floor contact settles instead of perpetually bouncing');
});
test('ripping a structural deck releases unsupported upper pieces into gravity',()=>{
const g=new Game(sprites,'foundry');g.start(true);const rig=g.structures[0],deck=rig.parts[2],upper=rig.parts[6];
g.bodies=[...rig.parts];g.walkers=[];g.rigs=[];
g.ship.x=rig.x-180;g.ship.y=deck.y;g.ship.vx=0;g.input.aim={x:deck.x,y:deck.y};g.input.field=true;g.setMode(2);
advance(g,2);assert.ok(g.demolished>=5,'connected assembly must come apart');assert.equal(upper.attached,false);
g.input.fire=true;g.input.field=false;g.clearInput();for(const part of rig.parts){part.held=false;part.vx=0;part.vy=0;}
g.bodies=[upper];upper.y=Math.min(upper.y,WORLD.floor-80);
const y=upper.y;advance(g,.25);assert.ok(upper.y>y,'disconnected pieces fall');
});
test('cells survive collection but arm on release and detonate on first body contact',()=>{
const g=emptyArena(1),focus=g.fieldCenter,cell=g.spawn('cell',6,focus.x,focus.y,1,.55);
g.input.field=true;advance(g,.5);assert.equal(cell.held,true);assert.equal(cell.armed,false);assert.equal(g.explosions,0);
g.input.fire=true;g.input.field=false;g.step(1/120);assert.equal(cell.armed,true);
const enemy=g.spawn('wheel',9,cell.x+cell.vx/120,cell.y+cell.vy/120,2.5,1);enemy.cooldown=999;
g.step(1/120);
assert.equal(cell.dead,true);assert.equal(g.explosions,1);assert.ok(enemy.hp<enemy.maxHp||enemy.dead);
});
test('armed cells detonate against floor and ceiling even with intact HP',()=>{
for(const ceiling of [false,true]){
const g=emptyArena(),cell=g.spawn('cell',6,g.ship.x+200,ceiling?WORLD.ceiling+10:WORLD.floor-10,1,.55);
cell.armed=true;cell.vy=ceiling?-600:600;advance(g,.025);
assert.equal(cell.dead,true);assert.equal(g.explosions,1);
}
});
test('cell blasts chain, impart violent debris motion and demolish load-bearing joints',()=>{
const g=new Game(sprites,'foundry');g.start(true);const rig=g.structures[0],deck=rig.parts[2];
g.bodies=g.bodies.filter(b=>b.kind==='structure');
const a=g.spawn('cell',6,deck.x,deck.y),b=g.spawn('cell',6,deck.x+70,deck.y);
const scrap=g.spawn('scrap',0,deck.x+140,deck.y,1,.4);g.destroy(a);
assert.equal(b.dead,true);assert.equal(g.explosions,2);assert.ok(g.demolished>=6);
assert.ok(scrap.vx>500);assert.ok(g.blasts.length>=2);assert.ok(g.particles.length>100);
assert.ok(rig.parts.some(p=>!p.attached&&Math.hypot(p.vx,p.vy)>200));
});
test('site selection and retry retain the selected gravity rules and reset destruction',()=>{
const g=new Game(sprites);assert.equal(g.site,'foundry');g.start(true,'orbit');assert.equal(g.environment.gravity,0);
g.start(true,'foundry');g.destroy(g.bodies.find(b=>b.kind==='cell'));assert.ok(g.explosions>0);
g.start(true);assert.equal(g.site,'foundry');assert.equal(g.explosions,0);assert.equal(g.demolished,0);assert.ok(g.environment.gravity>0);
});
const {createWalker,stepWalker,walkerLegs}=load('walkers');
function artilleryArena(mode=1){
const g=emptyArena(mode);g.site='foundry';g.ship.y=WORLD.floor-190;
const b=g.spawn('walker',13,g.ship.x+400,WORLD.floor-106,6,1.6);b.hp=b.maxHp=115;
const walker=createWalker(b);g.walkers=[walker];return {g,walker};
}
test('medium walkers telegraph physical shells, walk on articulated feet and stir floor scrap',()=>{
const {g,walker}=artilleryArena(),start=walker.body.x;
const foot=walkerLegs(walker,[]).find(leg=>leg.planted).foot;
const scrap=g.spawn('scrap',0,foot.x,foot.y,1,.4);
stepWalker(walker,g.ship,[scrap],1/120);assert.ok(scrap.vx<0&&scrap.vy<0,'planted feet push scrap');
advance(g,1.3);assert.ok(walker.windup>0);assert.equal(g.bodies.filter(b=>b.kind==='shell').length,0);
advance(g,.65);const shell=g.bodies.find(b=>b.kind==='shell');assert.ok(shell,'charge emits a physics body');
assert.ok(walker.body.x<start-25);assert.equal(shell.ordnance.owner,walker.body.id);assert.equal(shell.ordnance.state,'hostile');assert.equal(shell.thrown,0);
assert.ok(shell.vx<0&&shell.armed);assert.equal(shell.hull.length,4);
});
test('each gravity mode catches incoming shells, pauses their fuse and throws the same body back armed',()=>{
for(const mode of [0,1,2]){
const g=emptyArena(mode),focus=g.fieldCenter;
const shell=g.spawn('shell',14,focus.x+110,focus.y,1.2,.65);
shell.ordnance={owner:999,state:'hostile',fuse:.02};shell.armed=true;shell.vx=-300;
g.input.field=true;advance(g,.025);assert.equal(shell.held,true);assert.equal(shell.ordnance.state,'captured');assert.equal(shell.armed,false);
advance(g,7);assert.equal(shell.dead,false);assert.equal(g.explosions,0);assert.equal(g.shellsCaught,1);
assert.equal(shell.ordnance.fuse,6);g.input.fire=true;g.input.field=false;g.step(1/120);
assert.equal(shell.held,false);assert.equal(shell.ordnance.state,'returned');assert.equal(shell.armed,true);assert.equal(g.shellsReturned,1);assert.ok(shell.vx>600);
}
});
test('a real walker shot can be caught and returned to kill its own shooter with collision damage',()=>{
const {g,walker}=artilleryArena();g.input.aim={x:walker.body.x,y:walker.body.y};g.input.field=true;
for(let i=0;i<480&&g.shellsCaught===0;i++)g.step(1/120);
assert.equal(g.shellsCaught,1);const shell=g.held.find(b=>b.kind==='shell');assert.ok(shell);
advance(g,.8);g.input.aim={x:walker.body.x,y:walker.body.y};g.input.fire=true;g.input.field=false;
for(let i=0;i<240&&!walker.body.dead;i++)g.step(1/120);
assert.equal(shell.dead,true);assert.equal(walker.body.dead,true);assert.equal(g.returnKills,1);assert.equal(g.kills,1);assert.equal(g.salvage,75);
assert.ok(g.bodies.some(b=>b.kind==='scrap'),'wrecked legs and armor become salvage');
});
test('hostile shell ignores its launcher but returned shell hits it; uncaught shells hit pilots or expire',()=>{
const {g,walker}=artilleryArena();g.updateCamera(1);
const shell=g.spawn('shell',14,walker.body.x,walker.body.y,1.2,.65);
shell.ordnance={owner:walker.body.id,state:'hostile',fuse:6};shell.armed=true;g.step(1/120);assert.equal(shell.dead,false);
shell.ordnance.state='returned';g.step(1/120);assert.equal(shell.dead,true);assert.equal(walker.body.dead,true);
const pilot=emptyArena();pilot.practice=false;
const hit=pilot.spawn('shell',14,pilot.ship.x,pilot.ship.y,1.2,.65);hit.armed=true;hit.ordnance={owner:999,state:'hostile',fuse:6};pilot.step(1/120);
assert.equal(hit.dead,true);assert.equal(pilot.ship.hp,82);
const timed=pilot.spawn('shell',14,pilot.ship.x+500,500,1.2,.65);timed.ordnance={owner:999,state:'hostile',fuse:.05};advance(pilot,.1);assert.equal(timed.dead,true);
});
test('touch auto aim intercepts shells, then aims the captured payload back at its shooter',()=>{
const {g,walker}=artilleryArena();g.input.aim=null;
const shell=g.spawn('shell',14,g.ship.x+180,g.ship.y-80,1.2,.65);shell.armed=true;shell.ordnance={owner:walker.body.id,state:'hostile',fuse:6};
assert.ok(g.aim().y<0,'aim follows the incoming shell above the pilot');g.input.field=true;g.step(1/120);
assert.equal(shell.held,true);assert.ok(g.aim().y>0,'secured shell aims back down at its walker');
g.start(true);assert.equal(g.walkers.length,3);assert.equal(g.shellsCaught,0);assert.equal(g.shellsReturned,0);
});
test('returned shells retain their sender credit through an energy-cell chain reaction',()=>{
const g=emptyArena(),x=g.ship.x+400,y=g.ship.y;
const cell=g.spawn('cell',6,x-20,y),walker=g.spawn('walker',13,x,y,6,1.6);walker.hp=walker.maxHp=115;
const shell=g.spawn('shell',14,x-240,y,1.2,.65);shell.ordnance={owner:walker.id,state:'returned',fuse:6};
g.destroy(shell);assert.equal(cell.dead,true);assert.equal(walker.dead,true);assert.equal(g.returnKills,1);assert.equal(g.explosions,2);
});
const {Launcher}=load('launcher'),{Rig}=load('rigging');
test('release patterns conserve payload and distinguish continuous single shots from timed clusters',()=>{
for(const [mode,first,interval] of [[0,31,0],[1,1,.075],[2,5,.38]]){
const launcher=new Launcher();launcher.setMode(mode);let cargo=Array.from({length:31},(_,id)=>({id})),fired=[];
const shot=launcher.tick(1/120,true,cargo);assert.equal(shot.length,first);fired.push(...shot);cargo=cargo.filter(b=>!shot.includes(b));
if(mode){assert.equal(launcher.tick(interval/2,true,cargo).length,0);const next=launcher.tick(interval/2+.001,true,cargo);assert.equal(next.length,first);fired.push(...next);cargo=cargo.filter(b=>!next.includes(b));}
assert.equal(launcher.tick(1,false,cargo).length,0,'no automatic discharge after fire is released');
for(let i=0;i<500&&cargo.length;i++){const next=launcher.tick(1/120,true,cargo);fired.push(...next);cargo=cargo.filter(b=>!next.includes(b));}
assert.equal(cargo.length,0);assert.equal(new Set(fired.map(b=>b.id)).size,31);
}
});
test('firing and collection are independent; releasing collection drops without firing',()=>{
const g=emptyArena(1);g.setRelease(1);g.input.field=true;
for(let i=0;i<20;i++)g.spawn('scrap',0,g.fieldCenter.x+i,g.fieldCenter.y,1,.4);
advance(g,.5);assert.equal(g.held.length,20);g.input.fire=true;advance(g,.2);
assert.ok(g.held.length>0&&g.held.length<20);assert.ok(g.shots>=3);assert.equal(g.input.field,true);
g.input.fire=false;const held=g.held.length;advance(g,.2);assert.equal(g.held.length,held);
const shots=g.shots,cargo=[...g.held];g.input.field=false;g.step(1/120);assert.equal(g.held.length,0);assert.equal(g.shots,shots);assert.ok(cargo.every(b=>b.thrown===0));advance(g,.5);assert.equal(g.shots,shots);
});
test('shared rig breaks a joint subtree once and updates capabilities and inherited motion',()=>{
const root=physics.body('walker',13,square,100,100),rig=new Rig(root,'test');root.vx=130;
const upper=physics.body('limb',15,square,100,140),lower=physics.body('limb',15,square,100,170),power=physics.body('power',16,square,100,70);
rig.add({key:'upper',role:'leg',body:upper,offset:{x:0,y:40},strength:50,limb:'left'});
rig.add({key:'lower',parent:'upper',role:'leg',body:lower,offset:{x:0,y:30},strength:50,limb:'left'});
rig.add({key:'power',role:'shield',body:power,offset:{x:0,y:-30},strength:80});rig.sync();
assert.equal(rig.legs,1);assert.equal(rig.shielded,true);assert.equal(rig.stress(upper,49).length,0);
const fallen=rig.stress(upper,2);assert.equal(fallen.length,2);assert.equal(rig.mobility,0);assert.equal(lower.attached,false);assert.equal(lower.vx,130);assert.equal(lower.rigId,undefined);assert.equal(rig.detach(upper).length,0);
assert.equal(rig.detach(power).length,1);assert.equal(rig.shielded,false);assert.equal(power.kind,'cell');assert.equal(power.armed,false);
});
function surgicalRig(kind='walker'){
const g=new Game(sprites,'foundry',()=>.9);g.start(true);const rig=g.rigs.find(r=>r.root.kind===kind);
g.bodies=[rig.root,...rig.parts.map(p=>p.body)];g.rigs=[rig];g.walkers=g.walkers.filter(w=>w.body===rig.root);g.ship.x=rig.root.x-300;g.ship.y=rig.root.y-70;g.ship.vx=0;g.input.x=-TUG.cruise/TUG.speedX;g.setMode(3);g.bossShot=999;g.updateCamera(1);
return {g,rig};
}
test('Surgical locks exactly one selected piece, drags it, drops gently and shoots only on a fresh fire press',()=>{
const g=emptyArena(3),x=g.ship.x+180,y=g.ship.y;
const target=g.spawn('scrap',0,x,y,1,.5),other=g.spawn('scrap',0,x+90,y,1,.5);g.input.aim={x,y};g.input.field=true;advance(g,.2);
assert.equal(g.held.length,1);assert.equal(g.focus,target);assert.equal(other.held,false);
g.input.aim={x:x+160,y:y-90};advance(g,.8);assert.ok(target.x>x+130);assert.ok(target.y<y-60);
g.input.field=false;g.step(1/120);assert.equal(target.held,false);assert.equal(target.thrown,0);assert.equal(g.shots,0);
g.input.aim={x:target.x,y:target.y};g.input.field=true;advance(g,.1);g.input.fire=true;advance(g,.1);
assert.equal(target.held,false);assert.ok(target.thrown>0);assert.equal(g.shots,1);advance(g,.3);assert.equal(g.shots,1);assert.equal(other.held,false);
});
test('shield extraction unlocks living Strider and Warden rigs; limbs follow their drag and throw',()=>{
for(const kind of ['walker','core']){
const {g,rig}=surgicalRig(kind),root=rig.root,cell=rig.connected('shield')[0].body;
g.input.aim={x:root.x,y:root.y};g.input.field=true;advance(g,.1);assert.equal(root.held,false);assert.match(g.focusLabel,/GRAV SHIELD/);
g.input.field=false;g.step(1/120);g.input.aim={x:cell.x,y:cell.y};g.input.field=true;advance(g,1.3);
assert.equal(rig.shielded,false);assert.equal(cell.held,true);assert.equal(g.cellsExtracted,1);assert.equal(cell.kind,'cell');
g.input.field=false;g.step(1/120);g.bodies=g.bodies.filter(b=>b!==cell);g.input.aim={x:root.x,y:root.y};g.input.field=true;advance(g,.1);assert.equal(root.held,true);assert.equal(root.dead,false);
const y=root.y;g.input.aim={x:root.x+100,y:y-160};advance(g,1);assert.ok(root.y<y-100);
assert.ok(rig.connected('leg').every(p=>Math.hypot(p.body.x-root.x,p.body.y-root.y)<(kind==='core'?280:180)),'remaining limbs travel with the held root');
const x=root.x;g.input.fire=true;g.step(1/120);assert.equal(g.liveThrows,1);assert.ok(root.thrown>0);advance(g,.12);assert.ok(root.x>x+30);assert.equal(root.held,false);
}
});
test('ripped Strider legs reduce mobility and the shared Warden rig also loses limbs and gravity shielding',()=>{
for(const kind of ['walker','core']){
const {g,rig}=surgicalRig(kind),leg=rig.connected('leg')[0].body;
g.input.aim={x:leg.x,y:leg.y};g.input.field=true;advance(g,1.1);
assert.equal(leg.attached,false,kind);assert.ok(rig.mobility<1,kind);assert.ok(g.limbsTorn>=2,kind);assert.equal(rig.shielded,true);
g.input.field=false;g.step(1/120);const cell=rig.connected('shield')[0].body;g.input.aim={x:cell.x,y:cell.y};g.input.field=true;advance(g,1.3);
assert.equal(rig.shielded,false,kind);assert.equal(rig.root.attached,false,kind);assert.equal(cell.held,true,kind);
}
});
test('intact rig parts cannot collide with their own living root, but detached limbs can',()=>{
const root=physics.body('walker',13,square,0,0),rig=new Rig(root,'test'),part=physics.body('limb',15,square,10,0);
rig.add({key:'arm',role:'leg',body:part,offset:{x:10,y:0},strength:50});assert.equal([...physics.collisionPairs([root,part])].length,0);
rig.detach(part);assert.equal([...physics.collisionPairs([root,part])].length,1);
});
test('surgical support extraction releases the assembly but captures only the selected beam',()=>{
const g=new Game(sprites,'foundry');g.start(true);g.walkers=[];g.rigs=[];const rig=g.structures[0],deck=rig.parts[2];g.bodies=[...rig.parts];
g.ship.x=rig.x-250;g.ship.y=deck.y-90;g.ship.vx=0;g.input.x=-TUG.cruise/TUG.speedX;g.setMode(3);g.input.aim={x:deck.x,y:deck.y};g.input.field=true;advance(g,.9);
assert.equal(g.held.length,1);assert.equal(g.held[0],deck);assert.ok(g.demolished>=5);assert.ok(rig.parts.some(p=>!p.attached&&!p.held));
});
test('hostile artillery clears its whole attached rig; a returned shell hits those same parts',()=>{
const {g,rig}=surgicalRig(),cell=rig.connected('shield')[0].body;
const shell=g.spawn('shell',14,cell.x,cell.y,1.2,.65);shell.armed=true;
shell.ordnance={owner:rig.root.id,state:'hostile',fuse:6};g.step(1/120);
assert.equal(shell.dead,false);assert.equal(g.explosions,0);
shell.ordnance.state='returned';g.step(1/120);assert.equal(shell.dead,true);assert.ok(g.explosions>0);assert.equal(g.returnKills,1);
});
test('a thrown living boss stays within reach and survives offscreen cleanup',()=>{
const {g,rig}=surgicalRig('core'),root=rig.root;
rig.releaseAll();root.attached=false;root.thrown=1;root.x=WORLD.width-60;root.vx=1000;root.cooldown=999;
g.step(1/120);assert.ok(root.x<=WORLD.width-70-root.radius);assert.ok(root.vx<0);
root.x=150;root.thrown=0;g.step(1/120);assert.ok(g.bodies.includes(root));assert.ok(g.rigs.includes(rig));
});
test('sustained fire starts empty and keeps feeding newly gathered debris in the aimed direction',()=>{
for(const mode of [1,2]){
const g=emptyArena(1);g.setRelease(mode);g.input.fire=true;advance(g,.2);assert.equal(g.shots,0);g.input.field=true;
const supplied=[];
for(let i=0;i<12;i++){
const b=g.spawn('scrap',0,g.fieldCenter.x,g.fieldCenter.y,1,.4);supplied.push(b);advance(g,.1);
if(b.thrown>0)assert.ok(b.vx>600&&Math.abs(b.vy)<b.vx*.08,`aimed shot ${mode}: ${b.vx}, ${b.vy}`);
}
assert.ok(g.shots>2);assert.ok(supplied.filter(b=>b.thrown>0||b.dead).length>=8);
g.input.fire=false;const shots=g.shots;advance(g,.2);assert.equal(g.shots,shots);assert.equal(g.input.field,true);
}
});
test('dropping cargo does not arm cells or count shells as returned; shell fuses resume',()=>{
const g=emptyArena(1);g.input.field=true;const c=g.fieldCenter;
const cell=g.spawn('cell',6,c.x,c.y,1,.55),shell=g.spawn('shell',14,c.x+50,c.y,1.2,.65);
shell.ordnance={owner:999,state:'hostile',fuse:3};shell.armed=true;advance(g,.2);
g.input.field=false;g.step(1/120);assert.equal(cell.armed,false);assert.equal(cell.thrown,0);assert.equal(g.shots,0);
assert.equal(shell.ordnance.state,'hostile');assert.ok(shell.ordnance.fuse<6);assert.equal(shell.armed,true);assert.equal(g.shellsReturned,0);
});
