diff --git a/context/locomotion.md b/context/locomotion.md index 4f93f34..b184b5f 100644 --- a/context/locomotion.md +++ b/context/locomotion.md @@ -129,15 +129,21 @@ real logic. `animationClips[4]` = the `trn`/`trni` clip (loaders 13767/13971). P was the body channel leaking out-of-phase joints into the rendered skeleton every walk cycle (mj=1); fixed at the source by `AdvanceBodyAnimation(dt, mj=0)` (see two-channel section). With mj=0 the authentic cut renders cleanly. -- **Turn rate (disasm 0x4aa3d3–0x4aa4ff) [T1]:** `spd = |linearSpeed|`; base `turnRate = - walkingTurnRate`(mech+0x574, rad/s). If `spd ≥ reverseSpeedMax`(0x538): `t = (spd − walkStride - 0x534)/(topSpeed 0x34c − walkStride)`; `turnRate = t≤1 ? lerp(walkingTurnRate, runningTurnRate - 0x578, t) : runningTurnRate/t²` (over-run falloff). `turnRate = max(0, turnRate)`. Then - `angularVelocity`(0x1d4) `= turnDemand(mapper+0x12c) × turnRate`, ZEROED if legState∈{1,2,3} or - latch 0x650 set. ⚠ **Not yet wired into the port** — the bring-up drive still yaws at the constant - `kDriveTurnRate` (mech4.cpp:2618/2628/3062); `walkingTurnRate`/`runningTurnRate` are stored - (mech.cpp:1334-5, Wword(0x15d)/(0x15e)→0x574/0x578, deg→rad) but only the ANIMATION dispatcher is - wired. Wiring the rate lerp is the next locomotion polish item. +- **Turn rate (disasm 0x4aa3d3–0x4aa4ff) [T1] — WIRED (task #64b):** `spd = |linearSpeed|`; base + `turnRate = walkingTurnRate`(mech+0x574, rad/s). If `spd ≥ reverseSpeedMax`(0x538): `t = (spd − + walkStride 0x534)/(topSpeed=reverseStrideLength 0x34c − walkStride)`; `turnRate = t≤1 ? + lerp(walkingTurnRate, runningTurnRate 0x578, t) : runningTurnRate/t²` (over-run falloff). `turnRate = + max(0, turnRate)`. Then yaw `= turnDemand(mapper+0x12c) × turnRate`, ZEROED if legState∈{1,2,3} or + the death latch (`deathAnimationLatched`@0x650) is set. **Note the lerp is a RUN-speed refinement** + — below `reverseSpeedMax` (walk / turn-in-place) turnRate is the constant walkTR base; the walk→run + interpolation only engages above it, and `runningTurnRate < walkingTurnRate` (a running mech is less + maneuverable). Port: `walkingTurnRate`/`runningTurnRate` are now REAL named members (mech.hpp, set + from the model deg→rad at mech.cpp:1334-5 — the old `Wword(0x15d/0x15e)` writes were no-ops into a + static scratch bank, so the values were silently discarded). The drive (mech4.cpp ~2617) computes + `authTurnRate` per the formula and yaws by it instead of the bring-up constant `kDriveTurnRate` + (kept as the fallback when a model supplies no rates). Diagnostic: `BT_TURN_LOG` → `[turnrate]`. + Verified headless (DEV mech): walkTR 1.309 rad/s (75°/s), runTR 0.873 (50°/s); turn-in-place + walk + yaw at walkTR, no crash. [T1 logic / T2 runtime] - **Entry** (leg Standing case, disasm 0x4aa505 [T1]): arm trn when TURNING (`|angVel|>1e-4`, port proxy = turnDemand deadband ±0.05) AND `0 ≤ spd ≤ standSpeed` (the FULL sub-walk range — CORRECTS the `0.25·standSpeed` stand-in) AND `turnCapable`(hasCrashSet@0x588). **LOCKSTEP: arms BOTH channels the diff --git a/game/reconstructed/mech.cpp b/game/reconstructed/mech.cpp index 7c66f2b..0f71b33 100644 --- a/game/reconstructed/mech.cpp +++ b/game/reconstructed/mech.cpp @@ -1325,14 +1325,13 @@ Mech::Mech( << " turnVel=" << updateTurnVelocityDeadband << " turnAngle=" << updateTurnAngleDeadband << " (rad)" << std::endl; Wword(0x10c) = model->timeDelay; // rec+0xA4 -> mech+0x430 - // NOTE: the decomp writes word slots this[0x15d]/this[0x15e]. Routed to the - // scratch bank (unnamed pose-angle fields) -- writing through (float*)(this + - // 0x15d) would be typed-pointer arithmetic on Mech* (== this + 0x15d*sizeof(Mech)). - // The AUTHENTIC per-mech turn rates (rec+0x60/0x64, deg->rad -> - // mech+0x574/0x578) -- still absorbed pending the turn-code wiring - // (locomotion polish item: the port turns at a bring-up constant rate). - Wword(0x15d) = model->walkingTurnRate * DegreesToRadians; - Wword(0x15e) = model->runningTurnRate * DegreesToRadians; + // The AUTHENTIC per-mech turn rates (rec+0x60/0x64, deg->rad -> mech+0x574/0x578). + // The decomp writes word slots this[0x15d]/this[0x15e] (== 0x574/0x578); stored in + // real named members now (task #64b) -- read by the master-perf turn-rate lerp in + // the drive (mech4.cpp) that yaws at lerp(walkingTurnRate, runningTurnRate) by speed + // instead of the old bring-up constant kDriveTurnRate. + walkingTurnRate = model->walkingTurnRate * DegreesToRadians; + runningTurnRate = model->runningTurnRate * DegreesToRadians; // // SHADOW JOINT resolve (task #20; CORRECTED -- the earlier draft misread diff --git a/game/reconstructed/mech.hpp b/game/reconstructed/mech.hpp index 3437cb2..31d4415 100644 --- a/game/reconstructed/mech.hpp +++ b/game/reconstructed/mech.hpp @@ -848,6 +848,8 @@ protected: Scalar jumpWalkSpeedMax; // @0x540 Scalar jumpRunStrideLength; // @0x544 Scalar jumpWalkStrideLength; // @0x548 + Scalar walkingTurnRate; // @0x574 (WalkingTurnRate, rad/s) -- the master + Scalar runningTurnRate; // @0x578 (RunningTurnRate, rad/s) perf turn-rate lerp Scalar groundCycleRate; // @0x5b8 Scalar airborneCycleRate; // @0x5bc // Forward-throttle scale read by the controls mapper (@004afd10: diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 4ae23f6..43e2364 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -2614,18 +2614,55 @@ void if (!gDriveSeeded) { gDriveHeading = 0.0f; gDriveSeeded = 1; } + // --- AUTHENTIC turn rate (master-perf disasm 0x4aa3d3-0x4aa4ff [T1]) --------- + // The yaw rate is per-mech lerp(walkingTurnRate, runningTurnRate) by GROUND + // SPEED, not the old bring-up constant kDriveTurnRate. spd = horizontal speed + // (last frame's worldLinearVelocity); base = walkingTurnRate; above the low + // gate (reverseSpeedMax@0x538) lerp walk->run across [walkStride 0x534 .. + // topSpeed 0x34c], with an over-run falloff runningTurnRate/t^2 past topSpeed; + // clamp >=0. angularVelocity = turnDemand * turnRate, ZEROED in the death/ + // limbo/airborne leg states (1/2/3) or when the death-anim latch is set. + // Falls back to the bring-up constant if the model supplied no rates. + Scalar authTurnRate = kDriveTurnRate; + if (walkingTurnRate > 0.0f) + { + const Scalar spd = (Scalar)sqrtf(worldLinearVelocity.x * worldLinearVelocity.x + + worldLinearVelocity.z * worldLinearVelocity.z); + authTurnRate = walkingTurnRate; // base (walk / idle / turn-in-place) + if (spd >= reverseSpeedMax) // 0x538 low gate + { + const Scalar den = reverseStrideLength - walkStrideLength; // topSpeed - walkStride + const Scalar t = (den != 0.0f) ? (spd - walkStrideLength) / den : 0.0f; + if (t <= 1.0f) + authTurnRate = walkingTurnRate + (runningTurnRate - walkingTurnRate) * t; // walk->run + else + authTurnRate = runningTurnRate / (t * t); // over-run falloff + } + if (authTurnRate < 0.0f) authTurnRate = 0.0f; + const int ls = legAnimationState; // 0x3b0 + if (deathAnimationLatched != 0 || ls == 1 || ls == 2 || ls == 3) + authTurnRate = 0.0f; // no yaw in death/limbo/airborne + } + if (getenv("BT_TURN_LOG")) + DEBUG_STREAM << "[turnrate] walkTR=" << walkingTurnRate + << " runTR=" << runningTurnRate << " spd=" + << (Scalar)sqrtf(worldLinearVelocity.x*worldLinearVelocity.x + + worldLinearVelocity.z*worldLinearVelocity.z) + << " -> authTurnRate=" << authTurnRate + << " (kDrive=" << kDriveTurnRate << ")\n" << std::flush; + // --- turn: integrate heading, write it onto the body orientation ----- - gDriveHeading += turn * kDriveTurnRate * dt; // scalar mirror (drive log + fallback) + gDriveHeading += turn * authTurnRate * dt; // scalar mirror (drive log + fallback) if (s_realControls) { // AUTHENTIC ORIENTATION INTEGRATION (the IntegrateMotion-tail form): // compose the yaw RATE into the pose quaternion via the engine // integrate op (Quaternion::Add == FUN_00409f58) -- the same op the // binary's motion tail uses -- instead of rebuilding the orientation - // from a scalar heading. The rate SCALE (kDriveTurnRate) remains a - // bring-up constant; the authentic per-mech turn-rate parameter is - // part of the deferred full-IntegrateMotion work. - Vector3D angStep(0.0f, turn * kDriveTurnRate * dt, 0.0f); + // from a scalar heading. The rate SCALE is now the AUTHENTIC per-mech + // turn rate (authTurnRate above) -- lerp(walkingTurnRate, runningTurnRate) + // by ground speed, master-perf 0x4aa3d3 [T1]. + Vector3D angStep(0.0f, turn * authTurnRate * dt, 0.0f); Quaternion prevPose = localOrigin.angularPosition; localOrigin.angularPosition.Add(prevPose, angStep); } @@ -3059,7 +3096,7 @@ void worldLinearVelocity.x * worldLinearVelocity.x + worldLinearVelocity.z * worldLinearVelocity.z); localVelocity.linearMotion = Vector3D(0.0f, worldLinearVelocity.y, -fwdSpeed); - localVelocity.angularMotion = Vector3D(0.0f, turn * kDriveTurnRate, 0.0f); + localVelocity.angularMotion = Vector3D(0.0f, turn * authTurnRate, 0.0f); // CRASH motion suppression (spec: the binary zeroes localVelocity while // crashed). While the stagger clip (legState 0x20) plays, freeze the