using AcDream.Core.Physics; using AcDream.Core.Physics.Motion; using Xunit; namespace AcDream.Core.Tests.Physics.Motion; /// /// R4-V1 — MovementParameters::get_command (0x0052aa00, raw /// 307946-308012), verbatim per /// docs/research/2026-07-03-r4-moveto/r4-moveto-decomp.md §5c. Covers the /// command/moving_away pick (plain-towards / plain-away / towards_and_away /// delegate) crossed with the walk-vs-run HoldKey cascade, INCLUDING the /// CanCharge 0x10 fast-path ACE dropped (feedback_autowalk_cancharge_bit) /// and the walk_run_threshhold ≤-vs-< edge (retail: dist - dto ≤ /// threshold → walk; the raw's `test ah,0x41` is the inclusive ≤ reading, /// §5c @308003). /// public sealed class MovementParametersGetCommandTests { // ── plain TOWARDS (move_towards set, move_away clear) ───────────────── [Fact] public void PlainTowards_DistGreaterThanDto_WalkForward_NotMovingAway() { var p = new MovementParameters { DistanceToObject = 0.6f }; // move_towards=true (default), move_away=false (default) p.GetCommand(dist: 5f, headingDiff: 0f, out uint motion, out HoldKey holdKey, out bool movingAway); Assert.Equal(MotionCommand.WalkForward, motion); Assert.False(movingAway); } [Fact] public void PlainTowards_DistNotGreaterThanDto_Idle() { var p = new MovementParameters { DistanceToObject = 0.6f }; p.GetCommand(dist: 0.6f, headingDiff: 0f, out uint motion, out _, out bool movingAway); Assert.Equal(0u, motion); Assert.False(movingAway); } [Fact] public void PlainTowards_DistLessThanDto_Idle() { var p = new MovementParameters { DistanceToObject = 0.6f }; p.GetCommand(dist: 0.1f, headingDiff: 0f, out uint motion, out _, out _); Assert.Equal(0u, motion); } // ── pure AWAY (move_away set, move_towards clear) ───────────────────── [Fact] public void PureAway_DistLessThanMinDistance_WalkForward_MovingAway() { var p = new MovementParameters { MoveTowards = false, MoveAway = true, MinDistance = 5f, }; p.GetCommand(dist: 2f, headingDiff: 0f, out uint motion, out _, out bool movingAway); Assert.Equal(MotionCommand.WalkForward, motion); Assert.True(movingAway); } [Fact] public void PureAway_DistNotLessThanMinDistance_Idle() { var p = new MovementParameters { MoveTowards = false, MoveAway = true, MinDistance = 5f, }; p.GetCommand(dist: 5f, headingDiff: 0f, out uint motion, out _, out bool movingAway); Assert.Equal(0u, motion); Assert.False(movingAway); } // ── towards_and_away delegate (both move_towards AND move_away set) ─── [Fact] public void TowardsAndAway_DistGreaterThanDto_DelegatesToWalkForwardTowards() { var p = new MovementParameters { MoveTowards = true, MoveAway = true, DistanceToObject = 0.6f, MinDistance = 0.2f, }; p.GetCommand(dist: 5f, headingDiff: 0f, out uint motion, out _, out bool movingAway); Assert.Equal(MotionCommand.WalkForward, motion); Assert.False(movingAway); } [Fact] public void TowardsAndAway_InsideMinBand_WalkBackwards_MovingAway() { var p = new MovementParameters { MoveTowards = true, MoveAway = true, DistanceToObject = 0.6f, MinDistance = 0.2f, }; // dist - min_distance < epsilon → inside the min band p.GetCommand(dist: 0.2f, headingDiff: 0f, out uint motion, out _, out bool movingAway); Assert.Equal(MotionCommand.WalkBackward, motion); Assert.True(movingAway); } [Fact] public void TowardsAndAway_InsideDeadband_Idle() { var p = new MovementParameters { MoveTowards = true, MoveAway = true, DistanceToObject = 0.6f, MinDistance = 0.2f, }; // strictly inside [min, dto] — neither band fires p.GetCommand(dist: 0.4f, headingDiff: 0f, out uint motion, out _, out _); Assert.Equal(0u, motion); } // ── neither towards nor away (both clear) — falls to plain-towards path ── [Fact] public void NeitherTowardsNorAway_FallsToPlainTowardsBranch() { var p = new MovementParameters { MoveTowards = false, MoveAway = false, DistanceToObject = 0.6f, }; p.GetCommand(dist: 5f, headingDiff: 0f, out uint motion, out _, out bool movingAway); Assert.Equal(MotionCommand.WalkForward, motion); Assert.False(movingAway); } // ── walk-vs-run HoldKey cascade ──────────────────────────────────────── [Fact] public void HoldKey_CanChargeSet_AlwaysRun_FastPath() { // THE fast-path ACE dropped: can_charge (0x10) short-circuits // straight to HoldKey_Run regardless of distance/threshold. var p = new MovementParameters { CanCharge = true, CanRun = false, // even with can_run CLEAR CanWalk = true, WalkRunThreshhold = 15f, DistanceToObject = 0.6f, }; p.GetCommand(dist: 0.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _); Assert.Equal(HoldKey.Run, holdKey); } [Fact] public void HoldKey_CanRunClear_AlwaysWalk_RegardlessOfDistance() { var p = new MovementParameters { CanCharge = false, CanRun = false, CanWalk = true, WalkRunThreshhold = 15f, DistanceToObject = 0.6f, }; p.GetCommand(dist: 1000f, headingDiff: 0f, out _, out HoldKey holdKey, out _); Assert.Equal(HoldKey.None, holdKey); } [Fact] public void HoldKey_CanRunSet_CanWalkClear_AlwaysRun_WalkIncapable() { // can_walk clear → the "close enough to walk" branch is skipped // entirely; walk-incapable movers always run when can_run is set. var p = new MovementParameters { CanCharge = false, CanRun = true, CanWalk = false, WalkRunThreshhold = 15f, DistanceToObject = 0.6f, }; p.GetCommand(dist: 0.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _); Assert.Equal(HoldKey.Run, holdKey); } [Fact] public void HoldKey_CanRunAndCanWalk_WithinThreshold_Walk() { var p = new MovementParameters { CanCharge = false, CanRun = true, CanWalk = true, WalkRunThreshhold = 15f, DistanceToObject = 0.6f, }; // dist - dto = 10 <= 15 → walk p.GetCommand(dist: 10.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _); Assert.Equal(HoldKey.None, holdKey); } [Fact] public void HoldKey_CanRunAndCanWalk_BeyondThreshold_Run() { var p = new MovementParameters { CanCharge = false, CanRun = true, CanWalk = true, WalkRunThreshhold = 15f, DistanceToObject = 0.6f, }; // dist - dto = 15.1 > 15 → run p.GetCommand(dist: 15.7f, headingDiff: 0f, out _, out HoldKey holdKey, out _); Assert.Equal(HoldKey.Run, holdKey); } [Fact] public void HoldKey_ThresholdEdge_ExactlyAtThreshold_IsInclusive_Walk() { // retail: (dist - distance_to_object) <= walk_run_threshhold → WALK. // The raw's `test ah,0x41` after `fcom` renders as an inclusive // "not greater than" (≤) — the boundary itself walks, not runs. var p = new MovementParameters { CanCharge = false, CanRun = true, CanWalk = true, WalkRunThreshhold = 15f, DistanceToObject = 0.6f, }; // dist - dto = exactly 15.0 p.GetCommand(dist: 15.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _); Assert.Equal(HoldKey.None, holdKey); } [Fact] public void HoldKey_ThresholdEdge_JustOverThreshold_Run() { var p = new MovementParameters { CanCharge = false, CanRun = true, CanWalk = true, WalkRunThreshhold = 15f, DistanceToObject = 0.6f, }; // dist - dto = 15.0 + epsilon p.GetCommand(dist: 15.600001f, headingDiff: 0f, out _, out HoldKey holdKey, out _); Assert.Equal(HoldKey.Run, holdKey); } [Fact] public void HoldKey_CanChargeSet_OverridesWalkIncapableAndThreshold() { // CanCharge fast-path wins even when every other flag would say walk. var p = new MovementParameters { CanCharge = true, CanRun = true, CanWalk = true, WalkRunThreshhold = 1000f, // would otherwise force walk DistanceToObject = 0.6f, }; p.GetCommand(dist: 0.6f, headingDiff: 0f, out _, out HoldKey holdKey, out _); Assert.Equal(HoldKey.Run, holdKey); } // ── the four capability quadrants × plain-towards distance bands ────── [Theory] // (canRun, canWalk, canCharge, distBeyondThreshold) → expected HoldKey [InlineData(true, true, false, false, HoldKey.None)] // both capable, close → walk [InlineData(true, true, false, true, HoldKey.Run)] // both capable, far → run [InlineData(true, false, false, false, HoldKey.Run)] // run-only, close → still run (no walk branch) [InlineData(true, false, false, true, HoldKey.Run)] // run-only, far → run [InlineData(false, true, false, false, HoldKey.None)] // walk-only → always walk [InlineData(false, true, false, true, HoldKey.None)] // walk-only, far → still walk [InlineData(false, false, false, false, HoldKey.None)] // neither capable, no charge → walk (falls through) [InlineData(false, false, true, false, HoldKey.Run)] // can_charge alone → run regardless public void HoldKey_FourCapabilityQuadrants_MatchRetailCascade( bool canRun, bool canWalk, bool canCharge, bool distBeyondThreshold, HoldKey expected) { var p = new MovementParameters { CanRun = canRun, CanWalk = canWalk, CanCharge = canCharge, WalkRunThreshhold = 15f, DistanceToObject = 0.6f, }; float dist = distBeyondThreshold ? 20f : 5f; // 20-0.6=19.4>15 ; 5-0.6=4.4<=15 p.GetCommand(dist, headingDiff: 0f, out _, out HoldKey holdKey, out _); Assert.Equal(expected, holdKey); } }