using Unity.Collections;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
///
/// Pure, deterministic auto-target assist helper for player abilities (M2 combat). Given a shooter
/// position, a raw aim direction, and a set of candidate target positions, picks the best target
/// inside an assist cone and snaps the shot toward it; otherwise returns the raw aim unchanged.
///
/// Authored as a class (no state) so it is Burst-safe and allocation-free,
/// callable from inside predicted/jobified systems. It is intended to run server-side only (see
/// AbilityFireSystem) — the server's authoritative Projectile.Direction GhostField then
/// reconciles the client's raw-aim predicted projectile. Determinism: no wall-clock, no randomness,
/// pure math; ties are broken by smallest candidate index so identical inputs always yield identical
/// output across the prediction loop and across machines.
///
///
public static class AutoTarget
{
///
/// Resolves the planar (XZ) direction a shot should take. Returns the normalized direction toward
/// the nearest candidate within whose bearing from
/// lies within of
/// ; if no candidate qualifies, returns
/// unchanged.
///
/// Shooter world position; only the XZ plane is considered.
///
/// Caller-normalized planar aim direction (world XZ mapped to float2(x, y)). If it is
/// effectively zero-length, it is returned unchanged (no valid heading to assist).
///
/// Max planar distance to consider a candidate; 0 (or less) disables assist.
/// Half-angle of the assist cone, measured from .
/// Candidate target world positions (XZ used). Read-only; not modified.
///
/// The normalized direction toward the chosen candidate, or when no
/// candidate qualifies. Ties on distance are broken by the smallest candidate index for determinism.
///
public static float2 Resolve(float3 from, float2 rawAimDir, float autoTargetRange, float coneHalfAngleRadians,
in NativeArray candidatePositions)
{
// No valid heading to assist along — caller guarantees normalization, but guard zero-length.
if (math.lengthsq(rawAimDir) < 1e-6f)
return rawAimDir;
// Disabled / nothing to consider.
if (autoTargetRange <= 0f || candidatePositions.Length == 0)
return rawAimDir;
float rangeSq = autoTargetRange * autoTargetRange;
float cosCone = math.cos(coneHalfAngleRadians);
int bestIndex = -1;
float bestDistSq = float.MaxValue;
float2 bestDir = rawAimDir;
for (int i = 0; i < candidatePositions.Length; i++)
{
// Planar (XZ) offset from shooter to candidate.
float3 offset = candidatePositions[i] - from;
float2 planar = new float2(offset.x, offset.z);
float distSq = math.lengthsq(planar);
// Skip self / coincident candidates (effectively zero distance → undefined bearing).
if (distSq < 1e-6f)
continue;
// Out of range.
if (distSq > rangeSq)
continue;
// Bearing test: dot of unit bearing with the (unit) raw aim vs cos(half-angle).
float2 dir = planar * math.rsqrt(distSq); // normalized planar bearing
float dot = math.dot(dir, rawAimDir);
if (dot < cosCone)
continue; // outside the assist cone
// Nearest wins; strict less-than keeps the first (smallest-index) candidate on ties.
if (distSq < bestDistSq)
{
bestDistSq = distSq;
bestIndex = i;
bestDir = dir;
}
}
return bestIndex >= 0 ? bestDir : rawAimDir;
}
}
}