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