using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using UnityEngine.Networking; using UnityEngine.Serialization; namespace RoR2.Projectile; [RequireComponent(typeof(ProjectileTargetComponent))] [RequireComponent(typeof(TeamFilter))] public class ProjectileSphereTargetFinder : MonoBehaviour { [Tooltip("How far ahead the projectile should look to find a target.")] public float lookRange; [Tooltip("How long before searching for a target.")] public float targetSearchInterval = 0.5f; [Tooltip("Will not search for new targets once it has one.")] public bool onlySearchIfNoTarget; [Tooltip("Allows the target to be lost if it's outside the acceptable range.")] public bool allowTargetLoss; [Tooltip("If set, targets can only be found when there is a free line of sight.")] public bool testLoS; [Tooltip("Whether or not airborne characters should be ignored.")] public bool ignoreAir; [Tooltip("The difference in altitude at which a result will be ignored.")] [FormerlySerializedAs("altitudeTolerance")] public float flierAltitudeTolerance = float.PositiveInfinity; public UnityEvent onNewTargetFound; public UnityEvent onTargetLost; private new Transform transform; private TeamFilter teamFilter; private ProjectileTargetComponent targetComponent; private float searchTimer; private SphereSearch sphereSearch; private bool hasTarget; private bool hadTargetLastUpdate; private HurtBox lastFoundHurtBox; private Transform lastFoundTransform; private static readonly List foundHurtBoxes = new List(); private void Start() { if (!NetworkServer.active) { base.enabled = false; return; } transform = base.transform; teamFilter = GetComponent(); targetComponent = GetComponent(); sphereSearch = new SphereSearch(); searchTimer = 0f; } private void FixedUpdate() { searchTimer -= Time.fixedDeltaTime; if (!(searchTimer <= 0f)) { return; } searchTimer += targetSearchInterval; if (allowTargetLoss && (object)targetComponent.target != null && (object)lastFoundTransform == targetComponent.target && !PassesFilters(lastFoundHurtBox)) { SetTarget(null); } if (!onlySearchIfNoTarget || targetComponent.target == null) { SearchForTarget(); } hasTarget = targetComponent.target != null; if (hadTargetLastUpdate != hasTarget) { if (hasTarget) { onNewTargetFound?.Invoke(); } else { onTargetLost?.Invoke(); } } hadTargetLastUpdate = hasTarget; } private bool PassesFilters(HurtBox result) { CharacterBody body = result.healthComponent.body; if (!body || (ignoreAir && body.isFlying)) { return false; } if (body.isFlying && !float.IsInfinity(flierAltitudeTolerance) && flierAltitudeTolerance < Mathf.Abs(result.transform.position.y - transform.position.y)) { return false; } return true; } private void SearchForTarget() { sphereSearch.origin = transform.position; sphereSearch.radius = lookRange; sphereSearch.mask = LayerIndex.entityPrecise.mask; sphereSearch.queryTriggerInteraction = QueryTriggerInteraction.UseGlobal; sphereSearch.RefreshCandidates(); sphereSearch.FilterCandidatesByHurtBoxTeam(TeamMask.GetEnemyTeams(teamFilter.teamIndex)); sphereSearch.OrderCandidatesByDistance(); sphereSearch.FilterCandidatesByDistinctHurtBoxEntities(); sphereSearch.GetHurtBoxes(foundHurtBoxes); HurtBox target = null; if (foundHurtBoxes.Count > 0) { int i = 0; for (int count = foundHurtBoxes.Count; i < count; i++) { if (PassesFilters(foundHurtBoxes[i])) { target = foundHurtBoxes[i]; break; } } foundHurtBoxes.Clear(); } SetTarget(target); } private void SetTarget(HurtBox hurtBox) { lastFoundHurtBox = hurtBox; lastFoundTransform = hurtBox?.transform; targetComponent.target = lastFoundTransform; } private void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; Vector3 position = base.transform.position; Gizmos.DrawWireSphere(position, lookRange); if (!float.IsInfinity(flierAltitudeTolerance)) { Gizmos.DrawWireCube(position, new Vector3(lookRange * 2f, flierAltitudeTolerance * 2f, lookRange * 2f)); } } }