// Frost Mage staff: sculpted archmage staff model + per-frame grip and cast-pose controller.
// Keeps the staff near-vertical in idle/walk/run (fist ~1/3 down the shaft, crystal head at/above head height,
// shaft clear of body, cape and ground) and lifts/thrusts the crystal head toward the target during casts.
import * as THREE from "three";
import type { Walker } from "@/lib/cast/actors/walker";
import type { CastState } from "./combat/sim";

export interface StaffStateSource {
  cast: CastState | null;
  dead: boolean;
  speed?: number;
}

export interface MageStaff {
  /** Root group passed to `walker.hold(staff.root, "hand_r")`. */
  root: THREE.Group;
  /** The crystal head group (VFX anchor: read its world position to gather frost energy there). */
  head: THREE.Object3D;
  /** Notify when a cast-time spell starts channeling. */
  onCastStart(id?: string, duration?: number): void;
  /** Notify when any spell releases (instant or end of cast). */
  onRelease(id?: string): void;
  /** Notify when a cast is cancelled/interrupted. */
  onCancel(): void;
  /** Per-frame update (automatically hooked onto `walker.update` by `attachMageStaff`). */
  update(dt: number, state?: StaffStateSource | null): void;
}

// Default static grip rotation inside `socket-hand_r` so even before the first `update()` tick
// (or on a static mannequin in Idle_Loop) the staff stands upright instead of lying along the forearm.
// Measured against the UBC rig's Idle_Loop `socket-hand_r` axes:
//   socket xAxis = [-0.986,  0.163, -0.041]
//   socket yAxis = [ 0.153,  0.765, -0.625]
//   socket zAxis = [-0.070, -0.623, -0.779]
export const STAFF_STATIC_SOCKET_ROT: [number, number, number] = [-0.82, 0.08, 0.22];
export const STAFF_STATIC_SOCKET_POS: [number, number, number] = [0, 0.01, 0.02];

const SHAFT_BOTTOM_Y = -0.92; // bottom tip of metal ferrule below fist (m)
const MIN_GROUND_CLEARANCE = 0.10; // minimum world height above hero root (m)

export function createMageStaff(): MageStaff {
  const root = new THREE.Group();
  root.name = "prop-mage-staff";
  root.position.set(...STAFF_STATIC_SOCKET_POS);
  root.rotation.set(...STAFF_STATIC_SOCKET_ROT);

  // Inner group shifted along +Y only when ground-clearance guard needs it
  const body = new THREE.Group();
  body.name = "staff-body";
  root.add(body);

  // Pre-created materials (warmed by warmShaders at load, zero mid-game shader compiles)
  const woodDarkMat = new THREE.MeshStandardMaterial({ color: "#5e3c24", roughness: 0.78, metalness: 0.05 });
  const woodLightMat = new THREE.MeshStandardMaterial({ color: "#7c5233", roughness: 0.72, metalness: 0.06 });
  const gripLeatherMat = new THREE.MeshStandardMaterial({ color: "#182238", roughness: 0.85, metalness: 0.08 });
  const metalSilverMat = new THREE.MeshStandardMaterial({ color: "#b8cde2", roughness: 0.28, metalness: 0.82 });
  const metalGoldMat = new THREE.MeshStandardMaterial({ color: "#ebb844", roughness: 0.26, metalness: 0.84 });
  const runeGlowMat = new THREE.MeshStandardMaterial({
    color: "#7ceeff",
    emissive: "#48d8ff",
    emissiveIntensity: 1.9,
    roughness: 0.2,
  });
  const crystalMat = new THREE.MeshStandardMaterial({
    color: "#7be8ff",
    emissive: "#29b6f6",
    emissiveIntensity: 1.55,
    roughness: 0.14,
    metalness: 0.12,
    flatShading: true,
  });
  const coreMat = new THREE.MeshStandardMaterial({
    color: "#ffffff",
    emissive: "#9cf6ff",
    emissiveIntensity: 2.8,
    roughness: 0.1,
  });
  const shardMat = new THREE.MeshStandardMaterial({
    color: "#ccfaff",
    emissive: "#5fe0ff",
    emissiveIntensity: 2.1,
    roughness: 0.15,
    metalness: 0.1,
    flatShading: true,
  });

  const shadow = (m: THREE.Mesh) => {
    m.castShadow = true;
    m.receiveShadow = true;
    return m;
  };

  // 1. Bottom frost-iron ferrule + spike (y = -0.92 .. -0.78)
  const ferruleSpike = shadow(new THREE.Mesh(new THREE.ConeGeometry(0.022, 0.09, 6), metalSilverMat));
  ferruleSpike.rotation.x = Math.PI;
  ferruleSpike.position.y = -0.875;
  const ferruleCollar = shadow(new THREE.Mesh(new THREE.CylinderGeometry(0.025, 0.022, 0.06, 8), metalSilverMat));
  ferruleCollar.position.y = -0.80;
  const ferruleRing = shadow(new THREE.Mesh(new THREE.TorusGeometry(0.026, 0.006, 6, 12), metalGoldMat));
  ferruleRing.rotation.x = Math.PI / 2;
  ferruleRing.position.y = -0.77;
  body.add(ferruleSpike, ferruleCollar, ferruleRing);

  // 2. Lower carved wooden shaft (y = -0.77 .. -0.13, 0.64 m)
  const lowerShaftA = shadow(new THREE.Mesh(new THREE.CylinderGeometry(0.028, 0.023, 0.34, 8), woodDarkMat));
  lowerShaftA.position.y = -0.60;
  const lowerShaftB = shadow(new THREE.Mesh(new THREE.CylinderGeometry(0.031, 0.028, 0.30, 8), woodLightMat));
  lowerShaftB.position.y = -0.28;
  const midBand = shadow(new THREE.Mesh(new THREE.CylinderGeometry(0.032, 0.031, 0.038, 8), metalSilverMat));
  midBand.position.y = -0.43;
  const midRune = new THREE.Mesh(new THREE.TorusGeometry(0.032, 0.0045, 6, 12), runeGlowMat);
  midRune.rotation.x = Math.PI / 2;
  midRune.position.y = -0.43;
  body.add(lowerShaftA, lowerShaftB, midBand, midRune);

  // 3. Grip wrap around the fist (y = -0.13 .. +0.13, centered at y = 0)
  const grip = shadow(new THREE.Mesh(new THREE.CylinderGeometry(0.033, 0.032, 0.26, 8), gripLeatherMat));
  grip.position.y = 0;
  const gripBandBot = shadow(new THREE.Mesh(new THREE.TorusGeometry(0.034, 0.0065, 6, 12), metalGoldMat));
  gripBandBot.rotation.x = Math.PI / 2;
  gripBandBot.position.y = -0.125;
  const gripBandTop = shadow(new THREE.Mesh(new THREE.TorusGeometry(0.035, 0.0065, 6, 12), metalGoldMat));
  gripBandTop.rotation.x = Math.PI / 2;
  gripBandTop.position.y = 0.125;
  body.add(grip, gripBandBot, gripBandTop);

  // 4. Upper carved shaft (y = +0.13 .. +0.47, 0.34 m -> fist is 1/3 down the wooden shaft)
  const upperShaft = shadow(new THREE.Mesh(new THREE.CylinderGeometry(0.039, 0.033, 0.34, 8), woodLightMat));
  upperShaft.position.y = 0.30;
  const upperBand = shadow(new THREE.Mesh(new THREE.CylinderGeometry(0.041, 0.039, 0.036, 8), metalSilverMat));
  upperBand.position.y = 0.32;
  const upperRune = new THREE.Mesh(new THREE.TorusGeometry(0.041, 0.005, 6, 14), runeGlowMat);
  upperRune.rotation.x = Math.PI / 2;
  upperRune.position.y = 0.32;
  body.add(upperShaft, upperBand, upperRune);

  // 5. Ornate archmage crown collar & crescent prongs (y = +0.46 .. +0.68)
  const crownCollar = shadow(new THREE.Mesh(new THREE.CylinderGeometry(0.058, 0.040, 0.065, 8), metalGoldMat));
  crownCollar.position.y = 0.49;
  const crownNeck = shadow(new THREE.Mesh(new THREE.CylinderGeometry(0.044, 0.056, 0.04, 8), metalSilverMat));
  crownNeck.position.y = 0.535;
  // Crescent cradle around the lower half of the frost crystal
  const cradleArc = shadow(new THREE.Mesh(new THREE.TorusGeometry(0.092, 0.015, 6, 16, Math.PI * 1.28), metalSilverMat));
  cradleArc.position.y = 0.635;
  cradleArc.rotation.z = -Math.PI * 1.14;
  // Two sculpted side prongs framing the crystal
  const prongGeo = new THREE.ConeGeometry(0.018, 0.14, 5);
  const prongL = shadow(new THREE.Mesh(prongGeo, metalGoldMat));
  prongL.position.set(0.088, 0.67, 0);
  prongL.rotation.z = 0.22;
  const prongR = shadow(new THREE.Mesh(prongGeo, metalGoldMat));
  prongR.position.set(-0.088, 0.67, 0);
  prongR.rotation.z = -0.22;
  body.add(crownCollar, crownNeck, cradleArc, prongL, prongR);

  // 6. Frost crystal head + emissive inner core + floating ice shards (y = +0.55 .. +0.81)
  const headGroup = new THREE.Group();
  headGroup.position.y = 0.64;
  body.add(headGroup);

  const core = new THREE.Mesh(new THREE.OctahedronGeometry(0.046, 0), coreMat);
  core.scale.set(1, 1.35, 1);
  headGroup.add(core);

  const crystal = shadow(new THREE.Mesh(new THREE.OctahedronGeometry(0.082, 0), crystalMat));
  crystal.scale.set(0.86, 1.48, 0.78);
  headGroup.add(crystal);

  const shardRing = new THREE.Group();
  headGroup.add(shardRing);

  const shardGeo = new THREE.OctahedronGeometry(0.025, 0);
  const orbitShards: { mesh: THREE.Mesh; baseAngle: number; baseY: number; tilt: number }[] = [];
  for (let i = 0; i < 3; i++) {
    const sm = shadow(new THREE.Mesh(shardGeo, shardMat));
    sm.scale.set(0.75, 2.25, 0.68);
    const a = (i * Math.PI * 2) / 3;
    const r = 0.118;
    const by = (i - 1) * 0.022;
    sm.position.set(Math.cos(a) * r, by, Math.sin(a) * r);
    sm.rotation.order = "YXZ";
    sm.rotation.y = -a + Math.PI / 2;
    sm.rotation.x = 0.28;
    shardRing.add(sm);
    orbitShards.push({ mesh: sm, baseAngle: a, baseY: by, tilt: 0.28 });
  }
  // Crown apex floating shard above the main crystal
  const crownShard = shadow(new THREE.Mesh(shardGeo, shardMat));
  crownShard.scale.set(0.72, 2.1, 0.72);
  crownShard.position.set(0, 0.135, 0);
  headGroup.add(crownShard);

  // Animation state (zero per-frame heap allocations)
  let time = 0;
  let castChannel = 0; // 0..1 smoothed channeling blend
  let releaseT = -1; // >= 0 while a release thrust is active
  let releaseDur = 0.48;
  let lastCastActive = false;
  let smoothedPitch = 0.13;
  let smoothedRoll = 0.04;
  let smoothedArmCarry = 1;
  let prevHandZ = 0;
  let initialized = false;

  // Scratch math objects
  const qRoot = new THREE.Quaternion();
  const qRootInv = new THREE.Quaternion();
  const qSock = new THREE.Quaternion();
  const qWantHero = new THREE.Quaternion();
  const qTargetLocal = new THREE.Quaternion();
  const eulerHero = new THREE.Euler(0, 0, 0, "YXZ");
  const vHandWorld = new THREE.Vector3();
  const vHandHero = new THREE.Vector3();
  const qArmDelta = new THREE.Quaternion();
  const qIdentity = new THREE.Quaternion();
  const vCurBoneY = new THREE.Vector3();
  const vWantBoneY = new THREE.Vector3();
  const vBoneA = new THREE.Vector3();
  const vBoneB = new THREE.Vector3();
  const qBoneWorld = new THREE.Quaternion();
  const qParentWorld = new THREE.Quaternion();
  const qNeutralHand = new THREE.Quaternion();
  let hasNeutralHand = false;

  // Cached bone lookups on the walker
  let cachedRoot: THREE.Object3D | null = null;
  let uarm: THREE.Object3D | null = null;
  let larm: THREE.Object3D | null = null;
  let hand: THREE.Object3D | null = null;
  let sock: THREE.Object3D | null = null;
  let lastHeroX = NaN;
  let lastHeroZ = NaN;
  let measuredSpeed = 0;

  function findBones(walkerRoot: THREE.Object3D) {
    if (cachedRoot !== walkerRoot || !uarm || !larm || !hand || !sock) {
      cachedRoot = walkerRoot;
      uarm = walkerRoot.getObjectByName("upperarm_r") ?? null;
      larm = walkerRoot.getObjectByName("lowerarm_r") ?? null;
      hand = walkerRoot.getObjectByName("hand_r") ?? null;
      sock = walkerRoot.getObjectByName("socket-hand_r") ?? root.parent;
    }
  }

  /** Steer a bone's +Y shaft axis in hero-root space toward `wantDirHero` with blend `weight` (0..1). */
  function aimBoneYInHeroSpace(
    bone: THREE.Object3D,
    heroQ: THREE.Quaternion,
    heroQInv: THREE.Quaternion,
    wx: number,
    wy: number,
    wz: number,
    weight: number,
  ) {
    const parent = bone.parent;
    if (!parent || weight <= 0.001) return;
    parent.updateWorldMatrix(true, false);
    bone.updateMatrix();
    bone.matrixWorld.multiplyMatrices(parent.matrixWorld, bone.matrix);
    bone.getWorldQuaternion(qBoneWorld);
    // Current bone rotation in hero-root space
    qBoneWorld.premultiply(heroQInv);
    vCurBoneY.set(0, 1, 0).applyQuaternion(qBoneWorld).normalize();
    vWantBoneY.set(wx, wy, wz).normalize();
    qArmDelta.setFromUnitVectors(vCurBoneY, vWantBoneY);
    if (weight < 0.999) qArmDelta.slerp(qIdentity, 1 - weight);
    // Apply in hero space, then convert back to parent-local space
    qBoneWorld.premultiply(qArmDelta).premultiply(heroQ);
    parent.getWorldQuaternion(qParentWorld).invert();
    bone.quaternion.copy(qParentWorld).multiply(qBoneWorld).normalize();
    bone.updateMatrix();
    bone.matrixWorld.multiplyMatrices(parent.matrixWorld, bone.matrix);
  }

  const api: MageStaff = {
    root,
    head: headGroup,
    onCastStart(_id, dur = 1.5) {
      void dur;
      lastCastActive = true;
    },
    onRelease() {
      releaseT = 0;
      releaseDur = 0.48;
    },
    onCancel() {
      lastCastActive = false;
    },
    update(dt, state) {
      const step = Math.min(Math.max(dt, 0), 0.1);
      time += step > 0 ? step : 0.016;

      // Track cast channeling & release state
      const castingNow = Boolean(state?.cast);
      const isChannelSpell = state?.cast?.kind === "channel";
      if (lastCastActive && !castingNow && releaseT < 0) {
        // A cast-time spell just finished -> pulse release thrust if not already triggered
        releaseT = 0;
      }
      lastCastActive = castingNow;

      const targetChannel = castingNow ? 1 : 0;
      const chRate = step > 0 ? Math.min(1, step * 10) : 1;
      castChannel += (targetChannel - castChannel) * chRate;

      let thrust = 0; // 0..1 release thrust curve
      if (releaseT >= 0) {
        releaseT += step > 0 ? step : 0.03;
        if (releaseT >= releaseDur) {
          releaseT = -1;
          thrust = 0;
        } else {
          // Fast attack (0 -> 0.14 s), smooth hold & return (0.14 -> 0.48 s)
          if (releaseT < 0.14) {
            const u = releaseT / 0.14;
            thrust = u * (2 - u);
          } else {
            const u = (releaseT - 0.14) / (releaseDur - 0.14);
            thrust = 1 - u * u * (3 - 2 * u);
          }
        }
      }

      const castWeight = Math.max(castChannel * (isChannelSpell ? 0.94 : 0.78), thrust);

      // Animate crystal head & floating shards
      const spinSpeed = 1.3 + castWeight * 4.2;
      shardRing.rotation.y += spinSpeed * (step > 0 ? step : 0.016);
      crystal.rotation.y -= 0.45 * (step > 0 ? step : 0.016);
      for (let i = 0; i < orbitShards.length; i++) {
        const s = orbitShards[i];
        s.mesh.position.y = s.baseY + Math.sin(time * 3.2 + s.baseAngle) * 0.012;
      }
      crownShard.position.y = 0.135 + Math.sin(time * 2.6) * 0.01;
      crownShard.rotation.y = time * 1.8;
      coreMat.emissiveIntensity = 2.6 + Math.sin(time * 4.5) * 0.35 + castWeight * 1.8;
      crystalMat.emissiveIntensity = 1.45 + castWeight * 0.9;

      // Find walker root from parent chain
      let walkerRoot: THREE.Object3D | null = root.parent;
      while (walkerRoot && !walkerRoot.name.startsWith("walker-")) {
        walkerRoot = walkerRoot.parent;
      }
      if (!walkerRoot) return;

      findBones(walkerRoot);
      if (!sock) return;

      // Measure ground speed from walkerRoot movement or commanded walker speed
      if (step > 0 && lastHeroX === lastHeroX) {
        const d = Math.hypot(walkerRoot.position.x - lastHeroX, walkerRoot.position.z - lastHeroZ) / step;
        const cmd = state?.speed ?? 0;
        const eff = Math.max(d, cmd);
        measuredSpeed += (Math.min(eff, 10) - measuredSpeed) * Math.min(1, step * 10);
      } else if (state?.speed !== undefined) {
        measuredSpeed = state.speed;
      }
      lastHeroX = walkerRoot.position.x;
      lastHeroZ = walkerRoot.position.z;
      const runFactor = Math.max(0, Math.min(1, measuredSpeed / 4.8));

      walkerRoot.updateWorldMatrix(true, false);
      walkerRoot.getWorldQuaternion(qRoot);
      qRootInv.copy(qRoot).invert();

      const dead = Boolean(state?.dead);
      const targetCarry = dead ? 0 : 1;
      smoothedArmCarry += (targetCarry - smoothedArmCarry) * (step > 0 ? Math.min(1, step * 8) : 1);

      // 1. Pose the right arm into a natural mage staff-carrying / casting stance
      //    Steers upperarm_r (+Y = shoulder->elbow) and lowerarm_r (+Y = elbow->wrist) in hero-root space so:
      //    - In idle/run: the fist sits in front of the right waist (x ≈ -0.27 m, y ≈ 1.06 m, z ≈ +0.16 m),
      //      with subtle stride bob in run.
      //    - In cast (channeling or release thrust): the right arm reaches forward in front of the chest
      //      (x ≈ -0.25 m, y ≈ 1.32 m, z ≈ +0.36 m) to lift and thrust the staff head toward the target.
      if (uarm && larm && smoothedArmCarry > 0.01) {
        if (hand && !hasNeutralHand) {
          qNeutralHand.copy(hand.quaternion);
          hasNeutralHand = true;
        }
        const locoBlend = (1 - runFactor * 0.28) * (1 - castWeight) + castWeight;
        const wArm = smoothedArmCarry * locoBlend;

        // Upper arm target direction in hero space: carry [-0.28, -0.94, +0.16] -> cast [-0.36, -0.32, +0.88] / channel [-0.30, +0.10, +0.95]
        vBoneA.set(-0.28, -0.94, 0.16).normalize();
        if (isChannelSpell) {
          vBoneB.set(-0.30, 0.10 + Math.sin(time * 5.5) * 0.05, 0.95).normalize();
        } else {
          vBoneB.set(-0.34, -0.32, 0.88).normalize();
        }
        vBoneA.lerp(vBoneB, castWeight).normalize();
        aimBoneYInHeroSpace(uarm, qRoot, qRootInv, vBoneA.x, vBoneA.y, vBoneA.z, wArm);

        // Forearm target direction in hero space: carry [-0.14, -0.28, +0.95] -> cast [-0.16, +0.26, +0.95] / channel [-0.12, +0.62, +0.77]
        const wForearm = smoothedArmCarry * ((1 - runFactor * 0.18) * (1 - castWeight) + castWeight);
        vBoneA.set(-0.14, -0.28, 0.95).normalize();
        if (isChannelSpell) {
          vBoneB.set(-0.12, 0.62 + Math.sin(time * 5.5) * 0.06, 0.77).normalize();
        } else {
          vBoneB.set(-0.16, 0.26, 0.95).normalize();
        }
        vBoneA.lerp(vBoneB, castWeight).normalize();
        aimBoneYInHeroSpace(larm, qRoot, qRootInv, vBoneA.x, vBoneA.y, vBoneA.z, wForearm);

        // Keep the wrist (hand_r) in a firm neutral grip on the shaft (prevents OverhandThrow wrist twist)
        if (hand && hasNeutralHand) {
          hand.quaternion.slerp(qNeutralHand, smoothedArmCarry * 0.88);
          hand.updateMatrix();
          if (hand.parent) hand.matrixWorld.multiplyMatrices(hand.parent.matrixWorld, hand.matrix);
        }
      }

      // Update socket world matrix after arm adjustment
      sock.updateWorldMatrix(true, false);
      sock.getWorldQuaternion(qSock);
      vHandWorld.setFromMatrixPosition(sock.matrixWorld);
      vHandHero.copy(vHandWorld).sub(walkerRoot.position).applyQuaternion(qRootInv);

      // Hand stride sway (subtle, natural motion from the hand's Z oscillation during walk/run)
      const handZDelta = initialized ? Math.max(-0.18, Math.min(0.18, vHandHero.z - prevHandZ)) : 0;
      prevHandZ = vHandHero.z;
      const strideSway = (vHandHero.z - 0.15) * 0.18 * runFactor + handZDelta * 0.15;

      // 2. Desired staff orientation in hero-root space:
      //    +X rotation pitches the top of the staff (+Y) forward (+Z) toward the target.
      //    +Z rotation tilts the top of the staff slightly outward (-X, to the hero's right) away from the head/cape.
      const idlePitch = 0.11 + Math.sin(time * 1.7) * 0.015; // ~6.3 deg forward in idle
      const runPitch = 0.19 + strideSway; // ~11 deg forward + subtle stride sway in run
      const locoPitch = idlePitch * (1 - runFactor) + runPitch * runFactor;
      const castPitch = 0.44 * castChannel + (0.72 - 0.44 * castChannel) * thrust; // ~25 deg channel, ~41 deg thrust
      const wantPitch = locoPitch * (1 - castWeight) + castPitch;

      const wantRoll = 0.02 * (1 - castWeight) - 0.06 * castWeight; // keeps lower shaft clear of right hip during cast thrust

      if (!initialized || step === 0) {
        smoothedPitch = wantPitch;
        smoothedRoll = wantRoll;
        initialized = true;
      } else {
        const k = Math.min(1, step * 18);
        smoothedPitch += (wantPitch - smoothedPitch) * k;
        smoothedRoll += (wantRoll - smoothedRoll) * k;
      }

      if (dead) {
        // When fallen, relax back toward default socket grip
        root.rotation.set(...STAFF_STATIC_SOCKET_ROT);
        body.position.y = 0;
        return;
      }

      eulerHero.set(smoothedPitch, 0, smoothedRoll, "YXZ");
      qWantHero.setFromEuler(eulerHero);

      // Convert desired hero-space rotation into socket-local space:
      //   qWorldWant = qRoot * qWantHero
      //   qLocal = inverse(qSock) * qWorldWant
      qTargetLocal.copy(qSock).invert().multiply(qRoot).multiply(qWantHero).normalize();
      root.quaternion.copy(qTargetLocal);

      // 3. Ground-clearance guard: keep bottom ferrule at least MIN_GROUND_CLEARANCE above hero root y=0
      const cosP = Math.cos(smoothedPitch) * Math.cos(smoothedRoll);
      const bottomHeroY = vHandHero.y + SHAFT_BOTTOM_Y * cosP;
      if (bottomHeroY < MIN_GROUND_CLEARANCE) {
        body.position.y = (MIN_GROUND_CLEARANCE - bottomHeroY) / Math.max(0.4, cosP);
      } else {
        body.position.y = 0;
      }
    },
  };

  return api;
}

/**
 * Attach the mage staff to the hero walker and hook `walker.update` / `walker.act`
 * so the staff stays near-vertical in idle/run and lifts/thrusts on every cast,
 * whether driven by the game loop or by debug/test hooks.
 */
export function attachMageStaff(walker: Walker, getState?: () => StaffStateSource | null): MageStaff {
  const staff = createMageStaff();
  const sock = walker.hold(staff.root, "hand_r");
  // Attach socket to walker.root initially so warmShaders compiles staff materials even before GLB body resolves
  if (!sock.parent) walker.root.add(sock);

  const origUpdate = walker.update.bind(walker);
  const origAct = walker.act.bind(walker);
  const origGo = walker.go.bind(walker);
  const origSetSpeed = walker.setSpeed.bind(walker);
  let cmdSpeed = 0;

  walker.go = (mps: number) => {
    cmdSpeed = mps;
    origGo(mps);
  };
  walker.setSpeed = (mps: number) => {
    cmdSpeed = mps;
    origSetSpeed(mps);
  };

  walker.act = (name) => {
    const len = origAct(name);
    if (name === "throw" || name === "interact" || name === "punch" || name === "hook") {
      staff.onRelease(name);
    }
    return len;
  };

  walker.update = (dt: number) => {
    origUpdate(dt);
    const st = getState ? getState() : null;
    const locoActive = walker.state === "walk" || walker.state === "jog" || walker.state === "run";
    staff.update(dt, {
      cast: st?.cast ?? null,
      dead: st?.dead ?? false,
      speed: locoActive ? cmdSpeed : 0,
    });
  };

  // Also pose immediately once the walker's rig is ready
  void walker.ready.then(() => {
    staff.update(0, getState ? getState() : null);
  });

  return staff;
}
