Back to Mini Samurai
SOURCE / PINNED RELEASE

Made of little things.

Mini Samurai

Release
a7beb798adbc…
Author-recorded commit
4f97be1515b8…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-3b28f0efc5b

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

src/game/pose.ts
import * as THREE from 'three';

/**
 * A full-body pose in the fighter's local space (+Z forward, +Y up, +X is the
 * fighter's LEFT). Limbs are driven by IK targets, the trunk by rotations.
 * Hand targets move with the hips automatically (see Rig.apply).
 */
export interface Pose {
  /** Hips position relative to the feet origin. Rest is (0, 0.43, 0). */
  hips: THREE.Vector3;
  hipsQ: THREE.Quaternion;
  spineQ: THREE.Quaternion;
  chestQ: THREE.Quaternion;
  headQ: THREE.Quaternion;
  /** Ankle targets. */
  footL: THREE.Vector3;
  footR: THREE.Vector3;
  /** Foot pitch in radians (toe up = negative). */
  footLPitch: number;
  footRPitch: number;
  /** Wrist targets and their orientation (hand +Y = held item's blade, +Z = cutting edge). */
  handR: THREE.Vector3;
  handRQ: THREE.Quaternion;
  handL: THREE.Vector3;
  handLQ: THREE.Quaternion;
  /** 1 = left hand clamps onto the held item's grip, 0 = left hand follows handL. */
  twoHand: number;
  /** Elbow hints (direction the elbow points, local space). */
  elbowR: THREE.Vector3;
  elbowL: THREE.Vector3;
}

export const REST_HIPS_Y = 0.43;
const RAD = Math.PI / 180;

export function emptyPose(): Pose {
  return {
    hips: new THREE.Vector3(0, REST_HIPS_Y, 0),
    hipsQ: new THREE.Quaternion(),
    spineQ: new THREE.Quaternion(),
    chestQ: new THREE.Quaternion(),
    headQ: new THREE.Quaternion(),
    footL: new THREE.Vector3(0.08, 0.035, 0),
    footR: new THREE.Vector3(-0.08, 0.035, 0),
    footLPitch: 0,
    footRPitch: 0,
    handR: new THREE.Vector3(-0.2, 0.42, 0.05),
    handRQ: new THREE.Quaternion(),
    handL: new THREE.Vector3(0.2, 0.42, 0.05),
    handLQ: new THREE.Quaternion(),
    twoHand: 0,
    elbowR: new THREE.Vector3(-1, -0.3, -0.6).normalize(),
    elbowL: new THREE.Vector3(1, -0.3, -0.6).normalize(),
  };
}

export function copyPose(out: Pose, p: Pose): Pose {
  out.hips.copy(p.hips);
  out.hipsQ.copy(p.hipsQ);
  out.spineQ.copy(p.spineQ);
  out.chestQ.copy(p.chestQ);
  out.headQ.copy(p.headQ);
  out.footL.copy(p.footL);
  out.footR.copy(p.footR);
  out.footLPitch = p.footLPitch;
  out.footRPitch = p.footRPitch;
  out.handR.copy(p.handR);
  out.handRQ.copy(p.handRQ);
  out.handL.copy(p.handL);
  out.handLQ.copy(p.handLQ);
  out.twoHand = p.twoHand;
  out.elbowR.copy(p.elbowR);
  out.elbowL.copy(p.elbowL);
  return out;
}

/** out = a → b by t (out may alias a). */
export function blendPose(out: Pose, a: Pose, b: Pose, t: number): Pose {
  out.hips.lerpVectors(a.hips, b.hips, t);
  out.hipsQ.slerpQuaternions(a.hipsQ, b.hipsQ, t);
  out.spineQ.slerpQuaternions(a.spineQ, b.spineQ, t);
  out.chestQ.slerpQuaternions(a.chestQ, b.chestQ, t);
  out.headQ.slerpQuaternions(a.headQ, b.headQ, t);
  out.footL.lerpVectors(a.footL, b.footL, t);
  out.footR.lerpVectors(a.footR, b.footR, t);
  out.footLPitch = a.footLPitch + (b.footLPitch - a.footLPitch) * t;
  out.footRPitch = a.footRPitch + (b.footRPitch - a.footRPitch) * t;
  out.handR.lerpVectors(a.handR, b.handR, t);
  out.handRQ.slerpQuaternions(a.handRQ, b.handRQ, t);
  out.handL.lerpVectors(a.handL, b.handL, t);
  out.handLQ.slerpQuaternions(a.handLQ, b.handLQ, t);
  out.twoHand = a.twoHand + (b.twoHand - a.twoHand) * t;
  out.elbowR.lerpVectors(a.elbowR, b.elbowR, t).normalize();
  out.elbowL.lerpVectors(a.elbowL, b.elbowL, t).normalize();
  return out;
}

// ---- Authoring helpers -------------------------------------------------------

/** Rotation from degrees: pitch (x, + leans forward), yaw (y, + turns left), roll (z). */
export function rot(pitch = 0, yaw = 0, roll = 0): THREE.Quaternion {
  return new THREE.Quaternion().setFromEuler(new THREE.Euler(pitch * RAD, yaw * RAD, roll * RAD, 'YXZ'));
}

export function v(x: number, y: number, z: number): THREE.Vector3 {
  return new THREE.Vector3(x, y, z);
}

const _x = new THREE.Vector3();
const _y = new THREE.Vector3();
const _z = new THREE.Vector3();
const _m = new THREE.Matrix4();

/**
 * Orientation for a held item: its blade points along `blade`, and its cutting
 * edge faces `edge` (re-orthogonalised against the blade).
 */
export function grip(blade: THREE.Vector3, edge: THREE.Vector3): THREE.Quaternion {
  _y.copy(blade).normalize();
  _z.copy(edge).addScaledVector(_y, -edge.dot(_y)).normalize();
  _x.crossVectors(_y, _z);
  _m.makeBasis(_x, _y, _z);
  return new THREE.Quaternion().setFromRotationMatrix(_m);
}

/** Orientation for a blade swung around `axis`: the edge leads the motion. */
export function swing(blade: THREE.Vector3, axis: THREE.Vector3): THREE.Quaternion {
  const edge = new THREE.Vector3().crossVectors(axis, blade);
  return grip(blade, edge);
}

/** Rotation axis that carries direction a toward direction b. */
export function swingAxis(a: THREE.Vector3, b: THREE.Vector3): THREE.Vector3 {
  return new THREE.Vector3().crossVectors(a.clone().normalize(), b.clone().normalize()).normalize();
}

/** Partial pose for authoring: unspecified fields come from a base pose. */
export type PoseDraft = Partial<Pose>;

export function makePose(base: Pose, draft: PoseDraft): Pose {
  const p = copyPose(emptyPose(), base);
  for (const key of Object.keys(draft) as (keyof Pose)[]) {
    const value = draft[key];
    if (value === undefined) continue;
    const target = p[key];
    if (typeof value === 'number') (p as unknown as Record<string, number>)[key] = value;
    else if (target instanceof THREE.Vector3) target.copy(value as THREE.Vector3);
    else if (target instanceof THREE.Quaternion) target.copy(value as THREE.Quaternion);
  }
  return p;
}

// ---- Clips -------------------------------------------------------------------

export type Ease = (t: number) => number;
export const ease = {
  linear: (t: number) => t,
  in: (t: number) => t * t * t,
  out: (t: number) => 1 - (1 - t) ** 3,
  inOut: (t: number) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2),
  /** Snappy strike: slow start, very fast middle, soft end. */
  strike: (t: number) => (t < 0.5 ? 16 * t ** 5 : 1 - (-2 * t + 2) ** 5 / 2),
  outBack: (t: number) => {
    const c = 1.9;
    return 1 + (c + 1) * (t - 1) ** 3 + c * (t - 1) ** 2;
  },
};

export interface Key {
  t: number;
  pose: Pose;
  /** Easing used while travelling INTO this key from the previous one. */
  ease?: Ease;
}

/** Keyframed clip. Hand positions use Catmull-Rom so swings travel in arcs. */
export class Clip {
  readonly duration: number;
  constructor(readonly keys: Key[], readonly loop = false) {
    this.duration = keys[keys.length - 1]!.t;
  }

  sample(time: number, out: Pose): Pose {
    const keys = this.keys;
    let t = this.loop ? ((time % this.duration) + this.duration) % this.duration : Math.min(Math.max(time, 0), this.duration);
    if (t <= keys[0]!.t) return copyPose(out, keys[0]!.pose);
    let i = 1;
    while (i < keys.length - 1 && keys[i]!.t < t) i++;
    const a = keys[i - 1]!;
    const b = keys[i]!;
    const span = b.t - a.t;
    const raw = span > 0 ? Math.min(1, (t - a.t) / span) : 1;
    const u = (b.ease ?? ease.inOut)(raw);
    blendPose(out, a.pose, b.pose, u);
    // Curve the hands through neighbouring keys so strikes sweep instead of sliding.
    const prev = keys[Math.max(0, i - 2)]!.pose;
    const next = keys[Math.min(keys.length - 1, i + 1)]!.pose;
    catmull(out.handR, prev.handR, a.pose.handR, b.pose.handR, next.handR, u);
    catmull(out.handL, prev.handL, a.pose.handL, b.pose.handL, next.handL, u);
    return out;
  }
}

function catmull(out: THREE.Vector3, p0: THREE.Vector3, p1: THREE.Vector3, p2: THREE.Vector3, p3: THREE.Vector3, t: number): void {
  const t2 = t * t;
  const t3 = t2 * t;
  for (const k of ['x', 'y', 'z'] as const) {
    out[k] =
      0.5 *
      (2 * p1[k] + (-p0[k] + p2[k]) * t + (2 * p0[k] - 5 * p1[k] + 4 * p2[k] - p3[k]) * t2 + (-p0[k] + 3 * p1[k] - 3 * p2[k] + p3[k]) * t3);
  }
}