using System.Collections.Generic; using UnityEngine; namespace jeanf.scenemanagement { /// /// A place to send the player when the zone they stand in is about to be locked by a /// scenario unload. Positions are world space and are resolved at the moment of the /// evacuation, never cached across frames. /// public readonly struct ZoneExit { public readonly Vector3 Position; public readonly Quaternion Rotation; /// Zone this exit lands in, empty when the author did not declare it. public readonly string DestinationZoneId; /// Human-readable origin of the exit (door name, anchor name) - logs only. public readonly string SourceName; public ZoneExit(Vector3 position, Quaternion rotation, string destinationZoneId, string sourceName) { Position = position; Rotation = rotation; DestinationZoneId = destinationZoneId ?? string.Empty; SourceName = sourceName ?? string.Empty; } } /// /// Anything able to say "here is the way out of zone X". Implemented by /// and, when the door package is installed, by its /// DoorZoneEvacuationProvider. /// /// The dependency runs this way round on purpose: AutomaticDoorSystem already references /// jeanf.scenemanagement, so SceneManagement cannot reference it back. Providers push /// themselves into instead, which also keeps the door /// package optional - a project with no doors just registers anchors. /// /// public interface IZoneEvacuationProvider { /// Zone the player must leave. /// Where the player stands - used to pick the nearest way out. /// /// Every zone being locked by this unload. An exit landing in one of them is no exit; /// providers must skip those. Null means "nothing else is blocked". /// bool TryGetExit(string zoneId, Vector3 from, IReadOnlyCollection blockedZoneIds, out ZoneExit exit); } /// /// Where asks for a way out of a zone it is about to lock. /// A zone with no registered exit is treated as "not lockable" (typically a zone without a /// door): the manager logs it and unloads without moving anybody. /// public static class ZoneEvacuationRegistry { private static readonly List Providers = new List(8); public static int ProviderCount => Providers.Count; public static void Register(IZoneEvacuationProvider provider) { if (provider == null || Providers.Contains(provider)) return; Providers.Add(provider); } public static void Unregister(IZoneEvacuationProvider provider) { if (provider == null) return; Providers.Remove(provider); } /// Nearest exit out of across every provider. public static bool TryResolveExit(string zoneId, Vector3 from, IReadOnlyCollection blockedZoneIds, out ZoneExit exit) { exit = default; if (string.IsNullOrEmpty(zoneId) || Providers.Count == 0) return false; var found = false; var bestDistanceSq = float.MaxValue; for (var i = 0; i < Providers.Count; i++) { var provider = Providers[i]; if (provider == null) continue; if (!provider.TryGetExit(zoneId, from, blockedZoneIds, out var candidate)) continue; var distanceSq = (candidate.Position - from).sqrMagnitude; if (found && distanceSq >= bestDistanceSq) continue; bestDistanceSq = distanceSq; exit = candidate; found = true; } return found; } /// Does this zone have a way out at all - i.e. can it be locked safely? public static bool HasExitFor(string zoneId, IReadOnlyCollection blockedZoneIds) => TryResolveExit(zoneId, Vector3.zero, blockedZoneIds, out _); /// /// Membership test providers use on the blocked set. Kept here so every provider agrees on /// the convention (null or empty = nothing else is blocked) and so a HashSet keeps its O(1) /// lookup instead of falling back to a linear scan. /// public static bool IsBlocked(IReadOnlyCollection blockedZoneIds, string zoneId) { if (blockedZoneIds == null || blockedZoneIds.Count == 0 || string.IsNullOrEmpty(zoneId)) return false; if (blockedZoneIds is ICollection collection) return collection.Contains(zoneId); foreach (var blocked in blockedZoneIds) if (blocked == zoneId) return true; return false; } } }