MP: replicant gait animation -- peer mechs walk instead of sliding (task #50)

A peer's replicant mech slid around as a frozen statue: DeadReckon moved
localOrigin but nothing animated the legs.  Reconstruct the replicated-motion
display loop: after DeadReckon, derive 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 = forward dot against the -Z facing),
write it to controlsMapper->speedDemand (the LEG channel's live demand
source), and advance the LEG state machine for its JOINT writes only --
travel stays with DeadReckon, so position always follows the master and the
clip cadence matches the replicated speed.

Two replicant-only initialization holes found live (the master's equivalents
are primed per-frame in the PLAYER-only drive block):
- globalTimeScale/idleStrideScale read 0 -> clip advance time dt*0, the leg
  machine engaged (state 11) but the clip froze (legCycle stuck 0);
- reverseSpeedMax2 (the run-cycle rise-clamp @0x7a0; LoadLocomotionClips does
  NOT set it) read debug-heap garbage -> legCycleSpeed clamped to -4.3e8 on
  entering run state 13.  Primed both; legCycleSpeed sane-banded once.

VERIFIED 2-node (BT_GOTO driver + BT_REPL_LOG observer): the replicant runs
the full gait lifecycle -- stand -> walk(5,7) -> run(10,12), legCycle 7.5->34
tracking the master's speed, wind-down(8), Standing(0) when the master stops.
Solo un-regressed (goto->aim->kill chain intact, no crash).

projectedVelocity source-swap (the binary publishes the BODY channel's
smoothed projection instead of the actual travel velocity) evaluated and
DEFERRED: it risks a verified working replication feed for a smoothing
nuance; recorded in multiplayer.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-09 17:31:05 -05:00
co-authored by Claude Fable 5
parent 47aede3094
commit d09cda6c36
2 changed files with 73 additions and 4 deletions
+23 -3
View File
@@ -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
+50 -1
View File
@@ -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;
}
}
}