using NUnit.Framework; using ProjectM.Simulation; using Unity.Mathematics; namespace ProjectM.Tests { /// /// Pure-math coverage for the Shape-of-Dreams facing model (07-15): ResolveAim (the shared gameplay /// fire-direction resolver), SelectTarget (the Aim→Move→hold cascade — castActive grants Aim PRIORITY, /// never bypasses the cascade), and RotateToward (the rate-limited turn extracted verbatim from /// PlayerAimSystem). Plain NUnit, no World. /// public class FacingMathTests { const float Eps = 1e-4f; // ---- ResolveAim ---- [Test] public void ResolveAim_Aim_Wins_And_Normalizes() { var d = FacingMath.ResolveAim(new float2(3f, 0f), new float2(0f, 1f)); Assert.AreEqual(1f, d.x, Eps); Assert.AreEqual(0f, d.y, Eps); } [Test] public void ResolveAim_Zero_Aim_Falls_Back_To_Facing() { var d = FacingMath.ResolveAim(float2.zero, new float2(0f, -2f)); Assert.AreEqual(0f, d.x, Eps); Assert.AreEqual(-1f, d.y, Eps); } [Test] public void ResolveAim_Both_Zero_Falls_Back_To_PlusZ() { var d = FacingMath.ResolveAim(float2.zero, float2.zero); Assert.AreEqual(0f, d.x, Eps); Assert.AreEqual(1f, d.y, Eps); } // ---- SelectTarget (the cascade) ---- [Test] public void SelectTarget_CastActive_Prefers_Aim_Over_Move() { Assert.IsTrue(FacingMath.SelectTarget(true, new float2(1f, 0f), new float2(0f, 1f), out var t)); Assert.AreEqual(1f, t.x, Eps); } [Test] public void SelectTarget_CastActive_Zero_Aim_Falls_Through_To_Move() { // Gamepad resting right stick mid-cast: Aim is exact zero -> the cascade must fall to Move, // never freeze facing (the review's should-fix on the cast branch). Assert.IsTrue(FacingMath.SelectTarget(true, float2.zero, new float2(0f, 1f), out var t)); Assert.AreEqual(1f, t.y, Eps); } [Test] public void SelectTarget_Not_Casting_Ignores_Aim() { // The cursor is never passively tracked: outside a cast window Aim must NOT win. Assert.IsTrue(FacingMath.SelectTarget(false, new float2(1f, 0f), new float2(0f, 1f), out var t)); Assert.AreEqual(1f, t.y, Eps); Assert.AreEqual(0f, t.x, Eps); } [Test] public void SelectTarget_No_Input_Holds() { Assert.IsFalse(FacingMath.SelectTarget(false, float2.zero, float2.zero, out _)); Assert.IsFalse(FacingMath.SelectTarget(true, float2.zero, float2.zero, out _)); } // ---- RotateToward ---- [Test] public void RotateToward_Uninitialized_Snaps() { var d = FacingMath.RotateToward(float2.zero, new float2(1f, 0f), 0.01f); Assert.AreEqual(1f, d.x, Eps); } [Test] public void RotateToward_Within_Reach_Snaps_To_Target() { var d = FacingMath.RotateToward(new float2(0f, 1f), new float2(0f, 1f), 0.1f); Assert.AreEqual(0f, d.x, Eps); Assert.AreEqual(1f, d.y, Eps); } [Test] public void RotateToward_Steps_By_MaxStep_Toward_Target() { // 90° to cover, 30° step -> lands at 60° remaining (rotated 30° toward +X, the clockwise side). float step = math.radians(30f); var d = FacingMath.RotateToward(new float2(0f, 1f), new float2(1f, 0f), step); float remaining = math.degrees(math.acos(math.clamp(math.dot(d, new float2(1f, 0f)), -1f, 1f))); Assert.AreEqual(60f, remaining, 0.05f); Assert.Greater(d.x, 0f, "rotates toward the target side"); } [Test] public void RotateToward_Turns_The_Short_Way_Both_Sides() { float step = math.radians(10f); var right = FacingMath.RotateToward(new float2(0f, 1f), new float2(1f, 0.001f), step); var left = FacingMath.RotateToward(new float2(0f, 1f), new float2(-1f, 0.001f), step); Assert.Greater(right.x, 0f); Assert.Less(left.x, 0f); } } }