#if UNITY_EDITOR || DEVELOPMENT_BUILD using System; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Text; using ProjectI.Enemies; namespace ProjectI.Debugging { public static class EnemyDirectorDebugView { public sealed class Snapshot { public string Summary { get; } public ReadOnlyCollection History { get; } public ReadOnlyCollection GroupHistory { get; } internal Snapshot(string summary, string[] history, string[] groups) { Summary = summary; History = Array.AsReadOnly(history); GroupHistory = Array.AsReadOnly(groups); } } public static Snapshot Inactive { get; } = new Snapshot("No active director. Start the S2 arrival or S3 boss fixture in Shooter sandbox.", Array.Empty(), Array.Empty()); public static Snapshot Capture(EnemyAdmissionLedger ledger, EnemySpawnScheduler scheduler, EncounterPhase phase, double now) { if (ledger == null || double.IsNaN(now) || double.IsInfinity(now)) return Inactive; var text = new StringBuilder(); text.AppendLine($"{ledger.Profile.Id} · {phase} · generation {ledger.ProfileGeneration}"); text.AppendLine($"Bank: {Number(ledger.AvailableCredit)} / {Number(ledger.Profile.BankCeiling)} credits"); text.AppendLine($"Nonboss: {ledger.LiveCount} live + {ledger.PendingCount} pending / {ledger.Profile.AdmissionLimit} limit"); if (!ledger.Active) text.AppendLine("Stopped · no further purchases or births."); else if (!ledger.BudgetActive) text.AppendLine("Awaiting boss entry · reinforcements paused."); else { var intent = scheduler?.Saving; if (intent == null) text.AppendLine("Saving: no active intent"); else { string target = string.Join(" + ", intent.Choice.Members.GroupBy(c => c.Id).Select(g => $"{g.Count()}× {g.Key}")); text.AppendLine($"Saving: {Source(intent.Choice.Mode, intent.Choice.TemplateId)} · {target}"); text.AppendLine($"Target {Number(intent.Choice.TotalCost)} credits · need {Number(Math.Max(0, intent.Choice.TotalCost - ledger.AvailableCredit))} · " + (now > intent.ExpiresAt ? "expired; awaiting next opportunity" : "expires in " + Seconds(intent.ExpiresAt, now))); } text.AppendLine(scheduler == null ? "Next purchase attempt: not scheduled" : $"Next purchase attempt: {Seconds(scheduler.NextOpportunityAt, now)}"); if (scheduler != null && scheduler.PendingAdmissionId != 0 && ledger.TryGetAdmission(scheduler.PendingAdmissionId, out var pending) && pending.NextPendingSlot >= 0) text.AppendLine($"Next pending-member placement attempt: {Seconds(scheduler.NextMemberAt, now)}"); else text.AppendLine("Pending-member placement: none"); } var rows = ledger.RecentBirths.Reverse().Select(b => $"{Number(Math.Max(0, now - b.BornAt))} s ago · {b.EnemyName}{(b.Tier == EnemyDirectorTier.Elite ? " [Elite]" : "")} · #{b.AdmissionId}\n" + $"{(b.Origin == EnemyAdmissionOrigin.Purchase ? Source(b.SelectionMode.Value, b.TemplateId) + " · " + Number(b.Cost) + " cr · " + b.CardId : b.Origin == EnemyAdmissionOrigin.BossEntry ? "Boss entry · unpriced" : "Parent death · unpriced")} · {b.ProfileId}").ToArray(); var groups = ledger.RecentBirthGroups.Select(group => { var latest = group.OrderByDescending(b => b.BornAt).First(); string members = string.Join(", ", group.GroupBy(b => (b.CardId, b.EnemyName, b.Tier)) .Select(g => $"{g.Count()}× {g.Key.EnemyName}{(g.Key.Tier == EnemyDirectorTier.Elite ? " [Elite]" : "")}")); string source = latest.Origin == EnemyAdmissionOrigin.Purchase ? Source(latest.SelectionMode.Value, latest.TemplateId) : latest.Origin == EnemyAdmissionOrigin.BossEntry ? "Boss entry" : "Parent death"; return $"{Number(Math.Max(0, now - latest.BornAt))} s ago · #{latest.AdmissionId} · {source}\n{members} · {latest.ProfileId}"; }).ToArray(); return new Snapshot(text.ToString().TrimEnd(), rows, groups); } private static string Source(EnemySelectionMode mode, string template) => mode == EnemySelectionMode.Template ? "Template: " + template : mode == EnemySelectionMode.Procedural ? "Procedural" : "Single"; private static string Number(double value) => value.ToString("0.0", CultureInfo.InvariantCulture); private static string Seconds(double deadline, double now) => Number(Math.Max(0, deadline - now)) + " s"; } } #endif