7571091394
- 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>
115 lines
4.2 KiB
C#
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);
|
|
}
|
|
}
|
|
} |