--- id: locomotion title: "Locomotion — gait, the ground model, collision, shadow" status: established source_sections: "PROGRESS_LOG.md §7, §10 (gait/ground/collision/shadow); docs/P3_LOCOMOTION.md" related_topics: [asset-formats, decomp-reference, reconstruction-gotchas, combat-damage, rendering] key_terms: [RootTranslation, ground-model, SequenceController, MechControlsMapper, SLD, joint] open_questions: - "Authentic per-mech turn-rate constant (currently bring-up constant)" - "Body-callback gimp handlers (states 16-19); airborne callbacks" --- # Locomotion Walking, the gait controller, the ground/collision model, and the shadow. All DEFAULT-ON now (`BT_GAIT_CUTOVER`, `BT_GAIT_SM`, `BT_COLLISION`, `BT_REAL_CONTROLS` — set `=0` to fall back). Full detail: `docs/P3_LOCOMOTION.md` + `docs/PROGRESS_LOG.md §7, §10`. ## Walk speed is animation-driven (not physics) Each `.ANI` has a `[RootTranslation]` where **`.z` = forward root SPEED** (world units/s). The engine `AnimationInstance::Animate(dt, move_joints)` integrates `movement += Δt·rootTrans.z` and moves the body that far → feet plant by construction (no stride constant; k=1). Idle anims have z=0. The `.MOD` physics governed thrust VEHICLES, NOT walk gait. [T1] ## The two-channel gait (real controls) The BT `SequenceController` (`seqctl.cpp`, from `SelectSequence@004277a8`+`Advance@0042790c`) drives locomotion. Under `BT_REAL_CONTROLS` two channels run: - **LEG channel** writes the skeleton JOINTS from the live demand (a local sim; its joint writes are overwritten by the body). - **BODY channel** is the DISPLAYED motion — travel comes from the SAME Advance that writes the displayed joints (`move_joints=1`), so display == travel BY CONSTRUCTION (v4 fix — killed the desync class). [T2] The gait STATE MACHINE (`AdvanceBodyAnimation`, mech2.cpp) slews `bodyCycleSpeed` toward the demand through walk/run caps (`LoadLocomotionClips`, mech3.cpp:326 — measures `walkStride`/`standSpeed` via `legAnimation.SelectSequence`). Transition callbacks (`Mech::BodyClipFinished @0x4a6d8c`, `LegClipFinished @0x4a6928` — PE-parsed jump tables) alternate walk 6↔7 / run 12↔13 + walk→run. [T2] ⚠ **Aliasing bug class (namedClip):** `namedClip[]`@0x5e0 == `animationClips[]`@0x5cc+0x14 (ONE array); `gimpBaseClip`@0x64c == `animationClips[0x20]`; `bodyAnimationState`@0x728 == `bodyStateAlarm.level`. Declared-separate members don't see each other's writes — alias them. ## The AUTHENTIC 1995 ground model (default-on, `BT_GROUND_REAL`) Decoded by the 10-agent `ground-model-decode` workflow (binary `FUN_004a9b5c` = the master perf). **NO GRAVITY anywhere** — accelerations zeroed every frame. Vertical position = an ABSOLUTE per-frame ground SNAP: 1. ctor lifts `collisionTemplate->minY` by 5% of volume X-width (the probe height + wall-vs-floor separation). 2. per frame `MoveCollisionVolume()` re-finds the containing node in the zone's **BoundingBoxTree** (BOXTREE.cpp — NOT a heightfield). 3. probe `q = origin + (0, lift, 0)` → `FindBoundingBoxUnder(q, &h)` (h = distance down to the highest solid top; −1 = miss). 4. snap gate `h > 1e-4`: `origin.y -= (h − lift)` ⇒ y = surfaceY exactly. **MISS ⇒ NOTHING** (y holds, no runaway). Up-slope bounded by the lift window (implicit step allowance); walk-offs drop instantly. 5. `GetCurrentCollisions` → `ProcessCollisionList` → `Mech::ProcessCollision` (@0x4abb40, vtbl+0x3c): BoxedSolid resolver → StaticBounce → owner classification. Crushable icon (flags&0x8000) = damage 0.00123f sentinel = move stands; damage>0 = **FULL FRAME REJECTION** (walls block, never slide); impactVel²>40 = crash-anim (SetLegAnimation(0x20)). [T2] KEY: `FindSmallestNodeContainingColumn`/`FindBoundingBoxUnder` (`FUN_0040e36c`/`0040e5f0`, BOXTREE.CPP) are ALIVE in the 2007 engine — the "heightfield" labels were wrong (caused two reverted attempts). A master mech needs a **CollisionAssistant** (GetCurrentCollisions iterates it unchecked). BT solids ship ZERO tiles (h never negative). [T2] ## Knockdown / crash + the desync/stutter fixes Wall impacts iv²>40 (~6.3 u/s) bind the `bmp` stagger clip. Two documented bug fixes: (a) the knockdown must stagger BOTH channels (`SetBodyAnimation(0x20)` + `SetLegAnimation(0x20)`) or display + travel split permanently (foot-slip); (b) a contact-hysteresis gate (0.4 s `gBlockCooldown`) stops the knockdown re-firing on sustained/glancing contact. `localVelocity` is zeroed while crashed (spec-faithful) so a knocked mech can't keep advancing. [T2] ## Controls (`BT_REAL_CONTROLS`, default-on) `MechControlsMapper` (mechmppr.cpp @004afbe0; btl4mppr.cpp mappers) interprets input → `speedDemand` / `turnDemand`. ⚠ **WndProc NEVER receives WM_KEYUP** (the engine's per-frame reader `GetMessage`s them out) → poll `GetAsyncKeyState` per frame, gated on foreground. Virtual controls: W/S sweep a persistent throttle LEVER with a zero detent; A/D a momentary auto-centering stick. [T2] ## Shadow (default-on, `BT_SHADOW_TILT`) BT 4.10's shadow is the flat `*_tshd.bgf` proxy drawn as blend-pass geometry, posed by `jointshadow` (terrain tilt) + `jointtshadow` (torso twist). Terrain-tilt samples the collision surface gradient; a depth-bias (`D3DRS_DEPTHBIAS`, decal-style) stops it z-fighting/vanishing on inclines. Polar maps have STUB `*_tshd` (shadow authored-off on snow). Also the visual-ground conform (`btvisgnd.cpp`, render-only lift to the visible terrain triangles — presentation only, never touches localOrigin). [T2] ## Key Relationships - Detail: `docs/P3_LOCOMOTION.md`. Uses: [[asset-formats]] (SKL/ANI), [[decomp-reference]] (offsets). - Feeds: [[combat-damage]] (collision→damage), [[rendering]] (shadow/visual-conform).