diff --git a/context/multiplayer.md b/context/multiplayer.md index 9f88ccf..5de9eef 100644 --- a/context/multiplayer.md +++ b/context/multiplayer.md @@ -138,10 +138,30 @@ autofire → **B hdlr=192, DESTROYED** — a fully automated human-style cross-p (not the FORCE_DMG hook). Solo un-regressed (plain autodrive + goto both work). `scratchpad/mp_kill_test.sh`; `BT_GOTO_LOG` traces the beeline. +## Replicant gait animation (task #50, 2026-07-09) — DONE [T2] +A peer's replicant used to slide as a frozen statue (DeadReckon moved it; nothing animated it). Now the +replicant block (mech4.cpp, after `DeadReckon`) derives a SIGNED speed demand from the replicated +velocity — the master publishes `worldLinearVelocity` in every update record (`Mover::WriteUpdateRecord` +MOVER.cpp:759), the replicant stores it in `updateVelocity.linearMotion` (:712); sign = the forward dot +against the mech's -Z facing — writes it to `controlsMapper->speedDemand` (the LEG channel's live source) +and advances the LEG state machine for its JOINT writes only. Travel stays DeadReckon (the returned +distance is discarded) so position always follows the master; the clip cadence matches the replicated +speed. ⚠ REPLICANT GAIT-SCALAR PRIMING (same class as the master's per-frame primes, which live in the +PLAYER-only drive block): `globalTimeScale`/`idleStrideScale` read 0 on a replicant (clip frozen at +advance-time dt*0, legState engaged but legCycle stuck 0), and `reverseSpeedMax2`@0x7a0 (the run-cycle +rise-clamp; LoadLocomotionClips does NOT set it) read debug-heap garbage → legCycleSpeed clamped to +-4.3e8 entering run state 13. Both primed in the replicant block; `legCycleSpeed` itself sane-banded once. +VERIFIED 2-node: the observer's replicant runs the full lifecycle — stand→walk(5,7)→run(10,12) with +legCycle 7.5→34 tracking the master's speed, wind-down (8) and back to Standing(0) when the master stops. +Solo un-regressed. `BT_REPL_LOG` traces pos/vel/legState/legCycle; recipe `scratchpad/mp_gait_test.sh`. +NOTE: the update-record velocity is currently the master's ACTUAL travel velocity (worldLinearVelocity +from the leg channel); the binary publishes the BODY channel's smoothed projection (projectedVelocity, +disasm +0x2a0). Swapping the source is a fidelity refinement DEFERRED — it risks a verified working feed +for a smoothing nuance. [T3 on that nuance] + ## Remaining (P6 phase 4 / Phase 7) -Replicant GAIT animation (derive from replicated velocity); the pod-LAN config (real IPs, bare-IP pilot -entries). See -[[open-questions]]. [T3] +The pod-LAN config (real IPs, bare-IP pilot entries); update-record velocity sourced from the body-channel +projection (above). See [[open-questions]]. [T3] ## MP-front scout (task #45, 2026-07-09) — the P6 chain SURVIVES the combat/HUD rework [T2] Re-ran the one-box smoke test on the current build (world-pick targeting, weapon groups, death diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 13db2d2..04675f4 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -1194,6 +1194,51 @@ void { DeadReckon(dt); // engine: reckoner + lerp localToWorld = localOrigin; + + // REPLICANT GAIT (task #50): animate the peer mech's legs at the + // REPLICATED speed -- without this the replicant slides around as a + // frozen statue. The master publishes worldLinearVelocity in every + // update record (Mover::WriteUpdateRecord, MOVER.cpp:759); the + // replicant stores it in updateVelocity.linearMotion (:712). Derive + // the signed speed demand from it (forward = local -Z; a negative + // forward dot = the master is reversing), feed the LEG channel's live + // demand source, and advance the leg state machine for its JOINT + // writes only -- TRAVEL stays with DeadReckon (the returned distance + // is discarded), so position always follows the master and the clip + // cadence matches the replicated speed (residual foot-slip is the + // inherent dead-reckoning artifact). The state machine self-arms + // stand->walk / winds down from the demand exactly as on the master. + if (!IsMechDestroyed() && controlsMapper != 0) + { + const Vector3D &wv = updateVelocity.linearMotion; + float spd = sqrtf((float)(wv.x * wv.x + wv.z * wv.z)); + UnitVector zAxR; + localToWorld.GetFromAxis(Z_Axis, &zAxR); + const float fdot = -((float)wv.x * (float)zAxR.x + + (float)wv.z * (float)zAxR.z); // mech faces -Z + controlsMapper->speedDemand = (fdot < 0.0f) ? -spd : spd; + // Prime the same clip-advance scalars the master's gait block sets + // each frame -- uninitialized on a replicant they read 0, freezing + // the clip at advance-time dt*0 (observed: legState engaged at 11, + // legCycle stuck 0). Same forwardCycleRate floor as the master + // (the model-record decode reads ~1 u/s^2 -- a 55s ramp; floor 25). + globalTimeScale = 1.0f; + idleStrideScale = 1.0f; + if (forwardCycleRate < 25.0f) forwardCycleRate = 25.0f; + // reverseSpeedMax2@0x7a0: the run-cycle rise-CLAMP (leg case 12/13); + // LoadLocomotionClips doesn't set it, so a replicant reads 0xCDCD + // (-4.3e8) and the clamp CLOBBERS legCycleSpeed the moment the run + // cycle engages (observed live: legCycle=-4.31602e+08, state 13 + // stuck). Same heal as the master's drive block. + reverseSpeedMax2 = reverseStrideLength; + // legCycleSpeed itself is also debug-heap garbage until a walk + // state first writes it (Standing never touches it); sane-band it + // once so a direct run-state entry can't slew from -4.3e8. + if (legCycleSpeed < -100.0f || legCycleSpeed > 200.0f) + legCycleSpeed = 0.0f; + (void)AdvanceLegAnimation(dt); // joints only; travel = DeadReckon + } + if (getenv("BT_REPL_LOG")) { static float s_replLog = 0.0f; s_replLog += dt; @@ -1203,7 +1248,11 @@ void DEBUG_STREAM << "[repl] mech " << (long)GetEntityID() << " pos=(" << localOrigin.linearPosition.x << ", " << localOrigin.linearPosition.y << ", " - << localOrigin.linearPosition.z << ")\n" << std::flush; + << localOrigin.linearPosition.z << ")" + << " vel=(" << updateVelocity.linearMotion.x << "," + << updateVelocity.linearMotion.z << ")" + << " legState=" << (int)legStateAlarm.GetLevel() + << " legCycle=" << legCycleSpeed << "\n" << std::flush; } } }