Files
2026-05-31 21:35:12 -07:00

93 lines
4.5 KiB
C#

using Unity.Collections;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// 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.
/// <para>
/// Authored as a <see langword="static"/> 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
/// <c>AbilityFireSystem</c>) — the server's authoritative <c>Projectile.Direction</c> 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.
/// </para>
/// </summary>
public static class AutoTarget
{
/// <summary>
/// Resolves the planar (XZ) direction a shot should take. Returns the normalized direction toward
/// the nearest candidate within <paramref name="autoTargetRange"/> whose bearing from
/// <paramref name="from"/> lies within <paramref name="coneHalfAngleRadians"/> of
/// <paramref name="rawAimDir"/>; if no candidate qualifies, returns <paramref name="rawAimDir"/>
/// unchanged.
/// </summary>
/// <param name="from">Shooter world position; only the XZ plane is considered.</param>
/// <param name="rawAimDir">
/// Caller-normalized planar aim direction (world XZ mapped to <c>float2(x, y)</c>). If it is
/// effectively zero-length, it is returned unchanged (no valid heading to assist).
/// </param>
/// <param name="autoTargetRange">Max planar distance to consider a candidate; <c>0</c> (or less) disables assist.</param>
/// <param name="coneHalfAngleRadians">Half-angle of the assist cone, measured from <paramref name="rawAimDir"/>.</param>
/// <param name="candidatePositions">Candidate target world positions (XZ used). Read-only; not modified.</param>
/// <returns>
/// The normalized direction toward the chosen candidate, or <paramref name="rawAimDir"/> when no
/// candidate qualifies. Ties on distance are broken by the smallest candidate index for determinism.
/// </returns>
public static float2 Resolve(float3 from, float2 rawAimDir, float autoTargetRange, float coneHalfAngleRadians,
in NativeArray<float3> 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;
}
}
}