using System; using ProjectI.Runs; using UnityEngine; namespace ProjectI.Enemies { [Serializable] public readonly struct EnemySpawnCircle : IEquatable { public readonly Vector3 Centre; public readonly float Radius; public EnemySpawnCircle(Vector3 centre, float radius) { Centre = centre; Radius = radius; } public bool Valid => EnemyProjectileTrackingRules.Finite(Centre) && Finite(Radius) && Radius > 0; public bool Equals(EnemySpawnCircle other) => Centre.Equals(other.Centre) && Radius.Equals(other.Radius); public override bool Equals(object obj) => obj is EnemySpawnCircle other && Equals(other); public override int GetHashCode() => HashCode.Combine(Centre, Radius); private static bool Finite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); } public static class EnemySpawnCircleRules { public static bool AllowsDynamicBinding(StageSpatialDefinition stage, string actualScene, bool bound) => stage != null && bound && stage.spawnMode == StageSpawnMode.DynamicCircles && stage.Valid && stage.sceneName == actualScene && stage.DynamicSpawnRulesValid; public static bool TryResolveClearance(EnemyDefinition enemy, bool elite, out float radius, out float height, out float visibleHeight) { radius = height = visibleHeight = 0; if (enemy == null || enemy.boss || !Enum.IsDefined(typeof(EnemyRole), enemy.role)) return false; elite = enemy.ResolveElite(elite); if (enemy.archetype != EnemyArchetype.Standard && enemy.archetype != EnemyArchetype.Bombardier) return EnemyRosterGeometry.TryResolve(enemy.archetype, elite, out radius, out height, out visibleHeight); float size = elite ? 1.35f : 1; if (enemy.role == EnemyRole.Melee) { if (!Finite(enemy.meleeVisualScale) || enemy.meleeVisualScale <= 0) return false; size *= enemy.meleeVisualScale; radius = .45f * size; height = Mathf.Max(.975f * size, Mathf.Max(.9f * size, radius * 2)); visibleHeight = 1.1f * size; } else { // OnNetworkSpawn navigation and ApplySize collider dimensions are distinct. // Clearance must cover both; visibility remains the tier-scaled mesh top. radius = Mathf.Max(elite ? .65f : .45f, .45f * size); height = Mathf.Max(elite ? 2.4f : 1.8f, Mathf.Max(1.4f * size, radius * 2)); visibleHeight = 1.85f * size; } return Finite(radius) && Finite(height) && Finite(visibleHeight) && radius > 0 && height >= radius * 2 && visibleHeight > 0; } public static Vector3 SamplePoint(EnemySpawnCircle circle, float radial01, float angle01) { if (!circle.Valid || !Finite(radial01) || !Finite(angle01) || radial01 < 0 || radial01 > 1 || angle01 < 0 || angle01 > 1) throw new ArgumentOutOfRangeException("Circle samples require finite unit-interval inputs and a valid circle."); float radius = circle.Radius * Mathf.Sqrt(radial01), angle = angle01 * Mathf.PI * 2; return circle.Centre + new Vector3(Mathf.Cos(angle) * radius, 0, Mathf.Sin(angle) * radius); } public static bool AllowsPoint(Vector3 point, EnemySpawnCircle circle, Vector3[] livingPlayers, float minimum, float maximum) => AllowsPoint(point, circle, livingPlayers, livingPlayers != null ? livingPlayers.Length : 0, minimum, maximum); internal static bool AllowsPoint(Vector3 point, EnemySpawnCircle circle, Vector3[] livingPlayers, int count, float minimum, float maximum) { if (!EnemyProjectileTrackingRules.Finite(point) || !circle.Valid || livingPlayers == null || count < 1 || count > livingPlayers.Length || !Finite(minimum) || !Finite(maximum) || minimum < 0 || maximum <= minimum || HorizontalDistance(point, circle.Centre) > circle.Radius) return false; bool nearOne = false; for (int i = 0; i < count; i++) { if (!EnemyProjectileTrackingRules.Finite(livingPlayers[i])) return false; float distance = HorizontalDistance(point, livingPlayers[i]); if (distance <= minimum) return false; if (distance <= maximum) nearOne = true; } return nearOne; } public static float HorizontalDistance(Vector3 a, Vector3 b) { var delta = a - b; delta.y = 0; return delta.magnitude; } private static bool Finite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); } }