using Unity.Mathematics;
namespace ProjectM.Simulation
{
///
/// Pure facing/aim math for the Shape-of-Dreams facing model (body yaw follows movement; turns toward
/// the aim only inside a cast window; holds when idle; the cursor is never passively tracked).
/// RotateToward is the verbatim extraction of PlayerAimSystem's rate-limited planar turn so it stays
/// the tested-in-play math. ResolveAim is THE gameplay fire-direction resolver — every damage/spawn
/// direction (AbilityFireSystem archetypes, MeleeComboSystem cleave) and the aim-readout presentation
/// (reticle, local slash arcs) must route through it so sim and FX can never diverge. EditMode-tested,
/// Burst-safe, no World needed.
///
public static class FacingMath
{
/// Gameplay fire direction: raw replicated Aim when meaningful, else the current body facing
/// (resting gamepad right stick — preserves controller-first "zero aim = movement heading" because
/// facing tracks Move under the SoD model), else world +Z. Always normalized.
public static float2 ResolveAim(float2 aim, float2 facing)
{
if (math.lengthsq(aim) > 1e-6f) return math.normalize(aim);
if (math.lengthsq(facing) > 1e-6f) return math.normalize(facing);
return new float2(0f, 1f);
}
/// The shared Aim→Move→hold cascade. castActive grants Aim PRIORITY only — a zero Aim
/// (resting gamepad stick mid-cast) falls through to Move, then to "no target" (hold previous
/// facing). Returns false when there is no target this tick.
public static bool SelectTarget(bool castActive, float2 aim, float2 move, out float2 target)
{
if (castActive && math.lengthsq(aim) > 1e-6f)
{
target = math.normalize(aim);
return true;
}
if (math.lengthsq(move) > 1e-6f)
{
target = math.normalize(move);
return true;
}
target = default;
return false;
}
/// Rate-limited planar rotate toward a normalized target: snaps when uninitialized or within
/// reach this step, else rotates by maxStepRadians toward the target. Deterministic pure math
/// (fixed-step dt at the caller) so it replays identically on rollback re-simulation.
public static float2 RotateToward(float2 current, float2 target, float maxStepRadians)
{
if (math.lengthsq(current) < 1e-6f)
return target; // uninitialized facing -> snap to target
float2 cur = math.normalize(current);
float angle = math.acos(math.clamp(math.dot(cur, target), -1f, 1f));
if (angle <= maxStepRadians)
return target; // within reach this step
float sign = (cur.x * target.y - cur.y * target.x) >= 0f ? 1f : -1f;
math.sincos(maxStepRadians * sign, out float sn, out float cs);
return math.normalize(new float2(cur.x * cs - cur.y * sn, cur.x * sn + cur.y * cs));
}
}
}