using System; using System.Collections.Generic; using Unity.Netcode; namespace ProjectI.Runs { public enum RunPhase { Menu, Loading, Active, Collection, TravelCountdown, Defeated, Selection } public sealed class RunRules { private static ulong nextRunIdentity; public RunPhase Phase { get; private set; } public ulong RunId { get; private set; } public ulong StageId { get; private set; } public int CurrentStage { get; private set; } public int CompletedStages { get; private set; } public double StartedAt { get; private set; } public double EndedAt { get; private set; } public int StageParticipantCount { get; private set; } private double activeSeconds, activeClock = double.NaN, directorDifficulty = 1; public bool TryCaptureDirectorInputs(double now, out RunDirectorInputs inputs) { inputs = default; if (Phase == RunPhase.Menu || RunId == 0 || !Finite(now) || (!double.IsNaN(activeClock) && now < activeClock)) return false; double seconds = activeSeconds; if (Phase == RunPhase.Active && !double.IsNaN(activeClock)) seconds = Math.Min(double.MaxValue, seconds + (now - activeClock)); inputs = new RunDirectorInputs(RunId, StageId, CurrentStage, StageParticipantCount, seconds / 60, directorDifficulty); return true; } public void StartWithDirectorDifficulty(ulong[] owners, double now, double difficulty) { if (!Finite(difficulty) || difficulty <= 0) return; ulong before = RunId; Start(owners, now); if (RunId != before) directorDifficulty = difficulty; } public readonly HashSet Participants = new HashSet(); private bool bossClear; private readonly HashSet portalOutside = new HashSet(); private readonly HashSet portalInside = new HashSet(); public int PortalReadyCount => portalInside.Count; public bool AllPortalReady => Phase == RunPhase.Collection && Participants.Count > 0 && portalInside.Count == Participants.Count; public double TravelAt { get; private set; } public void Start(ulong[] owners, double now) { if (owners == null || owners.Length == 0 || !Finite(now)) return; if(nextRunIdentity==ulong.MaxValue||StageId==ulong.MaxValue)return; RunId=checked(++nextRunIdentity); StageId++; CurrentStage = 1; CompletedStages = 0; StartedAt = now; EndedAt = 0; Participants.Clear(); foreach (var owner in owners) Participants.Add(owner); StageParticipantCount = Participants.Count; bossClear = false; portalOutside.Clear(); portalInside.Clear(); TravelAt = 0; Phase = RunPhase.Active; activeSeconds = 0; activeClock = now; directorDifficulty = 1; } public void QueueBossClear() { if (Phase == RunPhase.Active) bossClear = true; } public void CommitTick(double now, int livingParticipants) { if (Phase != RunPhase.Active || !Finite(now)) return; if (double.IsNaN(activeClock)) activeClock = now; else if (now >= activeClock) { activeSeconds = Math.Min(double.MaxValue, activeSeconds + (now - activeClock)); activeClock = now; } if (livingParticipants <= 0 && Participants.Count > 0) { Phase = RunPhase.Defeated; EndedAt = now; bossClear = false; return; } if (bossClear) { CompletedStages++; Phase = RunPhase.Collection; bossClear = false; } } public void RemoveParticipant(ulong owner) { Participants.Remove(owner); portalOutside.Remove(owner); portalInside.Remove(owner); } public void ObservePortalPosition(ulong owner, bool alive, bool inside) { if (Phase != RunPhase.Collection || !Participants.Contains(owner)) return; if (!alive) { portalInside.Remove(owner); portalOutside.Remove(owner); return; } if (!inside) { portalOutside.Add(owner); portalInside.Remove(owner); } else if (portalOutside.Contains(owner)) portalInside.Add(owner); } public void InvalidatePortalSpatialEvidence(ulong owner) { if (Phase != RunPhase.Collection || !Participants.Contains(owner)) return; portalInside.Remove(owner); portalOutside.Remove(owner); } public bool SealCollection() { if (!AllPortalReady) return false; Phase = RunPhase.Selection; return true; } public bool BeginTravelAfterChoices(bool anyPending, double now) { if (Phase != RunPhase.Selection || Participants.Count == 0 || anyPending || !Finite(now)) return false; TravelAt = now + 3; Phase = RunPhase.TravelCountdown; return true; } public bool BeginLoading(double now) { if (Phase != RunPhase.TravelCountdown || !Finite(now) || now < TravelAt) return false; Phase = RunPhase.Loading; CurrentStage++; StageId++; return true; } public bool FinishLoading() { if (Phase != RunPhase.Loading || Participants.Count == 0) return false; StageParticipantCount = Participants.Count; portalInside.Clear(); portalOutside.Clear(); Phase = RunPhase.Active; activeClock = double.NaN; return true; } public bool FinishLoadingAt(double now) { if (!Finite(now) || !FinishLoading()) return false; activeClock = now; return true; } public void Reset() { Phase = RunPhase.Menu; Participants.Clear(); portalInside.Clear(); portalOutside.Clear(); bossClear = false; CurrentStage = CompletedStages = 0; TravelAt = 0; activeSeconds = 0; activeClock = double.NaN; directorDifficulty = 1; } public static bool AllowsMovement(RunPhase phase) => phase == RunPhase.Menu || phase == RunPhase.Active || phase == RunPhase.Collection; public static bool AllowsShieldRecharge(RunPhase phase) => phase == RunPhase.Menu || phase == RunPhase.Active || phase == RunPhase.Collection; public static bool AllowsCoreCollection(RunPhase phase) => phase == RunPhase.Active || phase == RunPhase.Collection; public static float HealthScale(int stage) => (float)Math.Min(8, Math.Pow(1.20, Math.Max(0, Math.Min(100,stage - 1)))); public static float DamageScale(int stage) => (float)Math.Min(3, Math.Pow(1.08, Math.Max(0, Math.Min(100,stage - 1)))); private static bool Finite(double value) => value >= 0 && !double.IsNaN(value) && !double.IsInfinity(value); } public struct RunSnapshot : INetworkSerializable, IEquatable { public RunPhase Phase; public ulong RunId, StageId; public int CurrentStage, CompletedStages, Participants, ConnectedParticipants, PortalReadyCount, RewardReadyParticipants; public double TravelAt; public void NetworkSerialize(BufferSerializer s) where T : IReaderWriter { s.SerializeValue(ref Phase); s.SerializeValue(ref RunId); s.SerializeValue(ref StageId); s.SerializeValue(ref CurrentStage); s.SerializeValue(ref CompletedStages); s.SerializeValue(ref Participants); s.SerializeValue(ref ConnectedParticipants); s.SerializeValue(ref PortalReadyCount); s.SerializeValue(ref RewardReadyParticipants); s.SerializeValue(ref TravelAt); } public bool Equals(RunSnapshot other) => Phase == other.Phase && RunId == other.RunId && StageId == other.StageId && CurrentStage == other.CurrentStage && CompletedStages == other.CompletedStages && Participants == other.Participants && ConnectedParticipants == other.ConnectedParticipants && PortalReadyCount == other.PortalReadyCount && RewardReadyParticipants == other.RewardReadyParticipants && TravelAt == other.TravelAt; } [Serializable] public sealed class PlayerRunResult { public ulong Owner; public string Name; public int Kills, Assists, Revives, Downs, UpgradesAcquired; public float DirectDamage, EffectDamage, DamageTaken, Healing; public UpgradeInventoryEntry[] Inventory = Array.Empty(); public string[] Weapons = Array.Empty(); public PlayerTelemetrySnapshot Telemetry; } [Serializable] public sealed class RunResult { public ulong RunId; public int CurrentStage, CompletedStages, TeamKills, NormalKills, EliteKills, BossKills; public double Duration; public string Outcome; public PlayerRunResult[] Players = Array.Empty(); public RunTelemetrySnapshot Telemetry; } }