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)); } } }