feat(D6.2a): port CMotionInterp motion normalization + unify local velocity/turn/jump
Ports retail's raw->interpreted motion normalization and routes the local player's velocity through it, so backward/strafe come from the retail source instead of hand-mirrored controller code (retires register TS-22 + UN-5). MotionInterpreter (decomp-verbatim, Ghidra/ACE-cross-checked): - adjust_motion (0x00528010): WalkBackward->WalkForward (x-0.649999976), TurnLeft->TurnRight (x-1), SideStepLeft->SideStepRight (x-1), sidestep scale 0.5*(WalkAnimSpeed/SidestepAnimSpeed), RunForward early-return, per-channel holdkey fallback to CurrentHoldKey. - apply_run_to_command (0x00527be0): WalkForward->RunForward when speed>0 + UNCONDITIONAL *runRate; TurnRight *1.5; SideStepRight *runRate clamp +/-3.0. - apply_raw_movement (0x005287e0): copy raw->interpreted, adjust the 3 channels with per-channel hold keys. - WalkAnimSpeed unified to the retail-exact 3.11999989f (was 3.12f). PlayerMovementController: build ONE RawMotionState from MovementInput at forward_speed=1.0 (apply_run_to_command applies the run rate — a pre-scaled speed would double-scale), run apply_raw_movement, then take grounded + jump velocity straight from get_state_velocity (now correct for all directions) and drive the keyboard turn omega from the interpreted turn_speed (BaseTurnRate x turn_speed = the same pi/2 formula the remote path uses; numerically identical to the old TurnRateFor). Deleted the hand-mirrored backward(x-0.65) / strafe(x+-1.25) formulas in both the grounded and jump blocks. Mouse turn and the auto-walk path are untouched. Retail-faithful behavior changes (confirm in smoke): strafe is now 1.25 * ~1.248 * runRate = ~1.56*runRate, clamped via SideStepSpeed<=3.0 so |v.X|<=3.75 (was 1.25*runRate, no scale/clamp); backward is unchanged (3.12*0.65*runRate); backward/strafe-left velocity now comes from the pipeline instead of returning zero. Forward run pace unchanged (4.0*runRate). Tests: MotionNormalizationTests (17, adjust_motion/apply_run_to_command) + MotionVelocityPipelineTests (10, apply_raw_movement->get_state_velocity golden velocities incl. the strafe clamp + turn speeds). Full suite green (3255). Register: TS-22 + UN-5 deleted (retired); TS-34 added (adjust_motion creature guard is a no-op — IWeenieObject has no IsCreature; correct for the only caller, the player). Pseudocode: docs/research/2026-07-01-d6-motion-interp-pseudocode.md. NEXT D6.2b: the echo-gated wire forward_speed=1.0 question (evidence suggests ACE relays rather than recomputes; verified via the smoke echo). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4ed278369b
commit
0f099bb652
6 changed files with 801 additions and 149 deletions
299
tests/AcDream.Core.Tests/Physics/MotionNormalizationTests.cs
Normal file
299
tests/AcDream.Core.Tests/Physics/MotionNormalizationTests.cs
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MotionNormalizationTests — Phase D6.1: adjust_motion + apply_run_to_command.
|
||||
//
|
||||
// Source addresses tested (docs/research/2026-07-01-d6-motion-interp-pseudocode.md):
|
||||
// FUN_00528010 (0x00528010) CMotionInterp::adjust_motion
|
||||
// FUN_00527be0 (0x00527be0) CMotionInterp::apply_run_to_command
|
||||
//
|
||||
// Golden constants (retail, verified against ACE MotionInterp.cs byte-for-byte):
|
||||
// BackwardsFactor = 0.649999976 (0x007c8910)
|
||||
// WalkAnimSpeed = 3.11999989 (0x007c891c, retail-exact — NOT 3.12f)
|
||||
// SidestepAnimSpeed = 1.25
|
||||
// SidestepFactor = 0.5
|
||||
// RunTurnFactor = 1.5
|
||||
// MaxSidestepAnimRate = 3.0
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Fake WeenieObject for injecting a known run rate into apply_run_to_command /
|
||||
/// adjust_motion without a real weenie.
|
||||
/// </summary>
|
||||
file sealed class FakeRunRateWeenie : IWeenieObject
|
||||
{
|
||||
public float RunRate;
|
||||
public bool InqRunRateResult = true;
|
||||
|
||||
public bool InqJumpVelocity(float extent, out float vz)
|
||||
{
|
||||
vz = 0f;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool InqRunRate(out float rate)
|
||||
{
|
||||
rate = RunRate;
|
||||
return InqRunRateResult;
|
||||
}
|
||||
|
||||
public bool CanJump(float extent) => true;
|
||||
}
|
||||
|
||||
public sealed class MotionNormalizationTests
|
||||
{
|
||||
// Retail-exact sidestep scale: SidestepFactor(0.5) * (3.11999989 / 1.25) ≈ 1.24799995...
|
||||
private const float SidestepScale = 0.5f * (3.11999989f / MotionInterpreter.SidestepAnimSpeed);
|
||||
|
||||
private static MotionInterpreter MakeInterp(IWeenieObject? weenie = null)
|
||||
{
|
||||
return new MotionInterpreter { WeenieObj = weenie };
|
||||
}
|
||||
|
||||
// ── adjust_motion: per-command remap + scale ─────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AdjustMotion_WalkBackward_RemapsToWalkForward_NegatesAndScalesSpeed()
|
||||
{
|
||||
// 0x00528010: WalkBackwards -> cmd = WalkForward; speed *= -BackwardsFactor
|
||||
var interp = MakeInterp();
|
||||
uint motion = MotionCommand.WalkBackward;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.adjust_motion(ref motion, ref speed, HoldKey.None);
|
||||
|
||||
Assert.Equal(MotionCommand.WalkForward, motion);
|
||||
Assert.Equal(-MotionInterpreter.BackwardsFactor, speed, 5);
|
||||
Assert.Equal(-0.649999976f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustMotion_TurnLeft_RemapsToTurnRight_NegatesSpeed()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
uint motion = MotionCommand.TurnLeft;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.adjust_motion(ref motion, ref speed, HoldKey.None);
|
||||
|
||||
Assert.Equal(MotionCommand.TurnRight, motion);
|
||||
Assert.Equal(-1.0f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustMotion_SideStepLeft_RemapsToSideStepRight_NegatesBeforeScale()
|
||||
{
|
||||
// Negate happens BEFORE the ×1.248 sidestep scale, so the net factor is -1.248.
|
||||
var interp = MakeInterp();
|
||||
uint motion = MotionCommand.SideStepLeft;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.adjust_motion(ref motion, ref speed, HoldKey.None);
|
||||
|
||||
Assert.Equal(MotionCommand.SideStepRight, motion);
|
||||
Assert.Equal(-SidestepScale, speed, 5);
|
||||
Assert.Equal(-1.24799995f, speed, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustMotion_SideStepRight_ScalesSpeed_NoNegate()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
uint motion = MotionCommand.SideStepRight;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.adjust_motion(ref motion, ref speed, HoldKey.None);
|
||||
|
||||
Assert.Equal(MotionCommand.SideStepRight, motion);
|
||||
Assert.Equal(SidestepScale, speed, 5);
|
||||
Assert.Equal(1.24799995f, speed, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustMotion_RunForward_EarlyReturns_NoScaleNoHoldKeyPath()
|
||||
{
|
||||
// GOTCHA: RunForward returns immediately -- no scale AND no holdkey promotion,
|
||||
// even when holdKey == Run (there's nothing left to promote it to, but the
|
||||
// early-return also means speed is untouched).
|
||||
var interp = MakeInterp(new FakeRunRateWeenie { RunRate = 2.94f });
|
||||
uint motion = MotionCommand.RunForward;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.adjust_motion(ref motion, ref speed, HoldKey.Run);
|
||||
|
||||
Assert.Equal(MotionCommand.RunForward, motion);
|
||||
Assert.Equal(1.0f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustMotion_NonSidestepNonTurnNonForward_Unchanged_ExceptHoldKeyPath()
|
||||
{
|
||||
// Ready is not one of the switch cases and not SideStepRight after remap,
|
||||
// so command/speed pass through unchanged when holdKey doesn't promote it.
|
||||
var interp = MakeInterp();
|
||||
uint motion = MotionCommand.Ready;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.adjust_motion(ref motion, ref speed, HoldKey.None);
|
||||
|
||||
Assert.Equal(MotionCommand.Ready, motion);
|
||||
Assert.Equal(1.0f, speed, 5);
|
||||
}
|
||||
|
||||
// ── adjust_motion: holdKey inheritance + Run promotion ───────────────────
|
||||
|
||||
[Fact]
|
||||
public void AdjustMotion_HoldKeyInvalid_FallsBackToCurrentHoldKey_PromotesWalkForwardWhenRun()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.CurrentHoldKey = HoldKey.Run;
|
||||
uint motion = MotionCommand.WalkForward;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.adjust_motion(ref motion, ref speed, HoldKey.Invalid);
|
||||
|
||||
// apply_run_to_command: WalkForward + speed>0 -> RunForward; speed *= speedMod (1.0, no weenie).
|
||||
Assert.Equal(MotionCommand.RunForward, motion);
|
||||
Assert.Equal(1.0f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustMotion_HoldKeyNone_DoesNotPromote()
|
||||
{
|
||||
var interp = MakeInterp();
|
||||
interp.CurrentHoldKey = HoldKey.Run;
|
||||
uint motion = MotionCommand.WalkForward;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.adjust_motion(ref motion, ref speed, HoldKey.None);
|
||||
|
||||
Assert.Equal(MotionCommand.WalkForward, motion);
|
||||
Assert.Equal(1.0f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjustMotion_HoldKeyRunPassedDirectly_PromotesWalkForward()
|
||||
{
|
||||
var interp = MakeInterp(new FakeRunRateWeenie { RunRate = 2.75f });
|
||||
uint motion = MotionCommand.WalkForward;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.adjust_motion(ref motion, ref speed, HoldKey.Run);
|
||||
|
||||
Assert.Equal(MotionCommand.RunForward, motion);
|
||||
Assert.Equal(2.75f, speed, 5);
|
||||
}
|
||||
|
||||
// ── apply_run_to_command ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ApplyRunToCommand_WalkForward_PositiveSpeed_PromotesToRunForward_ScalesByRunRate()
|
||||
{
|
||||
var interp = MakeInterp(new FakeRunRateWeenie { RunRate = 2.94f });
|
||||
uint motion = MotionCommand.WalkForward;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.apply_run_to_command(ref motion, ref speed);
|
||||
|
||||
Assert.Equal(MotionCommand.RunForward, motion);
|
||||
Assert.Equal(2.94f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyRunToCommand_WalkForward_NegativeSpeed_StaysWalkForward_ScalesUnconditionally()
|
||||
{
|
||||
// GOTCHA: speed *= speedMod is UNCONDITIONAL -- backward (negative speed)
|
||||
// still gets run-scaled even though it is NOT promoted to RunForward.
|
||||
var interp = MakeInterp(new FakeRunRateWeenie { RunRate = 2.94f });
|
||||
uint motion = MotionCommand.WalkForward;
|
||||
float speed = -0.649999976f;
|
||||
|
||||
interp.apply_run_to_command(ref motion, ref speed);
|
||||
|
||||
Assert.Equal(MotionCommand.WalkForward, motion);
|
||||
Assert.Equal(-0.649999976f * 2.94f, speed, 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyRunToCommand_TurnRight_ScalesByRunTurnFactor_NoRunRateNoClamp()
|
||||
{
|
||||
var interp = MakeInterp(new FakeRunRateWeenie { RunRate = 2.94f });
|
||||
uint motion = MotionCommand.TurnRight;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.apply_run_to_command(ref motion, ref speed);
|
||||
|
||||
Assert.Equal(MotionCommand.TurnRight, motion);
|
||||
Assert.Equal(1.5f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyRunToCommand_SideStepRight_BelowClamp_ScalesByRunRate()
|
||||
{
|
||||
var interp = MakeInterp(new FakeRunRateWeenie { RunRate = 2.0f });
|
||||
uint motion = MotionCommand.SideStepRight;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.apply_run_to_command(ref motion, ref speed);
|
||||
|
||||
Assert.Equal(MotionCommand.SideStepRight, motion);
|
||||
Assert.Equal(2.0f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyRunToCommand_SideStepRight_AbovePositiveClamp_ClampsToPositiveMax()
|
||||
{
|
||||
// runRate 2.94 * speed 1.248 (post adjust_motion sidestep scale) ≈ 3.669 > 3.0
|
||||
var interp = MakeInterp(new FakeRunRateWeenie { RunRate = 2.94f });
|
||||
uint motion = MotionCommand.SideStepRight;
|
||||
float speed = 1.24799995f;
|
||||
|
||||
interp.apply_run_to_command(ref motion, ref speed);
|
||||
|
||||
Assert.Equal(MotionCommand.SideStepRight, motion);
|
||||
Assert.Equal(3.0f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyRunToCommand_SideStepRight_AboveNegativeClamp_ClampsToNegativeMax()
|
||||
{
|
||||
var interp = MakeInterp(new FakeRunRateWeenie { RunRate = 2.94f });
|
||||
uint motion = MotionCommand.SideStepRight;
|
||||
float speed = -1.24799995f;
|
||||
|
||||
interp.apply_run_to_command(ref motion, ref speed);
|
||||
|
||||
Assert.Equal(MotionCommand.SideStepRight, motion);
|
||||
Assert.Equal(-3.0f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyRunToCommand_UsesMyRunRate_WhenInqRunRateFails()
|
||||
{
|
||||
var weenie = new FakeRunRateWeenie { RunRate = 999f, InqRunRateResult = false };
|
||||
var interp = MakeInterp(weenie);
|
||||
interp.MyRunRate = 1.5f;
|
||||
uint motion = MotionCommand.WalkForward;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.apply_run_to_command(ref motion, ref speed);
|
||||
|
||||
Assert.Equal(MotionCommand.RunForward, motion);
|
||||
Assert.Equal(1.5f, speed, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyRunToCommand_NoWeenie_SpeedModDefaultsToOne()
|
||||
{
|
||||
var interp = MakeInterp(weenie: null);
|
||||
uint motion = MotionCommand.SideStepRight;
|
||||
float speed = 1.0f;
|
||||
|
||||
interp.apply_run_to_command(ref motion, ref speed);
|
||||
|
||||
Assert.Equal(1.0f, speed, 5);
|
||||
}
|
||||
}
|
||||
187
tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs
Normal file
187
tests/AcDream.Core.Tests/Physics/MotionVelocityPipelineTests.cs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MotionVelocityPipelineTests — Phase D6.2: the full raw→interpreted→velocity
|
||||
// pipeline (apply_raw_movement 0x005287e0 → get_state_velocity 0x00527d50).
|
||||
//
|
||||
// These pin the retail-faithful LOCAL velocity for every direction — the exact
|
||||
// behavior the D6.2 controller integration now routes through, replacing the
|
||||
// hand-mirrored backward/strafe formulas (register TS-22). Golden values are
|
||||
// derived from the pseudocode doc
|
||||
// (docs/research/2026-07-01-d6-motion-interp-pseudocode.md) and the retail
|
||||
// constants on MotionInterpreter.
|
||||
//
|
||||
// Notable retail-faithful change vs the old hand-mirror: strafe is now
|
||||
// 1.25 × (0.5·WalkAnimSpeed/SidestepAnimSpeed) × runRate = ~1.56 × runRate,
|
||||
// clamped via SideStepSpeed ≤ 3.0 (so |v.X| ≤ 3.75). The old code used
|
||||
// 1.25 × runRate with no sidestep-scale and no clamp.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
file sealed class FakeRunRateWeenie : IWeenieObject
|
||||
{
|
||||
public float RunRate;
|
||||
public bool InqRunRateResult = true;
|
||||
public bool InqJumpVelocity(float extent, out float vz) { vz = 0f; return false; }
|
||||
public bool InqRunRate(out float rate) { rate = RunRate; return InqRunRateResult; }
|
||||
public bool CanJump(float extent) => true;
|
||||
}
|
||||
|
||||
public sealed class MotionVelocityPipelineTests
|
||||
{
|
||||
private const float RunRate = 2.75f; // ≈ the +Acdream test char's run rate
|
||||
|
||||
private const float WalkAnim = MotionInterpreter.WalkAnimSpeed; // 3.11999989
|
||||
private const float RunAnim = MotionInterpreter.RunAnimSpeed; // 4.0
|
||||
private const float SideAnim = MotionInterpreter.SidestepAnimSpeed; // 1.25
|
||||
private const float BackFac = MotionInterpreter.BackwardsFactor; // 0.649999976
|
||||
private const float MaxSide = MotionInterpreter.MaxSidestepAnimRate;// 3.0
|
||||
// adjust_motion sidestep scale: 0.5 * (WalkAnim / SideAnim) ≈ 1.24799995
|
||||
private const float SideScale = MotionInterpreter.SidestepFactor * (WalkAnim / SideAnim);
|
||||
|
||||
private static MotionInterpreter MakeInterp(float runRate)
|
||||
=> new() { WeenieObj = new FakeRunRateWeenie { RunRate = runRate } };
|
||||
|
||||
private static RawMotionState Raw(
|
||||
uint forward = MotionCommand.Ready, uint sidestep = 0u, uint turn = 0u,
|
||||
HoldKey hold = HoldKey.None)
|
||||
=> new()
|
||||
{
|
||||
CurrentHoldKey = hold,
|
||||
ForwardCommand = forward,
|
||||
ForwardHoldKey = forward != MotionCommand.Ready ? hold : HoldKey.Invalid,
|
||||
ForwardSpeed = 1.0f,
|
||||
SidestepCommand = sidestep,
|
||||
SidestepHoldKey = sidestep != 0u ? hold : HoldKey.Invalid,
|
||||
SidestepSpeed = 1.0f,
|
||||
TurnCommand = turn,
|
||||
TurnHoldKey = turn != 0u ? hold : HoldKey.Invalid,
|
||||
TurnSpeed = 1.0f,
|
||||
};
|
||||
|
||||
// ── forward ──────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RunForward_VelocityIsRunAnimSpeedTimesRunRate()
|
||||
{
|
||||
var interp = MakeInterp(RunRate);
|
||||
interp.apply_raw_movement(Raw(forward: MotionCommand.WalkForward, hold: HoldKey.Run));
|
||||
|
||||
// WalkForward + Run → RunForward @ runRate; v.Y = RunAnim * runRate (== maxSpeed, no clamp).
|
||||
var v = interp.get_state_velocity();
|
||||
Assert.Equal(RunAnim * RunRate, v.Y, 3);
|
||||
Assert.Equal(0f, v.X, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkForward_NoRun_VelocityIsWalkAnimSpeed()
|
||||
{
|
||||
var interp = MakeInterp(RunRate);
|
||||
interp.apply_raw_movement(Raw(forward: MotionCommand.WalkForward, hold: HoldKey.None));
|
||||
|
||||
var v = interp.get_state_velocity();
|
||||
Assert.Equal(WalkAnim * 1.0f, v.Y, 3); // no run scaling; WalkForward @ 1.0
|
||||
}
|
||||
|
||||
// ── backward (the TS-22 fix: no longer zero) ─────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RunBackward_VelocityIsNegativeWalkTimesBackwardFactorTimesRunRate()
|
||||
{
|
||||
var interp = MakeInterp(RunRate);
|
||||
interp.apply_raw_movement(Raw(forward: MotionCommand.WalkBackward, hold: HoldKey.Run));
|
||||
|
||||
// WalkBackward → WalkForward, speed *= -0.65; Run → *= runRate (unconditional,
|
||||
// promotion sign-gated so it stays WalkForward). v.Y = WalkAnim * (-0.65 * runRate).
|
||||
var v = interp.get_state_velocity();
|
||||
Assert.Equal(-(WalkAnim * BackFac * RunRate), v.Y, 3);
|
||||
Assert.True(v.Y < 0f, "backward velocity must be negative");
|
||||
// Matches the OLD hand-mirror (WalkAnim * 0.65 * runMul) — backward is unchanged.
|
||||
}
|
||||
|
||||
// ── strafe (retail-faithful magnitude + ±3.0 clamp) ──────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RunStrafeRight_ClampsSideStepSpeedToThree_VelocityIs3p75()
|
||||
{
|
||||
var interp = MakeInterp(RunRate); // 1.248*2.75 = 3.432 > 3.0 → clamps
|
||||
interp.apply_raw_movement(Raw(sidestep: MotionCommand.SideStepRight, hold: HoldKey.Run));
|
||||
|
||||
var v = interp.get_state_velocity();
|
||||
Assert.Equal(SideAnim * MaxSide, v.X, 3); // 1.25 * 3.0 = 3.75
|
||||
Assert.Equal(3.75f, v.X, 3);
|
||||
Assert.Equal(0f, v.Y, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunStrafeRight_BelowClamp_VelocityIsScaledSidestep()
|
||||
{
|
||||
const float lowRun = 2.0f; // 1.248*2.0 = 2.496 < 3.0 → no clamp
|
||||
var interp = MakeInterp(lowRun);
|
||||
interp.apply_raw_movement(Raw(sidestep: MotionCommand.SideStepRight, hold: HoldKey.Run));
|
||||
|
||||
var v = interp.get_state_velocity();
|
||||
// v.X = SideAnim * (SideScale * lowRun) = 1.56 * lowRun ≈ 3.12
|
||||
Assert.Equal(SideAnim * SideScale * lowRun, v.X, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunStrafeLeft_NegatedAndClamped_VelocityIsNeg3p75()
|
||||
{
|
||||
var interp = MakeInterp(RunRate);
|
||||
interp.apply_raw_movement(Raw(sidestep: MotionCommand.SideStepLeft, hold: HoldKey.Run));
|
||||
|
||||
// SideStepLeft → SideStepRight, negated → -1.248; *runRate = -3.432; clamp → -3.0.
|
||||
var v = interp.get_state_velocity();
|
||||
Assert.Equal(-(SideAnim * MaxSide), v.X, 3); // -3.75
|
||||
Assert.True(v.X < 0f, "strafe-left velocity must be negative");
|
||||
}
|
||||
|
||||
// ── turn (drives the local Yaw omega: base π/2 × TurnSpeed) ───────────────
|
||||
|
||||
[Fact]
|
||||
public void RunTurnRight_InterpretedTurnSpeedIsPositiveRunTurnFactor()
|
||||
{
|
||||
var interp = MakeInterp(RunRate);
|
||||
interp.apply_raw_movement(Raw(turn: MotionCommand.TurnRight, hold: HoldKey.Run));
|
||||
|
||||
Assert.Equal(MotionCommand.TurnRight, interp.InterpretedState.TurnCommand);
|
||||
Assert.Equal(MotionInterpreter.RunTurnFactor, interp.InterpretedState.TurnSpeed, 5); // +1.5
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunTurnLeft_RemapsToTurnRightWithNegativeRunTurnFactor()
|
||||
{
|
||||
var interp = MakeInterp(RunRate);
|
||||
interp.apply_raw_movement(Raw(turn: MotionCommand.TurnLeft, hold: HoldKey.Run));
|
||||
|
||||
// TurnLeft → TurnRight, speed *= -1; Run → *= 1.5 → -1.5.
|
||||
Assert.Equal(MotionCommand.TurnRight, interp.InterpretedState.TurnCommand);
|
||||
Assert.Equal(-MotionInterpreter.RunTurnFactor, interp.InterpretedState.TurnSpeed, 5); // -1.5
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WalkTurn_NoRun_TurnSpeedIsUnity()
|
||||
{
|
||||
var interp = MakeInterp(RunRate);
|
||||
interp.apply_raw_movement(Raw(turn: MotionCommand.TurnRight, hold: HoldKey.None));
|
||||
|
||||
Assert.Equal(MotionCommand.TurnRight, interp.InterpretedState.TurnCommand);
|
||||
Assert.Equal(1.0f, interp.InterpretedState.TurnSpeed, 5); // no run factor
|
||||
}
|
||||
|
||||
// ── idle ─────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Idle_ReadyState_ZeroVelocity()
|
||||
{
|
||||
var interp = MakeInterp(RunRate);
|
||||
interp.apply_raw_movement(Raw()); // Ready, no sidestep/turn
|
||||
|
||||
var v = interp.get_state_velocity();
|
||||
Assert.Equal(0f, v.X, 5);
|
||||
Assert.Equal(0f, v.Y, 5);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue