Back to GRAV / RIFT
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.

src/game/walkers.ts
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;
}