Back to Stick Runner
SOURCE / PINNED RELEASE

Made of little things.

Stick Runner

Release
068f2c4fd180…
Author-recorded commit
2f0b589e2e75…
License
LICENSE
Author’s source reference
nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-938fb0aa7b7

Archive hash verified: a0399c408781bf50…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/game/stickman.ts
export type Pose = 'stand' | 'run' | 'jump' | 'fall' | 'cheer' | 'roll' | 'slide';

export interface Point {
  x: number;
  y: number;
}

/** One straight piece of Rog's body (a bone), and whether it's on the near or far side. */
export interface Bone {
  a: Point;
  b: Point;
  side: 'near' | 'far' | 'middle';
}

/** Rog's whole body for one moment, measured from his feet, with +x pointing the way he faces and +y down. */
export interface Shape {
  bones: Bone[];
  head: Point;
  headRadius: number;
  /** Is he moving (so the bandana tails stream out behind)? */
  moving: boolean;
}

/**
 * Work out where Rog's arms, legs, body and head are.
 * `cycle` moves the legs and arms while running, or is how far through a roll he is (0 to 1).
 */
export function stickmanShape(pose: Pose, cycle: number, vy: number): Shape {
  if (pose === 'roll') return rollShape(cycle);

  const hip = { x: 0, y: -16 };
  let lean = 0;
  let lift = 0;
  let legs: [number, number][] = [];
  let arms: [number, number][] = [];

  if (pose === 'run') {
    const swing = Math.sin(cycle);
    lean = 3;
    legs = [limb(hip, 0.2 + swing * 0.8, 16), limb(hip, 0.2 - swing * 0.8, 16)];
    arms = [[-swing * 7, 7], [swing * 7, 7]];
  } else if (pose === 'jump') {
    const rising = vy < 0;
    lean = 2;
    legs = rising ? [[7, -6], [-5, -2]] : [[5, -1], [-4, 0]];
    arms = rising ? [[6, -8], [-6, -6]] : [[8, -2], [-8, -3]];
  } else if (pose === 'slide') {
    // Surfing on the ice: feet apart, leaning back, arms out for balance.
    const wobble = Math.sin(cycle * 3) * 2;
    lean = -3;
    legs = [[9, 0], [-7, 0]];
    arms = [[10, -4 + wobble], [-10, -6 - wobble]];
  } else if (pose === 'fall') {
    legs = [[-7, -4], [7, -2]];
    arms = [[-9, -12], [9, -12]];
  } else if (pose === 'cheer') {
    lift = Math.abs(Math.sin(cycle)) * 3;
    legs = [[-5, 0], [5, 0]];
    arms = [[-7, -14], [7, -14]];
  } else {
    legs = [[-4, 0], [4, 0]];
    arms = [[-4, 9], [4, 9]];
  }

  const shoulder = { x: lean, y: -29 };
  const bones: Bone[] = [{ a: hip, b: shoulder, side: 'middle' }];
  // Legs (knee halfway, pushed forward a little).
  legs.forEach(([fx, fy], i) => {
    const knee = { x: (hip.x + fx) / 2 + 3, y: (hip.y + fy) / 2 };
    const side = i === 0 ? 'near' : 'far';
    bones.push({ a: hip, b: knee, side }, { a: knee, b: { x: fx, y: fy }, side });
  });
  // Arms.
  arms.forEach(([hx, hy], i) => {
    const top = { x: shoulder.x, y: shoulder.y + 1 };
    const elbow = { x: shoulder.x + hx * 0.5 + (pose === 'run' ? -2 : 0), y: shoulder.y + hy * 0.5 + 2 };
    const side = i === 0 ? 'near' : 'far';
    bones.push({ a: top, b: elbow, side }, { a: elbow, b: { x: shoulder.x + hx, y: shoulder.y + hy }, side });
  });
  const shape: Shape = { bones, head: { x: shoulder.x + lean * 0.4, y: shoulder.y - 6 }, headRadius: 5, moving: pose !== 'stand' };
  return lift ? moveShape(shape, (p) => ({ x: p.x, y: p.y - lift })) : shape;
}

/** Curled up in a ball, spinning forward once. */
function rollShape(progress: number): Shape {
  // Squash down at the start and stand back up at the end.
  const tuck = Math.sin(progress * Math.PI);
  const angle = progress * Math.PI * 2;
  const centreY = -10 - (1 - tuck) * 4;
  const place = (p: Point): Point => ({
    x: p.x * Math.cos(angle) - p.y * Math.sin(angle),
    y: centreY + p.x * Math.sin(angle) + p.y * Math.cos(angle),
  });
  const bones: Bone[] = [];
  // Curved back.
  const steps = 5;
  for (let i = 0; i < steps; i++) {
    const t0 = Math.PI * (0.75 + (1.35 * i) / steps);
    const t1 = Math.PI * (0.75 + (1.35 * (i + 1)) / steps);
    bones.push({ a: { x: Math.cos(t0) * 8, y: Math.sin(t0) * 8 }, b: { x: Math.cos(t1) * 8, y: Math.sin(t1) * 8 }, side: 'middle' });
  }
  // Knees pulled up, arms hugging them.
  const chains: [number[], Bone['side']][] = [
    [[-6, 5, 2, -1, 6, 6], 'near'],
    [[-5, 6, 4, 3, 7, 8], 'far'],
    [[3, -4, 4, 2, 8, 1], 'near'],
  ];
  for (const [points, side] of chains) {
    for (let i = 0; i + 3 < points.length; i += 2) {
      bones.push({ a: { x: points[i], y: points[i + 1] }, b: { x: points[i + 2], y: points[i + 3] }, side });
    }
  }
  return moveShape({ bones, head: { x: 6, y: -5 }, headRadius: 4.5, moving: true }, place);
}

function moveShape(shape: Shape, move: (p: Point) => Point): Shape {
  return {
    ...shape,
    bones: shape.bones.map((bone) => ({ a: move(bone.a), b: move(bone.b), side: bone.side })),
    head: move(shape.head),
  };
}

function limb(from: Point, angle: number, length: number): [number, number] {
  return [from.x + Math.sin(angle) * length, Math.min(0, from.y + Math.cos(angle) * length)];
}