feat(physics): MotionInterpreter.GetMaxSpeed for InterpolationManager (L.3.1 Task 2)

Ports retail's CMotionInterp::get_max_speed (0x00527cb0). Returns
motion-table-derived max speed (m/s) for InterpretedState.ForwardCommand:
- RunForward:   RunAnimSpeed (4.0) × (InqRunRate ?? MyRunRate)
- WalkForward:  WalkAnimSpeed (3.12)
- WalkBackward: WalkAnimSpeed × 0.65 (BackwardsFactor from adjust_motion @ 0x00528010)
- otherwise:    0

Decomp note: Binary Ninja emits a spurious void return for x87 FPU-returning
functions; the actual float return is confirmed by both callers
(StickyManager::adjust_offset @ 0x00555430,
InterpolationManager::AdjustOffset @ 0x00555d52) which multiply the result
by 2.0 to produce a catch-up speed in m/s. The per-command switch is
consistent with get_state_velocity (0x00527d50) which uses the same constants.

Used by InterpolationManager.AdjustOffset in Task 5 as 2 × GetMaxSpeed().
Until Task 5 wires it, the method is unused — covered by 4 unit tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-02 19:16:38 +02:00
parent 927636ec77
commit 9c5634af17
2 changed files with 106 additions and 0 deletions

View file

@ -817,4 +817,60 @@ public sealed class MotionInterpreterTests
var vel = mi.get_state_velocity();
Assert.Equal(4.0f * 2.375f, vel.Y, precision: 2);
}
// =========================================================================
// GetMaxSpeed (CMotionInterp::get_max_speed @ 0x00527cb0)
// L.3.1 Task 2 — InterpolationManager catch-up speed source
// =========================================================================
[Fact]
public void GetMaxSpeed_RunForward_ReturnsRunAnimSpeedTimesRunRate()
{
// Retail: get_max_speed returns run rate from InqRunRate; callers
// multiply by 2 to get catch-up speed. For RunForward the per-m/s
// speed is RunAnimSpeed × rate = 4.0 × 1.5 = 6.0.
var weenie = new FakeWeenie { RunRate = 1.5f };
var interp = MakeInterp(weenie: weenie);
interp.InterpretedState.ForwardCommand = MotionCommand.RunForward;
float speed = interp.GetMaxSpeed();
Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.5f, speed, precision: 4); // 6.0
}
[Fact]
public void GetMaxSpeed_WalkForward_ReturnsWalkAnimSpeed()
{
// WalkForward max speed is always WalkAnimSpeed (3.12) — no run-rate scaling.
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward;
float speed = interp.GetMaxSpeed();
Assert.Equal(MotionInterpreter.WalkAnimSpeed, speed, precision: 4);
}
[Fact]
public void GetMaxSpeed_WalkBackward_ReturnsWalkAnimSpeedTimesBackwardsFactor()
{
// BackwardsFactor = 0.65, from adjust_motion @ 0x00528010 in the named retail decomp.
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.WalkBackward;
float speed = interp.GetMaxSpeed();
Assert.Equal(MotionInterpreter.WalkAnimSpeed * 0.65f, speed, precision: 4);
}
[Fact]
public void GetMaxSpeed_Idle_ReturnsZero()
{
// Ready / non-locomotion commands → 0 (no movement speed).
var interp = MakeInterp();
interp.InterpretedState.ForwardCommand = MotionCommand.Ready;
float speed = interp.GetMaxSpeed();
Assert.Equal(0f, speed, precision: 4);
}
}