using ProjectI.Combat; using Unity.Netcode; using UnityEngine; using UnityEngine.SceneManagement; namespace ProjectI.Enemies { public static class EnemyProjectileCollision { public static bool BlastHasLineOfSight(Vector3 impact, Vector3 surfaceNormal, Vector3 target) { // Start on the exposed side of the struck surface, not inside it. return ProjectI.Combat.CombatSpatialQuery.HasWorldLineOfSight(impact + surfaceNormal * .1f, target); } private static readonly Collider[] overlaps = new Collider[32]; public static ulong TracerSweepQueries { get; private set; } public static ulong SaturatedSweepFallbacks { get; private set; } public static ulong SaturatedOverlapFallbacks { get; private set; } public static int LargestFallbackHitCount { get; private set; } public static bool SweepTracerContacts(Vector3 origin, float radius, Vector3 direction, float distance, NetworkManager manager, Scene scene, RaycastHit[] buffer, out RaycastHit nearest, out PrototypePlayerVitals player) { nearest = default; player = null; if (!EnemyProjectileTrackingRules.Finite(origin) || !EnemyProjectileTrackingRules.Finite(direction) || direction.sqrMagnitude < .000001f || !Finite(radius) || !Finite(distance) || radius <= 0 || distance < 0 || manager == null || !scene.IsValid() || buffer == null || buffer.Length == 0) return false; // Solid terrain/props on any layer can absorb a bolt. Actor classification below excludes // enemies and ineligible player shapes without accidentally treating them as world cover. const int mask = ~0; direction.Normalize(); TracerSweepQueries++; // SphereCast does not guarantee initial overlap detection. World wins a zero-distance tie. int overlapCount = Physics.OverlapSphereNonAlloc(origin, radius, overlaps, mask, QueryTriggerInteraction.Ignore); Collider[] overlapHits = overlaps; if (overlapCount == overlaps.Length) { SaturatedOverlapFallbacks++; overlapHits = Physics.OverlapSphere(origin, radius, mask, QueryTriggerInteraction.Ignore); overlapCount = overlapHits.Length; LargestFallbackHitCount = Mathf.Max(LargestFallbackHitCount, overlapCount); } for (int i = 0; i < overlapCount; i++) { if (!TryClassifyContact(overlapHits[i], manager, scene, out var candidate)) continue; if (candidate == null) { player = null; nearest.point = overlapHits[i].ClosestPoint(origin); nearest.distance = 0; Vector3 normal = origin - nearest.point; nearest.normal = normal.sqrMagnitude > .000001f ? normal.normalized : -direction; return true; } if (player != null && candidate.NetworkObjectId >= player.NetworkObjectId) continue; player = candidate; } if (player != null) { nearest.point = origin; nearest.distance = 0; nearest.normal = -direction; return true; } if (distance == 0) return false; int count = Physics.SphereCastNonAlloc(origin, radius, direction, buffer, distance, mask, QueryTriggerInteraction.Ignore); RaycastHit[] sweepHits = buffer; if (count == buffer.Length) { // A full array cannot establish the nearest valid obstacle/player. Use the same mask // for the exceptional allocating query, then apply the same contact classification. SaturatedSweepFallbacks++; sweepHits = Physics.SphereCastAll(origin, radius, direction, distance, mask, QueryTriggerInteraction.Ignore); count = sweepHits.Length; LargestFallbackHitCount = Mathf.Max(LargestFallbackHitCount, count); } float best = float.PositiveInfinity; bool found = false; for (int i = 0; i < count; i++) { var hit = sweepHits[i]; if (!TryClassifyContact(hit.collider, manager, scene, out var candidate) || hit.distance > best) continue; if (found && hit.distance == best) { if (player == null) continue; // An already selected world contact wins the exact tie. if (candidate != null && candidate.NetworkObjectId >= player.NetworkObjectId) continue; } found = true; best = hit.distance; nearest = hit; player = candidate; } return found; } private static bool TryClassifyContact(Collider collider, NetworkManager manager, Scene scene, out PrototypePlayerVitals player) { player = null; if (collider == null || collider.isTrigger || !collider.enabled || !collider.gameObject.activeInHierarchy || collider.GetComponentInParent() != null) return false; var candidate = collider.GetComponentInParent(); if (candidate == null) return true; if (!candidate.IsSpawned || !candidate.IsAlive || candidate.NetworkManager != manager || candidate.gameObject.scene != scene || !EnemyProjectileTrackingRules.Finite(candidate.AcceptedPosition)) return false; player = candidate; return true; } private static bool Finite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); public static bool SweepSegment(Vector3 origin,float radius,Vector3 direction,float distance,RaycastHit[] buffer,out RaycastHit nearest) { nearest=default;float best=float.PositiveInfinity; if(distance<=0||radius<=0||buffer==null||buffer.Length==0)return false; int count=Physics.SphereCastNonAlloc(origin,radius,direction,buffer,distance,~0,QueryTriggerInteraction.Ignore); for(int i=0;i()==null&&buffer[i].distance