Files
Project-M/Assets/_Project/Tests/EditMode/FacingMathTests.cs
T
kronic 7571091394 LANTERN feel pass (DR-052 + gap list): SoD facing, underwater feel, Bathynaut kit, walk/run gait, suit lamp + new Synty anim packs
- SoD facing: PlayerFacing = body-yaw only (move-facing / cast-turn / idle-hold); every fire
  direction re-sourced to FacingMath.ResolveAim (pre-code review blocking catch); TickWindowMath
  shared windows with Movement-skip; reticle/FX coupled to the damage direction; cursor-dash kept.
- Underwater feel: sharpness 15->6, turn 720->360, MoveSpeed 6->4.2; TuningKnob 26-28
  (0 = no-override sentinels, dev-protocol bump on DebugTuningReport); stride footsteps + silt +
  cadence floor; bubbles; underwater ambience bed + distant groans; camera drag + dev scroll zoom.
- Bathynaut kit in-engine: dome/tank/shoulder-lamp + bare head grafted (GraftSmr rigid rebase,
  RecalculateTangents); EmissiveGloamSkinned shader (Rukhanka deformation); shoulder lamp CASTS
  (warm steady spot on body yaw).
- Gait: two-ring walk/run FreeformDirectional tree (walk @0.35, run @1.0) + blended-natural
  StrideScale; additive Posture(Bank) + Lead(chest-lead) layers; idle = AnimationIdles Base;
  banking driven from facing turn rate; flat terrain (Env_SeabedKit seabed squashed - CC is planar).
- New packs: Synty AnimationIdles + AnimationSwordCombat (combat pass queued) + SyntyPropBoneTool;
  four authored clips (sway/trudge/banks/lean) + Anim_Player_Underwater.blend + suit-kit FBX.
- Validation: 411/411 EditMode green; Play smokes (server==client facing, Aim-true projectile,
  bank/stride live-sampled, lamp beam verified); pre-code + post-impl adversarial reviews applied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:56:02 -07:00

115 lines
4.2 KiB
C#

using NUnit.Framework;
using ProjectM.Simulation;
using Unity.Mathematics;
namespace ProjectM.Tests
{
/// <summary>
/// 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.
/// </summary>
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);
}
}
}