using System.Linq; using CultMath; using static CultMath.math; // The run's goal: whether a zone's boss lives, and so whether the exit gate is open. Engine-free and stateless; the // answer is derived from Zone.Entities (the death path removes the dead, Zone.cs), so no "boss defeated" flag exists to // disagree with it, and no presentation decides it. public static class RunGoal { public static Entity Boss(Zone zone) => zone.Entities.FirstOrDefault(entity => entity.IsBoss); public static bool BossAlive(Zone zone) => Boss(zone) != null; // The exit gate is open in the galaxy's exit zone once that zone holds no living boss. public static bool ExitOpen(Zone zone) => zone.Galaxy?.Exit != null && zone.GalaxyZone == zone.Galaxy.Exit && !BossAlive(zone); // Where the exit gate sits: at the adjacency wormholes' radius (the zone's radius times the ratio), in the middle of // the widest angular gap between them, so it shares a point with none. A zone with no neighbours gets the +x axis. public static float2 ExitGatePosition(Zone zone, float wormholeDistanceRatio) { var radius = zone.Pack.Radius * wormholeDistanceRatio; var angles = zone.GalaxyZone.AdjacentZones .Select(adjacent => normalize(adjacent.Position - zone.GalaxyZone.Position)) .Select(direction => atan2(direction.y, direction.x)) .OrderBy(angle => angle) .ToArray(); if (angles.Length == 0) return float2(radius, 0); var bestAngle = 0f; var bestGap = -1f; for (var i = 0; i < angles.Length; i++) { var next = i + 1 < angles.Length ? angles[i + 1] : angles[0] + 2 * PI; if (next - angles[i] <= bestGap) continue; bestGap = next - angles[i]; bestAngle = angles[i] + bestGap / 2; } return float2(cos(bestAngle), sin(bestAngle)) * radius; } }