using System; using System.Collections.Generic; using System.Linq; using ProjectI.Combat; using ProjectI.Player; using ProjectI.Runs; using UnityEngine; using UnityEngine.AI; namespace ProjectI.Enemies { public enum EnemySpawnSelectionStatus { None, Pending, Rejected, Accepted } public enum EnemySpawnRejection { None, InvalidBindings, NoPlayers, Bounds, Disk, Distance, Ground, AgentMesh, Occupied, Visible, Route, Budget, InsufficientSlots, Timeout, Population, GroupRegistration } public enum EnemySpawnRouteResult { Rejected, Pending, Accepted } [Serializable] public struct EnemySpawnAdmissionDiagnostics { public uint Seed, GroupId; public EnemySpawnSelectionStatus Status; public EnemySpawnRejection LastRejection; public bool Pending; public int DesiredCount, CandidateCircles, CandidatePoints, AcceptedMembers; public long PathQueries, DistanceRejections; public Vector3 Centre, LastMember; public float Radius; } // Injectable geometry boundary: tests can exercise continuation without installing a scene or native paths. public abstract class EnemySpawnCircleWorld { public readonly Vector3[] Players = new Vector3[4]; public int PlayerCount { get; protected set; } public abstract bool Refresh(); public abstract bool TryProject(Vector3 requested, EnemyDefinition enemy, bool elite, out Vector3 point); public abstract EnemySpawnRejection CheckPoint(Vector3 point, EnemyDefinition enemy, bool elite); public abstract bool TryApproach(Vector3 from, EnemyDefinition enemy, bool elite, int player, int approach, out Vector3 endpoint); public abstract EnemySpawnRouteResult CheckRoute(Vector3 from, Vector3 endpoint, EnemyDefinition enemy, bool elite); public virtual double PressureAt(Vector3 player) => 0; } /// One bounded progressive search per role. Completion never performs an admission. public sealed class EnemySpawnCircleSelector { public const int MaximumCircles = 8, MaximumPointsPerCircle = 32, MaximumSlots = 5; private readonly EnemySpawnCircleWorld world; private readonly Vector3[] slots = new Vector3[MaximumSlots], playerSnapshot = new Vector3[4]; private readonly int[] slotOrder = new int[MaximumSlots]; private readonly bool[] slotUsed = new bool[MaximumSlots]; private readonly Vector3[] memberPlayers = new Vector3[4]; private uint random; private StageSpatialDefinition stage; private EnemyDefinition normal, large; private EnemyDefinition[] compositionProfiles; private EnemyDirectorDefinition.CardSettings[] directorCards; private EnemySpawnCircle candidate; private Vector3 point; private double until; private int slotCount, attempts, routeTier, routePlayer, routeApproach, snapshotCount, steppedFrame = -1; private bool hasCircle, checkingRoutes; private EnemySpawnCircle preparedCircle, memberCircle; private int preparedCount, memberSlot = -1, memberPlayer, memberApproach, memberPlayerCount; private uint preparedGroup; private bool memberPending, memberReady; private Vector3 memberPoint; private StageSpatialDefinition memberStage; private StageSpatialDefinition preparedStage; private MemberProfile memberProfile; private readonly struct MemberProfile : IEquatable { private readonly EnemyRole role; private readonly bool elite; private readonly int normalType, largeType; private readonly float radius, height, top; public MemberProfile(EnemyDefinition enemy, bool elite, float radius, float height, float top) { role = enemy.role; this.elite = enemy.ResolveElite(elite); normalType = enemy.ResolveAgentType(false); largeType = enemy.ResolveAgentType(true); this.radius = radius; this.height = height; this.top = top; } public bool Equals(MemberProfile other) => role == other.role && elite == other.elite && normalType == other.normalType && largeType == other.largeType && radius.Equals(other.radius) && height.Equals(other.height) && top.Equals(other.top); } private EnemySpawnAdmissionDiagnostics diagnostics; public EnemySpawnAdmissionDiagnostics Diagnostics => diagnostics; public EnemySpawnSelectionStatus Status => diagnostics.Status; public int DesiredCount => diagnostics.DesiredCount; public EnemySpawnCircle Circle => candidate; public EnemySpawnCircleSelector(EnemySpawnCircleWorld world, uint seed) { this.world = world ?? throw new ArgumentNullException(nameof(world)); Reset(seed); } public void Reset(uint seed) { random = seed == 0 ? 0x9e3779b9u : seed; Clear(); diagnostics.Seed = seed; } public void Clear() { ClearPreparedSlots(); stage = null; normal = large = null; candidate = default; hasCircle = checkingRoutes = false; directorCards = null; compositionProfiles = null; slotCount = attempts = routeTier = routePlayer = routeApproach = snapshotCount = 0; steppedFrame = -1; diagnostics = default; } public void Consume() { ClearMember(); diagnostics.Pending = false; diagnostics.Status = EnemySpawnSelectionStatus.None; hasCircle = checkingRoutes = false; stage = null; } private void ClearMember() { memberPending = memberReady = false; memberSlot = -1; memberPlayer = memberApproach = memberPlayerCount = 0; memberStage = null; memberCircle = default; memberPoint = default; memberProfile = default; Array.Clear(memberPlayers, 0, memberPlayers.Length); } private void ClearPreparedSlots() { ClearMember(); preparedCount = 0; preparedGroup = 0; preparedCircle = default; preparedStage = null; Array.Clear(slotUsed, 0, slotUsed.Length); } public int NextCount(int minimum, int maximum) => minimum + (int)(Next01() * (maximum - minimum + 1)); private float Next01() { random ^= random << 13; random ^= random >> 17; random ^= random << 5; return (random >> 8) * (1f / 16777216f); } public bool BeginSelection(StageSpatialDefinition definition, EnemyDefinition normalEnemy, EnemyDefinition eliteEnemy, int desired, double now, double duration) { compositionProfiles = null; directorCards = null; ClearPreparedSlots(); if (definition == null || !definition.Valid || definition.spawnMode != StageSpawnMode.DynamicCircles || !definition.DynamicSpawnRulesValid || normalEnemy == null || eliteEnemy == null || !normalEnemy.Valid || !eliteEnemy.Valid || normalEnemy.boss || eliteEnemy.boss || normalEnemy.role != eliteEnemy.role || desired < 1 || desired > MaximumSlots || !Finite(now) || now < 0 || !Finite(duration) || duration <= 0) { Reject(EnemySpawnRejection.InvalidBindings); return false; } stage = definition; normal = normalEnemy; large = eliteEnemy; until = now + duration; slotCount = attempts = routeTier = routePlayer = routeApproach = snapshotCount = 0; hasCircle = checkingRoutes = false; diagnostics.DesiredCount = desired; diagnostics.CandidateCircles = diagnostics.CandidatePoints = 0; diagnostics.Status = EnemySpawnSelectionStatus.Pending; diagnostics.Pending = true; diagnostics.LastRejection = EnemySpawnRejection.None; steppedFrame = -1; return true; } public bool BeginComposition(StageSpatialDefinition stage, EnemyCompositionDefinition composition, double now, double duration) { if (composition == null || !composition.Valid) { Reject(EnemySpawnRejection.InvalidBindings); return false; } var definitions = new EnemyDefinition[composition.members.Length]; for (int i = 0; i < definitions.Length; i++) definitions[i] = composition.members[i].GetComponent().definition; if (!BeginSelection(stage, definitions[0], definitions[0], definitions.Length, now, duration)) return false; compositionProfiles = definitions; return true; } public bool BeginDirectorSelection(StageSpatialDefinition stage, IReadOnlyList cards, double now, double duration) { if (cards == null || cards.Count < 1 || cards.Count > MaximumSlots || !Finite(now + duration)) { Reject(EnemySpawnRejection.InvalidBindings); return false; } var definitions = cards.Select(c => c?.Prefab != null ? c.Prefab.GetComponent()?.definition : null).ToArray(); if (definitions.Any(d => d == null) || !BeginSelection(stage, definitions[0], definitions[0], cards.Count, now, duration)) return false; compositionProfiles = definitions; directorCards = cards.ToArray(); if (!DirectorBindingsValid()) { Reject(EnemySpawnRejection.InvalidBindings); return false; } return true; } private bool DirectorBindingsValid() { if (directorCards == null) return true; for (int i = 0; i < directorCards.Length; i++) { var card = directorCards[i]; var definition = compositionProfiles[i]; bool elite = card.Tier == EnemyDirectorTier.Elite; if (definition == null || !definition.Valid || definition.boss || definition.role != card.Role || definition.archetype != card.Archetype || (elite ? definition.largeAgentType : definition.normalAgentType) != card.AgentType || !EnemySpawnCircleRules.TryResolveClearance(definition, elite, out float radius, out float height, out _) || Mathf.Abs(radius - card.Radius) > .0001f || Mathf.Abs(height - card.Height) > .0001f) return false; } return true; } public EnemySpawnSelectionStatus StepSelection(double now, int frame, out EnemySpawnCircle circle) { circle = candidate; if (Status == EnemySpawnSelectionStatus.Accepted && (!Finite(now) || now >= until)) { Reject(EnemySpawnRejection.Timeout); return Status; } if (Status != EnemySpawnSelectionStatus.Pending) return Status; if (!Finite(now) || now >= until) { Reject(EnemySpawnRejection.Timeout); return Status; } if (stage == null || !stage.Valid || !stage.DynamicSpawnRulesValid || stage.spawnMode != StageSpawnMode.DynamicCircles || !DirectorBindingsValid()) { Reject(EnemySpawnRejection.InvalidBindings); return Status; } if (!world.Refresh() || world.PlayerCount < 1) { Reject(EnemySpawnRejection.NoPlayers); return Status; } if (frame == steppedFrame) return Status; steppedFrame = frame; bool moved = snapshotCount != world.PlayerCount; for (int i = 0; i < world.PlayerCount; i++) { if ((world.Players[i] - playerSnapshot[i]).sqrMagnitude > .0625f) moved = true; playerSnapshot[i] = world.Players[i]; } snapshotCount = world.PlayerCount; if (moved) { hasCircle = checkingRoutes = false; slotCount = attempts = 0; } // CPU candidate work as well as native path queries is bounded per frame. for (int work = 0; work < 8; work++) { if (!hasCircle) { if (diagnostics.CandidateCircles >= MaximumCircles) { Reject(EnemySpawnRejection.InsufficientSlots); break; } diagnostics.CandidateCircles++; var anchor = world.Players[directorCards != null ? DirectorAnchor() : NextCount(0, world.PlayerCount - 1)]; float distance = Mathf.Lerp(stage.minimumSpawnDistance, stage.maximumSpawnDistance, Next01()); float angle = Next01() * Mathf.PI * 2; Vector3 requested = anchor + new Vector3(Mathf.Cos(angle) * distance, 0, Mathf.Sin(angle) * distance); if (!world.TryProject(requested, normal, directorCards != null && directorCards[0].Tier == EnemyDirectorTier.Elite, out var centre) || !stage.movementEnvelope.Contains(centre)) { diagnostics.LastRejection = EnemySpawnRejection.Ground; continue; } candidate = new EnemySpawnCircle(centre, stage.spawnCircleRadius); hasCircle = true; attempts = slotCount = 0; } if (!checkingRoutes) { if (attempts++ >= MaximumPointsPerCircle) { hasCircle = false; slotCount = 0; continue; } diagnostics.CandidatePoints++; var slotProfile = directorCards != null ? compositionProfiles[slotCount] : normal; bool slotElite = directorCards != null && directorCards[slotCount].Tier == EnemyDirectorTier.Elite; if (!TryPoint(candidate, slotProfile, slotElite, out point) || !Separated(point)) continue; if (directorCards == null && !ValidateTier(point, large, true)) continue; bool profilesValid = true; if (compositionProfiles != null && directorCards == null) foreach (var profile in compositionProfiles) if (!ValidateTier(point, profile, false) || !ValidateTier(point, profile, true)) { profilesValid = false; break; } if (!profilesValid) continue; routeTier = routePlayer = routeApproach = 0; checkingRoutes = true; } var route = AdvanceRoutes(); if (route == EnemySpawnRouteResult.Pending) { diagnostics.LastRejection = EnemySpawnRejection.Budget; break; } checkingRoutes = false; if (route == EnemySpawnRouteResult.Rejected) { diagnostics.LastRejection = EnemySpawnRejection.Route; continue; } slots[slotCount++] = point; if (slotCount == diagnostics.DesiredCount) { preparedCircle = candidate; preparedCount = slotCount; preparedStage = stage; for (int i = 0; i < preparedCount; i++) slotOrder[i] = i; for (int i = directorCards != null ? -1 : preparedCount - 1; i > 0; i--) { int other = NextCount(0, i), original = slotOrder[i]; slotOrder[i] = slotOrder[other]; slotOrder[other] = original; } diagnostics.Status = EnemySpawnSelectionStatus.Accepted; diagnostics.Pending = false; diagnostics.Centre = candidate.Centre; diagnostics.Radius = candidate.Radius; diagnostics.LastRejection = EnemySpawnRejection.None; break; } } circle = candidate; return Status; } private EnemySpawnRouteResult AdvanceRoutes() { while (routeTier < (directorCards != null ? 1 : compositionProfiles != null ? compositionProfiles.Length * 2 : 2)) { var enemy = directorCards != null ? compositionProfiles[slotCount] : compositionProfiles != null ? compositionProfiles[routeTier / 2] : routeTier == 0 ? normal : large; bool elite = directorCards != null ? directorCards[slotCount].Tier == EnemyDirectorTier.Elite : compositionProfiles != null ? routeTier % 2 != 0 : routeTier != 0, accepted = false; while (routePlayer < world.PlayerCount) { if (EnemySpawnCircleRules.HorizontalDistance(point, world.Players[routePlayer]) > stage.maximumSpawnDistance) { routePlayer++; routeApproach = 0; continue; } while (routeApproach < 4) { if (!world.TryApproach(point, enemy, elite, routePlayer, routeApproach, out var end)) { routeApproach++; continue; } var result = world.CheckRoute(point, end, enemy, elite); if (result == EnemySpawnRouteResult.Pending) return result; diagnostics.PathQueries++; if (result == EnemySpawnRouteResult.Accepted) { accepted = true; break; } routeApproach++; } if (accepted) break; routePlayer++; routeApproach = 0; } if (!accepted) return EnemySpawnRouteResult.Rejected; routeTier++; routePlayer = routeApproach = 0; } return EnemySpawnRouteResult.Accepted; } private int DirectorAnchor() { double minimum = double.PositiveInfinity; var choices = new List(4); for (int i = 0; i < world.PlayerCount; i++) { double pressure = world.PressureAt(world.Players[i]); if (!Finite(pressure) || pressure < 0) pressure = double.MaxValue; if (pressure < minimum) { minimum = pressure; choices.Clear(); } if (pressure == minimum) choices.Add(i); } return choices[NextCount(0, choices.Count - 1)]; } private bool Separated(Vector3 candidatePoint) { for (int i = 0; i < slotCount; i++) if (Vector3.Distance(candidatePoint, slots[i]) < 2.5f) return false; return true; } private bool TryPoint(EnemySpawnCircle circle, EnemyDefinition enemy, bool elite, out Vector3 projected) { projected = default; var sample = EnemySpawnCircleRules.SamplePoint(circle, Next01(), Next01()); if (!world.TryProject(sample, enemy, elite, out projected)) { diagnostics.LastRejection = EnemySpawnRejection.AgentMesh; return false; } if (!stage.movementEnvelope.Contains(projected)) { diagnostics.LastRejection = EnemySpawnRejection.Bounds; return false; } if (!EnemySpawnCircleRules.AllowsPoint(projected, circle, world.Players, world.PlayerCount, stage.minimumSpawnDistance, stage.maximumSpawnDistance)) { RecordDistanceRejection(); diagnostics.LastRejection = EnemySpawnRejection.Distance; return false; } return ValidateTier(projected, enemy, elite); } private bool ValidateTier(Vector3 projected, EnemyDefinition enemy, bool elite) { bool hasProjection=world.TryProject(projected, enemy, elite, out var tierPoint); if (!hasProjection || Vector3.Distance(tierPoint, projected) > .25f) { diagnostics.LastRejection = EnemySpawnRejection.AgentMesh; return false; } var rejection = world.CheckPoint(projected, enemy, elite); if (rejection == EnemySpawnRejection.Distance) RecordDistanceRejection(); diagnostics.LastRejection = rejection; return rejection == EnemySpawnRejection.None; } private void RecordDistanceRejection() { if (diagnostics.DistanceRejections < long.MaxValue) diagnostics.DistanceRejections++; } public bool TryMemberPoint(StageSpatialDefinition definition, EnemySpawnCircle circle, EnemyDefinition enemy, bool elite, double now, out Vector3 member, uint groupId = 0, int requiredSlot = -1) { member = default; stage = definition; if (stage == null || !stage.Valid || !stage.DynamicSpawnRulesValid || stage.spawnMode != StageSpawnMode.DynamicCircles || !circle.Valid || enemy == null || !enemy.Valid || enemy.boss || !DirectorBindingsValid() || !Finite(now) || now < 0 || !world.Refresh() || world.PlayerCount < 1 || !EnemySpawnCircleRules.TryResolveClearance(enemy, elite, out float radius, out float height, out float top)) { ClearMember(); diagnostics.Pending = false; diagnostics.LastRejection = EnemySpawnRejection.InvalidBindings; return false; } var profile = new MemberProfile(enemy, elite, radius, height, top); if (preparedCount > 0 && (preparedStage != stage || !preparedCircle.Equals(circle) || preparedGroup != 0 && groupId != 0 && preparedGroup != groupId)) ClearPreparedSlots(); if (memberPending && (memberStage != stage || !memberCircle.Equals(circle) || !memberProfile.Equals(profile))) ClearMember(); int preparedCursor = 0, freshAttempts = 0; if (memberPending) { // Revalidate a deferred point before touching its saved route cursor. if (!ValidateMemberPoint(memberPoint, circle, enemy, elite, out var current) || Vector3.Distance(current, memberPoint) > .25f) ClearMember(); else { memberPoint = current; bool changed = memberPlayerCount != world.PlayerCount; for (int i = 0; i < world.PlayerCount; i++) if ((world.Players[i] - memberPlayers[i]).sqrMagnitude > .0625f) changed = true; if (changed || memberReady) { memberPlayer = memberApproach = 0; memberReady = false; CaptureMemberPlayers(); } } } while (true) { if (!memberPending) { bool found = false; int selectedSlot = -1; Vector3 selectedPoint = default; while (preparedCursor < preparedCount) { int slot = slotOrder[preparedCursor++]; if (requiredSlot >= 0 && slot != requiredSlot) continue; if (slotUsed[slot]) continue; diagnostics.CandidatePoints++; if (!ValidateMemberPoint(slots[slot], circle, enemy, elite, out selectedPoint) || Vector3.Distance(selectedPoint, slots[slot]) > .25f) continue; selectedSlot = slot; found = true; break; } while (!found && freshAttempts < 8) { freshAttempts++; diagnostics.CandidatePoints++; var requested = EnemySpawnCircleRules.SamplePoint(circle, Next01(), Next01()); found = ValidateMemberPoint(requested, circle, enemy, elite, out selectedPoint); } if (!found) break; memberPending = true; memberReady = false; memberStage = stage; memberCircle = circle; memberProfile = profile; memberPoint = selectedPoint; memberSlot = selectedSlot; memberPlayer = memberApproach = 0; CaptureMemberPlayers(); } var route = AdvanceMemberRoute(enemy, elite); if (route == EnemySpawnRouteResult.Pending) { diagnostics.Pending = true; diagnostics.LastRejection = EnemySpawnRejection.Budget; return false; } if (route == EnemySpawnRouteResult.Accepted) { memberReady = true; diagnostics.Pending = false; member = memberPoint; diagnostics.LastRejection = EnemySpawnRejection.None; return true; } // Avoid retrying this same prepared slot again within the current bounded call. if (memberSlot >= 0) for (int i = 0; i < preparedCount; i++) if (slotOrder[i] == memberSlot) preparedCursor = Math.Max(preparedCursor, i + 1); ClearMember(); diagnostics.LastRejection = EnemySpawnRejection.Route; } diagnostics.Pending = false; return false; } private void CaptureMemberPlayers() { memberPlayerCount = world.PlayerCount; for (int i = 0; i < world.PlayerCount; i++) memberPlayers[i] = world.Players[i]; } private bool ValidateMemberPoint(Vector3 requested, EnemySpawnCircle circle, EnemyDefinition enemy, bool elite, out Vector3 projected) { projected = default; if (!world.TryProject(requested, enemy, elite, out projected)) { diagnostics.LastRejection = EnemySpawnRejection.AgentMesh; return false; } if (!stage.movementEnvelope.Contains(projected)) { diagnostics.LastRejection = EnemySpawnRejection.Bounds; return false; } if (!EnemySpawnCircleRules.AllowsPoint(projected, circle, world.Players, world.PlayerCount, stage.minimumSpawnDistance, stage.maximumSpawnDistance)) { RecordDistanceRejection(); diagnostics.LastRejection = EnemySpawnRejection.Distance; return false; } bool valid = ValidateTier(projected, enemy, elite); return valid; } private EnemySpawnRouteResult AdvanceMemberRoute(EnemyDefinition enemy, bool elite) { while (memberPlayer < world.PlayerCount) { if (EnemySpawnCircleRules.HorizontalDistance(memberPoint, world.Players[memberPlayer]) > stage.maximumSpawnDistance) { memberPlayer++; memberApproach = 0; continue; } while (memberApproach < 4) { if (!world.TryApproach(memberPoint, enemy, elite, memberPlayer, memberApproach, out var endpoint)) { memberApproach++; continue; } var route = world.CheckRoute(memberPoint, endpoint, enemy, elite); if (route == EnemySpawnRouteResult.Pending) return route; diagnostics.PathQueries++; if (route == EnemySpawnRouteResult.Accepted) return route; memberApproach++; } memberPlayer++; memberApproach = 0; } return EnemySpawnRouteResult.Rejected; } public void RecordAdmission(uint groupId, EnemySpawnCircle circle, Vector3 member) { if (memberReady && memberSlot >= 0 && preparedCircle.Equals(circle) && Vector3.Distance(member, memberPoint) < .001f) slotUsed[memberSlot] = true; if (preparedCount > 0 && preparedCircle.Equals(circle)) preparedGroup = groupId; ClearMember(); diagnostics.Pending = false; diagnostics.GroupId = groupId; diagnostics.Centre = circle.Centre; diagnostics.Radius = circle.Radius; diagnostics.LastMember = member; diagnostics.AcceptedMembers++; diagnostics.LastRejection = EnemySpawnRejection.None; } public void RecordRejection(EnemySpawnRejection rejection) { diagnostics.LastRejection = rejection; } private void Reject(EnemySpawnRejection rejection) { ClearMember(); diagnostics.LastRejection = rejection; diagnostics.Status = EnemySpawnSelectionStatus.Rejected; diagnostics.Pending = false; } private static bool Finite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); } internal sealed class HostEnemySpawnCircleWorld : EnemySpawnCircleWorld { private readonly PrototypeEncounterDirector director; private readonly bool allowVisible; private readonly EnemyAdmissionLedger ledger; private readonly PrototypePlayerVitals[] actors = new PrototypePlayerVitals[4]; private NavMeshPath path; private readonly Vector3[] corners = new Vector3[64]; public HostEnemySpawnCircleWorld(PrototypeEncounterDirector director) : this(director, false, null) { } public HostEnemySpawnCircleWorld(PrototypeEncounterDirector director, bool allowVisible = false, EnemyAdmissionLedger ledger = null) { this.director = director; this.allowVisible = allowVisible; this.ledger = ledger; } public override double PressureAt(Vector3 player) { if (ledger == null) return 0; double pressure = 0; foreach (var admission in ledger.Admissions) { if (!admission.HasRegion || admission.Origin == EnemyAdmissionOrigin.BossEntry) continue; float relevance = Mathf.Max(0, 1 - EnemySpawnCircleRules.HorizontalDistance(admission.Region.Centre, player) / 60); foreach (var member in admission.Members) if (member.Status == EnemyAdmissionMemberStatus.Pending || member.Status == EnemyAdmissionMemberStatus.Born && member.Alive) pressure += relevance * (admission.Origin == EnemyAdmissionOrigin.Purchase ? member.Definition.Cost / 10 : 1); } return pressure; } public override bool Refresh() { PlayerCount = 0; if (director == null || !director.IsServer || !director.IsSpawned || !StageSpatialContext.Bound) return false; foreach (var player in PrototypePlayerVitals.ActivePlayers) { if (player == null || !player.IsSpawned || !player.IsAlive || player.NetworkManager != director.NetworkManager || player.gameObject.scene != director.gameObject.scene) continue; if (PlayerCount == 4 || !EnemyProjectileTrackingRules.Finite(player.AcceptedPosition)) return false; int index = PlayerCount++; while (index > 0 && actors[index - 1].OwnerClientId > player.OwnerClientId) { actors[index] = actors[index - 1]; index--; } actors[index] = player; } for (int i = 0; i < PlayerCount; i++) Players[i] = actors[i].AcceptedPosition; return PlayerCount > 0; } private static NavMeshQueryFilter Filter(EnemyDefinition enemy, bool elite) => new NavMeshQueryFilter { agentTypeID = enemy.ResolveAgentType(elite), areaMask = NavMesh.AllAreas }; public override bool TryProject(Vector3 requested, EnemyDefinition enemy, bool elite, out Vector3 point) { point = default; if (!EnemyProjectileTrackingRules.Finite(requested) || enemy == null || !Physics.Raycast(requested + Vector3.up * 8, Vector3.down, out var ground, 16, LayerMask.GetMask("Default"), QueryTriggerInteraction.Ignore) // Height detail can lie outside the coarse polygon's small search bounds. // Search farther, while retaining the original final placement tolerance below. || ground.normal.y < .707f || !NavMesh.SamplePosition(ground.point, out var nav, 2, Filter(enemy, elite)) || Vector3.Distance(nav.position, ground.point) > .25f) return false; point = nav.position; return true; } public override EnemySpawnRejection CheckPoint(Vector3 point, EnemyDefinition enemy, bool elite) { if (!EnemySpawnCircleRules.TryResolveClearance(enemy, elite, out float radius, out float height, out float visibleHeight)) return EnemySpawnRejection.InvalidBindings; foreach (var other in PrototypeEnemy.ActiveEnemies) if (other != null && other.IsAlive && Vector3.Distance(point, other.transform.position) < 2.5f) return EnemySpawnRejection.Occupied; if (Physics.CheckCapsule(point + Vector3.up * (radius + .05f), point + Vector3.up * (height - radius + .05f), radius, LayerMask.GetMask("Default", "CombatTargets", "Players"), QueryTriggerInteraction.Ignore)) return EnemySpawnRejection.Occupied; for (int i = 0; i < PlayerCount; i++) { if (!allowVisible && PrototypeEncounterDirector.SpawnVisibleToPlayer(actors[i], point, visibleHeight)) return EnemySpawnRejection.Visible; } return EnemySpawnRejection.None; } public override bool TryApproach(Vector3 from, EnemyDefinition enemy, bool elite, int player, int approach, out Vector3 endpoint) { return TryProject(Players[player], enemy, elite, out endpoint); } public override EnemySpawnRouteResult CheckRoute(Vector3 from, Vector3 endpoint, EnemyDefinition enemy, bool elite) { if (!director.TrySpendSpawnPathQuery()) return EnemySpawnRouteResult.Pending; path ??= new NavMeshPath(); return NavMesh.CalculatePath(from, endpoint, Filter(enemy, elite), path) && path.status == NavMeshPathStatus.PathComplete ? EnemySpawnRouteResult.Accepted : EnemySpawnRouteResult.Rejected; } } }