feat(core): port decompiled AC client MotionInterpreter — 10 methods, 45 tests
C# port of CMotionInterp from chunk_00520000.c — the AC client's motion state machine that controls walk/run/jump/turn. Methods ported: - PerformMovement: top-level 5-case dispatcher - DoMotion: process raw motion command from packet - StopCompletely: reset to Ready (0x41000003) - get_state_velocity: compute velocity for current interpreted state - apply_current_movement: push velocity to PhysicsBody - jump: initiate jump (validate + set extent + leave ground) - get_jump_v_z: vertical jump velocity (delegates to WeenieObj) - get_leave_ground_velocity: full 3D launch vector - jump_is_allowed: requires Gravity + Contact + OnWalkable - contact_allows_move: slope angle + state checks Supporting types: MotionCommand constants, MovementType enum, WeenieError enum, RawMotionState/InterpretedMotionState structs, IWeenieObject interface. 412 total tests (303 core + 109 net). 45 new MotionInterpreter tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6a5d8c1580
commit
e3f8f95dfc
2 changed files with 1496 additions and 0 deletions
654
tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs
Normal file
654
tests/AcDream.Core.Tests/Physics/MotionInterpreterTests.cs
Normal file
|
|
@ -0,0 +1,654 @@
|
|||
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);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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_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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue