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 { clamp, length, rotate, sub, unit, type Body, type Vec } from './physics.js';
import { WORLD } from './world.js';
import { legKnee, type Rig } from './rigging.js';
import { poseLeg } from './enemy-rigs.js';
export interface Walker { body: Body; gait: number; direction: number; aim: Vec; reload: number; windup: number; rig?: Rig }
export interface Leg { hip: Vec; knee: Vec; foot: Vec; planted: boolean }
export function createWalker(body: Body,rig?: Rig): Walker {
return {body,rig,gait:0,direction:-1,aim:{x:-1,y:0},reload:1.2,windup:0};
}
/** Stance feet cancel the body's travel; the alternating swing lifts over scrap. */
export function walkerLegs(walker: Walker,bodies: Body[]): Leg[] {
const b=walker.body;
return [-32,-17,17,32].map((offset,i)=>{
const phase=(walker.gait+i%2*.5)%1,planted=phase<.65;
const progress=planted?phase/.65:(phase-.65)/.35;
const dx=(planted?.5-progress:progress-.5)*64*walker.direction;
const foot={x:b.x+offset+dx,y:b.held||b.thrown>0?b.y+103:WORLD.floor-3};
let pile=0;
for(const part of bodies) if(!part.dead&&!part.held&&!part.attached&&['scrap','plate','structure'].includes(part.kind)&&Math.abs(part.x-foot.x)<part.radius+12&&part.y>WORLD.floor-48)
pile=Math.max(pile,clamp(WORLD.floor-part.y+part.radius*.4,0,25));
foot.y-=pile+(planted?0:Math.sin(progress*Math.PI)*32);
const hip={x:b.x+offset*.8,y:b.y+20};
const knee=legKnee(hip,foot,58,64,i<2?-1:1);
if(b.held||b.thrown>0)for(const point of [hip,knee,foot]){const p=rotate(sub(point,b),b.angle);point.x=b.x+p.x;point.y=b.y+p.y;}
return {hip,knee,foot,planted};
});
}
export function poseWalker(walker: Walker,bodies: Body[]): void {
if(!walker.rig)return;
walkerLegs(walker,bodies).forEach((pose,i)=>poseLeg(walker.rig!,i,pose));walker.rig.sync();
}
/** Returns true once after the telegraphed charge, when a shell should launch. */
export function stepWalker(walker: Walker,ship: Vec,bodies: Body[],dt: number): boolean {
const b=walker.body,dx=ship.x-b.x;
if(b.held||b.thrown>0){walker.windup=0;return false;}
const mobility=walker.rig?.mobility??1;
const moving=Math.abs(dx)>270&&Math.abs(dx)<1100;
const speed=moving?Math.sign(dx)*32*mobility:0;
b.vx+=(speed-b.vx)*Math.min(1,dt*3);
if(mobility>0)b.vy+=((WORLD.floor-40-66*mobility-b.y)*70-b.vy*15)*dt;
if(Math.abs(b.vx)>3)walker.direction=Math.sign(b.vx);
walker.gait+=Math.abs(b.vx)*dt*.65/64;
b.angle=Math.sin(walker.gait*Math.PI*4)*(.025+(1-mobility)*.14);
walker.aim=unit(sub(ship,b));
for(const [i,leg] of walkerLegs(walker,bodies).entries()) if(leg.planted&&(!walker.rig||walker.rig.connected('leg').some(p=>p.key===`leg-${i}-lower`))) for(const part of bodies) {
if(part.dead||part.held||part.attached||!['scrap','cell','plate','structure'].includes(part.kind))continue;
if(length(sub(part,leg.foot))<part.radius+17) {
// Shove litter at walking speed; repeated foot contact must not charge a
// resting cell up to a hard-impact detonation before the fight begins.
part.vx+=(b.vx+walker.direction*28-part.vx)*Math.min(1,dt*4/Math.sqrt(part.mass));
part.vy-=dt*20;part.omega+=walker.direction*dt*2;
}
}
if(Math.abs(dx)>900)return false;
if(walker.windup>0) {
walker.windup-=dt;
if(walker.windup<=0) { walker.reload=3.6;return true; }
} else {
walker.reload-=dt;
if(walker.reload<=0)walker.windup=.7;
}
return false;
}
