#if UNITY_EDITOR || DEVELOPMENT_BUILD using System.Collections.Generic; using System.Linq; using ProjectI.Enemies; using UnityEngine; using UnityEngine.UIElements; namespace ProjectI.Debugging { public sealed class EnemyBehaviorOverhead : VisualElement { private readonly Label identity, state, description, time, enclosing, search, counts, stop; private readonly VisualElement timeFill, searchFill; public const float PanelWidth = 206; public EnemyBehaviorOverhead() { name = "enemy-overhead"; pickingMode = PickingMode.Ignore; style.position = Position.Absolute; style.width = PanelWidth; style.backgroundColor = new Color(.06f, .09f, .12f, .94f); style.color = new Color(.95f, .97f, 1); style.paddingLeft = style.paddingRight = 8; style.paddingTop = style.paddingBottom = 6; identity = Text(10); state = Text(23); description = Text(11); time = Text(11); timeFill = Bar(); enclosing = Text(10); search = Text(11); searchFill = Bar(); counts = Text(10); stop = Text(11); } private Label Text(int size) { var label = new Label { pickingMode = PickingMode.Ignore }; label.style.fontSize = size; label.style.whiteSpace = WhiteSpace.Normal; label.style.flexShrink = 0; Add(label); return label; } private VisualElement Bar() { var track = new VisualElement { pickingMode = PickingMode.Ignore }; track.style.height = 5; track.style.backgroundColor = new Color(.2f, .26f, .31f); track.style.marginTop = track.style.marginBottom = 3; var fill = new VisualElement { pickingMode = PickingMode.Ignore }; fill.style.height = Length.Percent(100); track.Add(fill); Add(track); return fill; } public void Refresh(EnemyBehaviorInspection view) { Color phase = view.Phase == "P" ? new Color(.43f, .84f, 1) : view.Phase == "F" ? new Color(1, .72f, .45f) : view.Phase == "B" ? new Color(.93f, .62f, .65f) : new Color(.71f, .78f, .82f); identity.text = view.Identity; state.text = view.Phase + " | " + view.Activity; state.style.color = phase; description.text = view.Description; time.text = view.Timer + (view.Capacity > 0 ? $" {view.Remaining:F1} / {view.Capacity:F1}s" : ""); timeFill.style.width = Length.Percent(100 * EnemyBehaviorInspection.Fraction(view.Remaining, view.Capacity)); timeFill.style.backgroundColor = phase; enclosing.text = view.PhaseCountdown; search.text = view.Search + (view.PredictionLimit > 0 ? $" {view.TravelRemaining:F1}m" : ""); searchFill.style.width = Length.Percent(100 * EnemyBehaviorInspection.Fraction(view.TravelRemaining, view.TravelSpent + view.TravelRemaining)); searchFill.style.backgroundColor = new Color(.77f, .63f, .94f); counts.text = view.PredictionLimit > 0 ? $"Spent {view.TravelSpent:F1}m · Pred {view.Predictions}/{view.PredictionLimit} · Inspect {view.Inspections}/{view.InspectionLimit}" : ""; stop.text = "Stop: " + view.Stop; } } public sealed partial class EnemyBehaviorScenarioDriver { public bool ShowEnemyOverheads = true; private VisualElement overheadRoot; private readonly Dictionary overheads = new Dictionary(); private double nextOverheadCapture; private void ClearEnemyOverheads() { overheadRoot?.RemoveFromHierarchy(); overheadRoot = null; overheads.Clear(); } private void LateUpdate() { if (!ShowEnemyOverheads || !Ready || Busy || Stopped || controller == null || controller.document == null || Player == null) { ClearEnemyOverheads(); return; } var camera = Player.GetComponentInChildren(); if (camera == null || !camera.isActiveAndEnabled) { ClearEnemyOverheads(); return; } var root = controller.document.rootVisualElement; if (overheadRoot == null) { overheadRoot = new VisualElement { name = "enemy-overhead-layer", pickingMode = PickingMode.Ignore }; overheadRoot.style.position = Position.Absolute; overheadRoot.style.left = overheadRoot.style.right = overheadRoot.style.top = overheadRoot.style.bottom = 0; root.Add(overheadRoot); } if (overheadRoot.parent != root) root.Add(overheadRoot); var live = enemies.Where(enemy => enemy != null && enemy.IsSpawned).ToArray(); foreach (ulong id in overheads.Keys.Where(id => !live.Any(enemy => enemy.SpawnIdentity == id)).ToArray()) { overheads[id].RemoveFromHierarchy(); overheads.Remove(id); } bool capture = Time.unscaledTimeAsDouble >= nextOverheadCapture; if (capture) nextOverheadCapture = Time.unscaledTimeAsDouble + .1; foreach (var enemy in live) { bool fresh = !overheads.TryGetValue(enemy.SpawnIdentity, out var panel); if (fresh) { panel = new EnemyBehaviorOverhead(); overheads.Add(enemy.SpawnIdentity, panel); overheadRoot.Add(panel); } if (fresh || capture) panel.Refresh(EnemyBehaviorInspection.Capture(enemy)); // Screen projection keeps every label facing the local player's camera without rotating an actor. Vector3 point = camera.WorldToViewportPoint(enemy.transform.position + Vector3.up * (enemy.Navigation.height + .35f)); bool visible = point.z > 0 && point.x >= 0 && point.x <= 1 && point.y >= 0 && point.y <= 1; panel.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None; if (!visible) continue; float height = float.IsNaN(panel.layout.height) || panel.layout.height <= 0 ? 180 : panel.layout.height; panel.style.left = Mathf.Clamp(point.x * root.layout.width - EnemyBehaviorOverhead.PanelWidth * .5f, 0, Mathf.Max(0, root.layout.width - EnemyBehaviorOverhead.PanelWidth)); panel.style.top = Mathf.Max(0, (1 - point.y) * root.layout.height - height); } } } } #endif