#if UNITY_EDITOR || DEVELOPMENT_BUILD using System; using ProjectI.Enemies; namespace ProjectI.Debugging { // Captured owner state only. Rendering this receipt never advances an owner or issues a query. public sealed class EnemyBehaviorInspection { public string Identity { get; private set; } public string Phase { get; private set; } = "—"; public string Activity { get; private set; } = "H"; public string Description { get; private set; } = "No active engagement owner"; public string Timer { get; private set; } = "No active task"; public double Remaining { get; private set; } public double Capacity { get; private set; } public double PursuitRemaining { get; private set; } public string PhaseCountdown { get; private set; } = ""; public string LastSearchStop { get; private set; } public string Search { get; private set; } = "No active search"; public float TravelSpent { get; private set; } public float TravelRemaining { get; private set; } public int Predictions { get; private set; } public int PredictionLimit { get; private set; } public int Inspections { get; private set; } public int InspectionLimit { get; private set; } public string Stop { get; private set; } = "Owners inactive"; public string LastMovementStop { get; private set; } public ulong Generation { get; private set; } public double AttemptDeadline { get; private set; } public double SearchDeadline { get; private set; } public double BreakoffUntil { get; private set; } public static float Fraction(double remaining, double capacity) => capacity > 0 && !double.IsInfinity(capacity) && !double.IsNaN(capacity) && !double.IsNaN(remaining) ? (float)Math.Max(0, Math.Min(1, remaining / capacity)) : 0; public static EnemyBehaviorInspection Capture(PrototypeEnemy enemy) { var view = new EnemyBehaviorInspection { Identity = enemy == null ? "Inactive" : $"{enemy.name} · {enemy.SpawnIdentity}" }; var actor = enemy != null ? enemy.GetComponent()?.Actor : null; if (actor == null || !actor.IsValid) return view; double now = actor.Now; var director = actor.Encounter.PressureDirector; EnemyPressureDirector.EngagementReceipt cycle = default; bool hasCycle = director != null && director.TryInspectEngagementServer(actor, out cycle); if (hasCycle) { view.Phase = cycle.Phase.ToString().Substring(0, 1); view.Generation = cycle.Generation; view.AttemptDeadline = cycle.AttemptDeadline; view.BreakoffUntil = cycle.BreakoffUntil; view.Description = cycle.Phase + " · Holding"; view.PursuitRemaining = Math.Max(0, cycle.AttemptDeadline - now); if (cycle.Phase == EnemyEngagementPhase.Pursue) { view.Timer = "Pursuit"; view.Remaining = view.PursuitRemaining; view.Capacity = cycle.AttemptSeconds; } else if (cycle.Phase == EnemyEngagementPhase.Breakoff) { view.Timer = "Breakoff"; view.Remaining = Math.Max(0, cycle.BreakoffUntil - now); view.Capacity = cycle.BreakoffSeconds; } else view.Timer = cycle.Phase == EnemyEngagementPhase.Fight ? "Fight · no deadline" : "No active task"; view.PhaseCountdown = cycle.Phase == EnemyEngagementPhase.Pursue ? $"Pursuit {view.PursuitRemaining:F1}s" : cycle.Phase == EnemyEngagementPhase.Breakoff ? $"Breakoff {view.Remaining:F1}s" : ""; } view.Stop = hasCycle && cycle.Phase == EnemyEngagementPhase.Breakoff ? "Pursuit deadline reached" : "Awaiting reassessment"; var movement = enemy.GetComponent(); var positioning = enemy.GetComponent(); var episode = positioning != null ? positioning.ActiveInvestigation : null; var plan = movement != null ? movement.AcceptedPlan : null; bool moving = movement != null && movement.isActiveAndEnabled && plan != null && plan.Token.IsValidFor(actor) && now < plan.ValidUntil; bool searching = positioning != null && positioning.isActiveAndEnabled && episode != null && !episode.Terminal && now < episode.Deadline && (moving && plan.Token.Equals(episode.Action) || movement != null && movement.isActiveAndEnabled && movement.HasObservation && movement.ObservationToken.Equals(episode.Action) || episode.Phase == EnemyInvestigationPhase.Planning); if (episode != null) { view.SearchDeadline = episode.Deadline; view.Search = searching ? "Search travel left" : "Last search · inactive"; view.TravelSpent = episode.Travelled; view.TravelRemaining = episode.TravelRemaining; view.Predictions = episode.Attempts; view.PredictionLimit = episode.Settings.MaximumAttempts; view.Inspections = episode.Inspections; view.InspectionLimit = episode.Settings.MaximumInspections; if (episode.Terminal && episode.Reason != EnemyInvestigationReason.Reacquired) { view.LastSearchStop = SearchStop(episode); if (!hasCycle || cycle.Phase != EnemyEngagementPhase.Breakoff) view.Stop = "Last search: " + view.LastSearchStop; } } string activity = "Holding"; if (searching) { view.Activity = episode.SearchesWall ? "WS" : "PS"; activity = (episode.SearchesWall ? "Wall search" : "Prediction search") + " · " + episode.Phase; view.Timer = "Search"; view.Remaining = Math.Max(0, episode.Deadline - now); view.Capacity = Math.Max(0, episode.Deadline - episode.CreatedAt); view.Stop = "—"; view.PhaseCountdown += $" · Search {view.Remaining:F1}s"; } else if (moving) { view.Activity = plan.Context?.Kind == EnemyMovementContextKind.AllyReposition ? "AR" : plan.Context?.AssignmentId > 0 ? plan.Context.AssignmentPurpose == EnemyPressureAssignmentPurpose.Regroup ? "RG" : plan.Context.AssignmentPurpose == EnemyPressureAssignmentPurpose.Recovery ? "RA" : "PA" : "DP"; activity = view.Activity == "AR" ? "Ally clearance" : view.Activity == "RG" ? "Allied regroup" : view.Activity == "RA" ? "Recovery approach" : view.Activity == "PA" ? "Pressure approach" : "Direct pursuit"; view.TaskTimer("Move", movement.InspectionDeadline, movement.InspectionDuration, now); view.Stop = "—"; } else if (movement != null && movement.isActiveAndEnabled && movement.HasObservation) { view.Activity = "OB"; activity = "Observing captured evidence"; view.Stop = "—"; view.TaskTimer("Observation", movement.InspectionDeadline, movement.InspectionDuration, now); } var bolt = enemy.GetComponent(); var melee = enemy.GetComponent(); if (bolt != null && bolt.isActiveAndEnabled && (bolt.Phase == EnemyBoltExecution.AttackPhase.Preparation || bolt.Phase == EnemyBoltExecution.AttackPhase.Committed || bolt.Phase == EnemyBoltExecution.AttackPhase.BurstSpacing)) { view.Activity = "SH"; activity = "Shooting · " + bolt.Phase; view.Stop = "—"; view.TaskTimer("Shot " + bolt.Phase, bolt.InspectionDeadline, bolt.InspectionDuration, now); } else if (melee != null && melee.isActiveAndEnabled && (melee.Phase == EnemyMeleePhase.Preparation || melee.Phase == EnemyMeleePhase.Committed || melee.Phase == EnemyMeleePhase.Lunge)) { view.Activity = "ME"; activity = "Melee · " + melee.Phase; view.Stop = "—"; view.TaskTimer("Melee " + melee.Phase, melee.InspectionDeadline, melee.InspectionDuration, now); } else if (!moving && !searching && (bolt != null && bolt.isActiveAndEnabled && bolt.Phase == EnemyBoltExecution.AttackPhase.Recovery && now < bolt.ReadyAt || melee != null && melee.isActiveAndEnabled && melee.Phase == EnemyMeleePhase.Recovery && now < melee.ReadyAt)) { view.Activity = "RC"; activity = "Attack recovery"; view.Stop = "Paid recovery"; bool boltRecovery = bolt != null && bolt.isActiveAndEnabled && bolt.Phase == EnemyBoltExecution.AttackPhase.Recovery && now < bolt.ReadyAt; view.TaskTimer("Paid cooldown", boltRecovery ? bolt.InspectionDeadline : melee.InspectionDeadline, boltRecovery ? bolt.InspectionDuration : melee.InspectionDuration, now); } if (view.Activity == "H" && (episode == null || !episode.Terminal) && director != null && director.TryCaptureTaskInspection(actor, out var task)) view.Stop = hasCycle && cycle.Phase == EnemyEngagementPhase.Breakoff ? "Pursuit deadline reached" : task.Reason; view.LastMovementStop = movement != null ? movement.LastStop : null; if (view.Activity == "H" && movement?.LastOutcome?.Reason == EnemyMovementStopReason.Blocked) view.Stop = "Last route blocked · " + view.Stop; view.Description = (hasCycle ? cycle.Phase.ToString() : "No cycle") + " · " + activity; return view; } private void TaskTimer(string task, double deadline, double duration, double now) { Timer = task; Remaining = Math.Max(0, deadline - now); Capacity = Math.Max(0, duration); } private static string SearchStop(EnemyInvestigationEpisode episode) { bool inspected = episode.HypothesisCount > 0; for (int i = 0; i < episode.HypothesisCount; i++) inspected &= episode.HypothesisChecked(i); return episode.Reason switch { EnemyInvestigationReason.TravelLimit => "Search travel exhausted", EnemyInvestigationReason.AttemptLimit => "Prediction attempts exhausted", EnemyInvestigationReason.InspectionLimit => "Inspection allowance exhausted", EnemyInvestigationReason.NoUsefulViewpoint => inspected ? "Predictions finished" : "No useful prediction viewpoint", EnemyInvestigationReason.NoEligibleGround => "No supported search route", EnemyInvestigationReason.TimeLimit => "Search deadline reached", EnemyInvestigationReason.EvidenceExpired => "Search evidence expired", _ => "Search stopped: " + episode.Reason }; } public override string ToString() => $"{Phase} | {Activity} · {Description}\n{Timer}: {(Capacity > 0 ? $"{Remaining:F1}/{Capacity:F1}s" : "—")}; {PhaseCountdown}; generation {Generation}; attempt deadline {AttemptDeadline:F3}; search deadline {SearchDeadline:F3}; Breakoff until {BreakoffUntil:F3}\n{Search}: {TravelRemaining:F1}m left / {TravelSpent:F1}m spent; predictions {Predictions}/{PredictionLimit}, inspections {Inspections}/{InspectionLimit}\nStop: {Stop}; last search (history): {LastSearchStop}; last movement (history): {LastMovementStop}"; } } #endif