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