acdream/tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs

820 lines
28 KiB
C#

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
// ─────────────────────────────────────────────────────────────────────────────
/// <summary>
/// Fake WeenieObject for test isolation — all methods return configurable values.
/// </summary>
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);
}
}