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