using System; using System.Numerics; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; // ───────────────────────────────────────────────────────────────────────────── // MotionInterpreterTests — covers the 10 ported methods. // // Source addresses tested: // FUN_00529a90 PerformMovement // FUN_00529930 DoMotion // FUN_00528a50 StopCompletely // FUN_00528960 get_state_velocity // FUN_00529390 jump // FUN_005286b0 get_jump_v_z // FUN_00528cd0 get_leave_ground_velocity // FUN_00528ec0 jump_is_allowed // FUN_00528dd0 contact_allows_move // FUN_00529210 apply_current_movement // ───────────────────────────────────────────────────────────────────────────── /// /// Fake WeenieObject for test isolation — all methods return configurable values. /// file sealed class FakeWeenie : IWeenieObject { public float RunRate = 1.0f; public float JumpVz = 10.0f; public bool CanJumpResult = true; public bool InqRunRateResult = true; public bool InqJumpVelocityResult = true; public bool InqJumpVelocity(float extent, out float vz) { vz = JumpVz * extent; return InqJumpVelocityResult; } public bool InqRunRate(out float rate) { rate = RunRate; return InqRunRateResult; } public bool CanJump(float extent) => CanJumpResult; } public sealed class MotionInterpreterTests { // ── helpers ─────────────────────────────────────────────────────────────── private static PhysicsBody MakeGrounded() { var body = new PhysicsBody { State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions, }; body.TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable | TransientStateFlags.Active; return body; } private static PhysicsBody MakeAirborne() { var body = new PhysicsBody { State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions, }; body.TransientState = TransientStateFlags.Active; return body; } private static MotionInterpreter MakeInterp(PhysicsBody? body = null, IWeenieObject? weenie = null) { body ??= MakeGrounded(); return new MotionInterpreter(body, weenie); } // ========================================================================= // PerformMovement (FUN_00529a90) — dispatcher // ========================================================================= [Theory] [InlineData(MovementType.RawCommand)] [InlineData(MovementType.InterpretedCommand)] [InlineData(MovementType.StopRawCommand)] [InlineData(MovementType.StopInterpretedCommand)] [InlineData(MovementType.StopCompletely)] public void PerformMovement_ValidTypes_ReturnNone(MovementType type) { var interp = MakeInterp(); var mvs = new MovementStruct { Type = type, Motion = MotionCommand.WalkForward, Speed = 1.0f, ModifyInterpretedState = true, ModifyRawState = false, }; var result = interp.PerformMovement(mvs); Assert.Equal(WeenieError.None, result); } [Fact] public void PerformMovement_InvalidType_ReturnsGeneralFailure() { var interp = MakeInterp(); var mvs = new MovementStruct { Type = (MovementType)99, Motion = MotionCommand.WalkForward }; var result = interp.PerformMovement(mvs); Assert.Equal(WeenieError.GeneralMovementFailure, result); } [Fact] public void PerformMovement_StopCompletely_ResetsToReady() { var interp = MakeInterp(); interp.RawState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; var mvs = new MovementStruct { Type = MovementType.StopCompletely }; interp.PerformMovement(mvs); Assert.Equal(MotionCommand.Ready, interp.RawState.ForwardCommand); Assert.Equal(MotionCommand.Ready, interp.InterpretedState.ForwardCommand); } [Fact] public void PerformMovement_NullPhysicsObj_ReturnsNoPhysicsObject() { var interp = new MotionInterpreter(); // no PhysicsObj var mvs = new MovementStruct { Type = MovementType.RawCommand, Motion = MotionCommand.WalkForward }; var result = interp.PerformMovement(mvs); Assert.Equal(WeenieError.NoPhysicsObject, result); } // ========================================================================= // DoMotion (FUN_00529930) — raw motion command sets RawState // ========================================================================= [Fact] public void DoMotion_WalkForward_SetsRawForwardCommand() { var interp = MakeInterp(); interp.DoMotion(MotionCommand.WalkForward, 0.8f); Assert.Equal(MotionCommand.WalkForward, interp.RawState.ForwardCommand); Assert.Equal(0.8f, interp.RawState.ForwardSpeed, precision: 5); } [Fact] public void DoMotion_NullPhysicsObj_ReturnsNoPhysicsObject() { var interp = new MotionInterpreter(); var result = interp.DoMotion(MotionCommand.WalkForward); Assert.Equal(WeenieError.NoPhysicsObject, result); } // ========================================================================= // StopCompletely (FUN_00528a50) // ========================================================================= [Fact] public void StopCompletely_ResetsRawAndInterpretedToReady() { var interp = MakeInterp(); interp.RawState.ForwardCommand = MotionCommand.WalkForward; interp.RawState.SideStepCommand = MotionCommand.SideStepRight; interp.RawState.TurnCommand = MotionCommand.TurnRight; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.TurnCommand = MotionCommand.TurnLeft; var result = interp.StopCompletely(); Assert.Equal(WeenieError.None, result); Assert.Equal(MotionCommand.Ready, interp.RawState.ForwardCommand); Assert.Equal(1.0f, interp.RawState.ForwardSpeed, precision: 5); Assert.Equal(0u, interp.RawState.SideStepCommand); Assert.Equal(0u, interp.RawState.TurnCommand); Assert.Equal(MotionCommand.Ready, interp.InterpretedState.ForwardCommand); Assert.Equal(1.0f, interp.InterpretedState.ForwardSpeed, precision: 5); Assert.Equal(0u, interp.InterpretedState.SideStepCommand); Assert.Equal(0u, interp.InterpretedState.TurnCommand); } [Fact] public void StopCompletely_ZerosPhysicsBodyVelocity() { var body = MakeGrounded(); body.set_velocity(new Vector3(5f, 3f, 0f)); var interp = MakeInterp(body); interp.StopCompletely(); Assert.Equal(Vector3.Zero, body.Velocity); } [Fact] public void StopCompletely_NullPhysicsObj_ReturnsNoPhysicsObject() { var interp = new MotionInterpreter(); var result = interp.StopCompletely(); Assert.Equal(WeenieError.NoPhysicsObject, result); } // ========================================================================= // get_state_velocity (FUN_00528960) // ========================================================================= [Fact] public void GetStateVelocity_Idle_ReturnsZero() { var interp = MakeInterp(); // Default interpreted state = Ready, no sidestep, no turn var vel = interp.get_state_velocity(); Assert.Equal(Vector3.Zero, vel); } [Fact] public void GetStateVelocity_WalkForward_ReturnsWalkSpeed() { var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardSpeed = 1.0f; var vel = interp.get_state_velocity(); // Y = WalkAnimSpeed * 1.0 = 3.12 Assert.Equal(0f, vel.X, precision: 5); Assert.Equal(MotionInterpreter.WalkAnimSpeed, vel.Y, precision: 4); Assert.Equal(0f, vel.Z, precision: 5); } [Fact] public void GetStateVelocity_RunForward_ReturnsRunSpeed() { var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; var vel = interp.get_state_velocity(); // Y = RunAnimSpeed * 1.0 = 4.0 Assert.Equal(MotionInterpreter.RunAnimSpeed, vel.Y, precision: 4); } [Fact] public void GetStateVelocity_SidestepRight_ReturnsPositiveX() { var interp = MakeInterp(); interp.InterpretedState.SideStepCommand = MotionCommand.SideStepRight; interp.InterpretedState.SideStepSpeed = 1.0f; var vel = interp.get_state_velocity(); Assert.Equal(MotionInterpreter.SidestepAnimSpeed, vel.X, precision: 4); Assert.Equal(0f, vel.Y, precision: 5); } [Fact] public void GetStateVelocity_ClampsToMaxSpeed_WhenExceedRunRate() { // Speed=10 for walk would produce velocity.Y = 3.12*10 = 31.2, well over // RunAnimSpeed * runRate = 4.0 * 1.0 = 4.0; must be clamped. var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardSpeed = 10.0f; var vel = interp.get_state_velocity(); float maxSpeed = MotionInterpreter.RunAnimSpeed * interp.MyRunRate; Assert.True(vel.Length() <= maxSpeed + 1e-4f, $"velocity {vel.Length()} exceeds maxSpeed {maxSpeed}"); } [Fact] public void GetStateVelocity_UsesWeenieRunRate() { var weenie = new FakeWeenie { RunRate = 2.0f }; var interp = MakeInterp(weenie: weenie); interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; var vel = interp.get_state_velocity(); // maxSpeed = 4.0 * 2.0 = 8.0; velocity.Y = RunAnimSpeed * 1.0 = 4.0 ≤ 8.0 Assert.Equal(MotionInterpreter.RunAnimSpeed, vel.Y, precision: 4); } // ========================================================================= // get_state_velocity + GetCycleVelocity accessor (Option B — r03 §1.3) // // When the accessor is wired (AnimationSequencer.CurrentVelocity = // MotionData.Velocity * speedMod), get_state_velocity prefers that value // over the decompiled constant path. Keeps legs-per-meter invariant. // ========================================================================= [Fact] public void GetStateVelocity_CycleAccessor_OverridesRunAnimSpeed() { // Sequencer's CurrentVelocity represents MotionData.Velocity * speedMod. // With speedMod=2.0 and MotionData.Velocity.Y=3.0, the sequencer reports // 6.0 — which should override the decompiled `RunAnimSpeed * ForwardSpeed` // path (= 4.0 * 2.0 = 8.0). // // maxSpeed clamp (RunAnimSpeed * runRate = 4.0 * 2.0 = 8.0) allows 6.0. var weenie = new FakeWeenie { RunRate = 2.0f }; var interp = MakeInterp(weenie: weenie); interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 2.0f; interp.GetCycleVelocity = () => new Vector3(0f, 6.0f, 0f); var vel = interp.get_state_velocity(); Assert.Equal(6.0f, vel.Y, precision: 4); } [Fact] public void GetStateVelocity_CycleAccessor_OverridesWalkAnimSpeed() { // Walk with MotionData.Velocity.Y=2.5 + speedMod=1.0 → sequencer reports 2.5. // Without accessor, get_state_velocity would return WalkAnimSpeed (3.12). var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.GetCycleVelocity = () => new Vector3(0f, 2.5f, 0f); var vel = interp.get_state_velocity(); Assert.Equal(2.5f, vel.Y, precision: 4); } [Fact] public void GetStateVelocity_CycleAccessor_ZeroY_FallsBackToConstant() { // During a zero-velocity link transition the sequencer's CurrentVelocity // temporarily goes to (0,0,0). The constant path remains the safe default // so the body keeps moving at ForwardSpeed's expected rate. var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.GetCycleVelocity = () => Vector3.Zero; var vel = interp.get_state_velocity(); Assert.Equal(MotionInterpreter.RunAnimSpeed, vel.Y, precision: 4); } [Fact] public void GetStateVelocity_CycleAccessor_ClampStillApplies() { // Runaway MotionData.Velocity (20 m/s) must still be clamped to // RunAnimSpeed * runRate (4.0 * 1.0 = 4.0). This preserves the // decompiled `if (|velocity| > maxSpeed)` guard at the bottom of // FUN_00528960 — the accessor only replaces the *drive*, not the clamp. var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.GetCycleVelocity = () => new Vector3(0f, 20.0f, 0f); var vel = interp.get_state_velocity(); float maxSpeed = MotionInterpreter.RunAnimSpeed * interp.MyRunRate; Assert.True(vel.Length() <= maxSpeed + 1e-4f, $"velocity {vel.Length()} exceeds maxSpeed {maxSpeed}"); } [Fact] public void GetStateVelocity_CycleAccessor_OnlyAffectsForwardAxis() { // Sidestep uses its own constant path — the sequencer only tracks the // forward cycle's velocity, so strafe must ignore the accessor's X. // Even if the accessor returned a bogus X, SideStepAnimSpeed wins. var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.Ready; interp.InterpretedState.SideStepCommand = MotionCommand.SideStepRight; interp.InterpretedState.SideStepSpeed = 1.0f; interp.GetCycleVelocity = () => new Vector3(99f, 0f, 0f); var vel = interp.get_state_velocity(); // velocity.X must equal SidestepAnimSpeed (1.25), NOT 99. Assert.Equal(MotionInterpreter.SidestepAnimSpeed, vel.X, precision: 4); } [Fact] public void GetStateVelocity_CycleAccessor_NotCalledWhenIdle() { // When ForwardCommand == Ready, the decompiled path never reads the // forward-axis; the accessor shouldn't be invoked either (avoid // misleading zero readings when stationary). int invocations = 0; var interp = MakeInterp(); interp.InterpretedState.ForwardCommand = MotionCommand.Ready; interp.GetCycleVelocity = () => { invocations++; return new Vector3(0f, 5f, 0f); }; var vel = interp.get_state_velocity(); // Velocity must be zero; accessor must not have supplied the 5.0f. Assert.Equal(0f, vel.Y, precision: 4); // Implementation detail: it's OK to call the accessor once (we do), // but it must never leak its value into velocity when ForwardCommand // is Ready. The assertion above guarantees that without over-pinning // the exact call count. } // ========================================================================= // jump (FUN_00529390) // ========================================================================= [Fact] public void Jump_Grounded_SetsJumpExtentAndLeavesWalkable() { var body = MakeGrounded(); var interp = MakeInterp(body); var result = interp.jump(0.5f); Assert.Equal(WeenieError.None, result); Assert.Equal(0.5f, interp.JumpExtent, precision: 5); Assert.False(body.OnWalkable, "Body should no longer be on walkable after jump"); } [Fact] public void Jump_Airborne_ReturnsYouCantJumpWhileInTheAir() { var body = MakeAirborne(); var interp = MakeInterp(body); var result = interp.jump(0.5f); Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result); } [Fact] public void Jump_WeenieBlocksJump_ClearStandingLongJump() { var weenie = new FakeWeenie { CanJumpResult = false }; var body = MakeGrounded(); var interp = MakeInterp(body, weenie); interp.StandingLongJump = true; var result = interp.jump(0.5f); Assert.Equal(WeenieError.CantJumpLoadedDown, result); Assert.False(interp.StandingLongJump); } [Fact] public void Jump_NullPhysicsObj_ReturnsNoPhysicsObject() { var interp = new MotionInterpreter(); var result = interp.jump(0.5f); Assert.Equal(WeenieError.NoPhysicsObject, result); } // ========================================================================= // get_jump_v_z (FUN_005286b0) // ========================================================================= [Fact] public void GetJumpVz_ZeroExtent_ReturnsZero() { var interp = MakeInterp(); interp.JumpExtent = 0f; Assert.Equal(0f, interp.get_jump_v_z(), precision: 5); } [Fact] public void GetJumpVz_NoWeenie_ReturnsDefault() { var interp = MakeInterp(); interp.JumpExtent = 0.5f; Assert.Equal(MotionInterpreter.DefaultJumpVz, interp.get_jump_v_z(), precision: 4); } [Fact] public void GetJumpVz_WithWeenie_DelegatesToInqJumpVelocity() { var weenie = new FakeWeenie { JumpVz = 8.0f }; var interp = MakeInterp(weenie: weenie); interp.JumpExtent = 1.0f; float vz = interp.get_jump_v_z(); // FakeWeenie returns JumpVz * extent = 8.0 * 1.0 = 8.0 Assert.Equal(8.0f, vz, precision: 4); } [Fact] public void GetJumpVz_ExtentClampsAt1() { var weenie = new FakeWeenie { JumpVz = 10.0f }; var interp = MakeInterp(weenie: weenie); interp.JumpExtent = 5.0f; // over-clamped float vz = interp.get_jump_v_z(); // Should clamp extent to 1.0: FakeWeenie returns 10.0 * 1.0 = 10.0 Assert.Equal(10.0f, vz, precision: 4); } [Fact] public void GetJumpVz_WeenieReturnsFailure_ReturnsZero() { var weenie = new FakeWeenie { InqJumpVelocityResult = false }; var interp = MakeInterp(weenie: weenie); interp.JumpExtent = 0.5f; Assert.Equal(0f, interp.get_jump_v_z(), precision: 5); } // ========================================================================= // get_leave_ground_velocity (FUN_00528cd0) // ========================================================================= [Fact] public void GetLeaveGroundVelocity_WalkingForward_HasPositiveYAndZ() { var weenie = new FakeWeenie { JumpVz = 10.0f }; var body = MakeGrounded(); var interp = new MotionInterpreter(body, weenie) { JumpExtent = 1.0f, }; interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardSpeed = 1.0f; var vel = interp.get_leave_ground_velocity(); Assert.True(vel.Y > 0f, "Y velocity should be positive when walking forward"); Assert.True(vel.Z > 0f, "Z (jump) velocity should be positive"); } [Fact] public void GetLeaveGroundVelocity_Standing_ZeroExtent_ReturnsZeroXY() { var body = MakeGrounded(); var interp = new MotionInterpreter(body) { JumpExtent = 0f, // below epsilon }; // Default interpreted state = Ready (no walk/run command) var vel = interp.get_leave_ground_velocity(); Assert.Equal(0f, vel.X, precision: 5); Assert.Equal(0f, vel.Y, precision: 5); Assert.Equal(0f, vel.Z, precision: 5); } // ========================================================================= // jump_is_allowed (FUN_00528ec0) // ========================================================================= [Fact] public void JumpIsAllowed_Grounded_ReturnsNone() { var interp = MakeInterp(MakeGrounded()); var result = interp.jump_is_allowed(0.5f, 0); Assert.Equal(WeenieError.None, result); } [Fact] public void JumpIsAllowed_Airborne_ReturnsYouCantJumpWhileInTheAir() { var interp = MakeInterp(MakeAirborne()); var result = interp.jump_is_allowed(0.5f, 0); Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result); } [Fact] public void JumpIsAllowed_WeenieBlocks_ReturnsCantJumpLoadedDown() { var weenie = new FakeWeenie { CanJumpResult = false }; var interp = MakeInterp(MakeGrounded(), weenie); var result = interp.jump_is_allowed(0.5f, 0); Assert.Equal(WeenieError.CantJumpLoadedDown, result); } [Fact] public void JumpIsAllowed_NoGravityFlag_ReturnsYouCantJumpWhileInTheAir() { var body = new PhysicsBody { State = PhysicsStateFlags.None, // no gravity TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable, }; var interp = MakeInterp(body); // No gravity → must be airborne-style var result = interp.jump_is_allowed(0.5f, 0); Assert.Equal(WeenieError.YouCantJumpWhileInTheAir, result); } [Fact] public void JumpIsAllowed_NullPhysicsObj_ReturnsGeneralFailure() { var interp = new MotionInterpreter(); var result = interp.jump_is_allowed(0.5f, 0); Assert.Equal(WeenieError.GeneralMovementFailure, result); } // ========================================================================= // contact_allows_move (FUN_00528dd0) // ========================================================================= [Fact] public void ContactAllowsMove_Grounded_Ready_AllowsMove() { var body = MakeGrounded(); var interp = MakeInterp(body); // InterpretedState.ForwardCommand defaults to Ready bool allowed = interp.contact_allows_move(MotionCommand.WalkForward); Assert.True(allowed); } [Fact] public void ContactAllowsMove_Airborne_RejectsMove() { var body = MakeAirborne(); var interp = MakeInterp(body); bool allowed = interp.contact_allows_move(MotionCommand.WalkForward); Assert.False(allowed); } [Fact] public void ContactAllowsMove_TurnCommand_AlwaysAllowed_EvenAirborne() { var body = MakeAirborne(); var interp = MakeInterp(body); // Turn commands bypass the ground-contact check. bool allowRight = interp.contact_allows_move(MotionCommand.TurnRight); bool allowLeft = interp.contact_allows_move(MotionCommand.TurnLeft); Assert.True(allowRight); Assert.True(allowLeft); } [Fact] public void ContactAllowsMove_FallenState_RejectsMove() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.InterpretedState.ForwardCommand = MotionCommand.Fallen; bool allowed = interp.contact_allows_move(MotionCommand.WalkForward); Assert.False(allowed); } [Fact] public void ContactAllowsMove_DeadState_RejectsMove() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.InterpretedState.ForwardCommand = MotionCommand.Dead; bool allowed = interp.contact_allows_move(MotionCommand.WalkForward); Assert.False(allowed); } [Theory] [InlineData(MotionCommand.Crouch)] [InlineData(MotionCommand.Sitting)] [InlineData(MotionCommand.Sleeping)] public void ContactAllowsMove_PostureState_RejectsMove(uint postureCommand) { var body = MakeGrounded(); var interp = MakeInterp(body); interp.InterpretedState.ForwardCommand = postureCommand; bool allowed = interp.contact_allows_move(MotionCommand.WalkForward); Assert.False(allowed); } [Fact] public void ContactAllowsMove_CrouchRange_RejectsMove() { var body = MakeGrounded(); var interp = MakeInterp(body); // 0x41000012 is inside (0x41000011, 0x41000015) — crouch state interp.InterpretedState.ForwardCommand = 0x41000012u; bool allowed = interp.contact_allows_move(MotionCommand.WalkForward); Assert.False(allowed); } [Fact] public void ContactAllowsMove_NoGravity_AlwaysAllows() { // E.g. flying or swimming object var body = new PhysicsBody { State = PhysicsStateFlags.None, TransientState = TransientStateFlags.None, }; var interp = MakeInterp(body); bool allowed = interp.contact_allows_move(MotionCommand.WalkForward); Assert.True(allowed); } [Fact] public void ContactAllowsMove_GroundedAndIdle_SetsStandingLongJump() { var body = MakeGrounded(); var interp = MakeInterp(body); // All interpreted commands at default (Ready, no sidestep, no turn) interp.StandingLongJump = false; interp.contact_allows_move(MotionCommand.WalkForward); Assert.True(interp.StandingLongJump, "Should set StandingLongJump when grounded and idle"); } // ========================================================================= // apply_current_movement (FUN_00529210) // ========================================================================= [Fact] public void ApplyCurrentMovement_WalkForward_PushesNonZeroVelocity() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.InterpretedState.ForwardCommand = MotionCommand.WalkForward; interp.InterpretedState.ForwardSpeed = 1.0f; interp.apply_current_movement(cancelMoveTo: false, allowJump: false); // The body's local-space velocity should have been set to some non-zero forward component. // We can't easily test world-space (depends on orientation = Identity), but with // Identity orientation the local Y becomes world Y. Assert.True(body.Velocity.Length() > 0f, "Velocity should be non-zero when walking"); } [Fact] public void ApplyCurrentMovement_Idle_PushesZeroVelocity() { var body = MakeGrounded(); var interp = MakeInterp(body); // Default state = Ready interp.apply_current_movement(cancelMoveTo: false, allowJump: false); Assert.Equal(Vector3.Zero, body.Velocity); } [Fact] public void ApplyCurrentMovement_Run_UpdatesMyRunRate() { var body = MakeGrounded(); var interp = MakeInterp(body); interp.MyRunRate = 1.0f; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; interp.InterpretedState.ForwardSpeed = 2.0f; // e.g. speed-buff interp.apply_current_movement(cancelMoveTo: false, allowJump: false); Assert.Equal(2.0f, interp.MyRunRate, precision: 5); } [Fact] public void ApplyCurrentMovement_RunForward_SetsMyRunRate() { // Verifies that wiring a server-echoed ForwardSpeed (from UpdateMotion // 0xF74C InterpretedMotionState flag 0x10) into the MotionInterpreter // produces the correct MyRunRate and get_state_velocity result. var body = new PhysicsBody(); var mi = new MotionInterpreter(body); mi.InterpretedState.ForwardCommand = MotionCommand.RunForward; mi.InterpretedState.ForwardSpeed = 2.375f; mi.apply_current_movement(cancelMoveTo: false, allowJump: true); Assert.Equal(2.375f, mi.MyRunRate, precision: 3); 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); } [Fact] public void GetMaxSpeed_RunForward_NoWeenie_FallsBackToMyRunRate() { // WeenieObj is null (MakeInterp with no weenie argument); MyRunRate // is set explicitly. GetMaxSpeed must use MyRunRate as the run-rate // source when InqRunRate is unavailable. var interp = MakeInterp(); interp.MyRunRate = 1.75f; interp.InterpretedState.ForwardCommand = MotionCommand.RunForward; float speed = interp.GetMaxSpeed(); Assert.Equal(MotionInterpreter.RunAnimSpeed * 1.75f, speed, precision: 4); } }