// Assets/_Project/Scripts/UI/HUDController.cs using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UIElements; using TD.Gameplay; using TD.Towers; using TD.UI.Minimap; namespace TD.UI { /// /// Drives the in-match HUD. Requires a UIDocument on the same GameObject. /// Wires gold display, tower command grid, tooltip, rejection messages, /// and minimap RenderTexture to their UI Toolkit counterparts. /// [RequireComponent(typeof(UIDocument))] public class HUDController : MonoBehaviour { // ----- Inspector -------------------------------------------------- [Header("Scene References")] [Tooltip("The local client's TowerPlacementController.")] [SerializeField] private TowerPlacementController placementController; [Tooltip("The TowerPlacementManager NetworkObject in the scene.")] [SerializeField] private TowerPlacementManager placementManager; [Tooltip("The local client's CameraController. Used by the minimap for click-to-jump " + "and drag-to-pan.")] [SerializeField] private CameraController cameraController; [Header("Settings")] [SerializeField] private float rejectionMessageDuration = 2.5f; // ----- Cached UI element references ------------------------------- private Label goldLabel; private Label waveLabel; private Label portraitName; private Label levelLabel; private VisualElement statLines; private VisualElement commandGrid; private VisualElement actionFrame; // hidden via display:none when no actions are available private VisualElement buildProgressContainer; // info-panel sub-view, shown for BuildSiteVisual selections private VisualElement buildProgressFill; // width driven each frame from progress private Label buildProgressPercent; private Label ttTitle; private Label ttDesc; private Label ttStats; private Label ttCost; private Label rejectionLabel; // ----- State ------------------------------------------------------ private Coroutine rejectionFadeCoroutine; private bool placementManagerReady; // true once TowerPlacementManager.Instance is non-null private bool uiInitialized; private bool selectionSubscribed; // true once we've successfully hooked SelectionState.OnSelectionChanged private MinimapView minimapView; private IPanel myPanel; // tracked separately so OnDestroy only clears the static if it still points at us // ----- Hotkeys ---------------------------------------------------- // // Per-slot hotkey layout matching the WC3 / Wintermaul Reforged convention. // Slot index 0..14 corresponds to row-major position in the 5×3 action grid. // To rebind, edit this array. (Per-tower hotkeys could live on TowerDefinition // later; for now the position-based layout is enough and predictable.) private static readonly Key[] HotkeyLayout = { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.A, Key.S, Key.D, Key.F, Key.G, Key.Z, Key.X, Key.C, Key.V, Key.B, }; // Active hotkey bindings — rebuilt on every selection change inside // PopulateGridForSelection. HandleHotkeys reads this every Update. private readonly List hotkeyBindings = new List(); private readonly struct HotkeyBinding { public readonly Key Key; public readonly VisualElement Button; // for enabledSelf gating public readonly System.Action Action; public HotkeyBinding(Key k, VisualElement b, System.Action a) { Key = k; Button = b; Action = a; } } // ----- Static hit-test probe -------------------------------------- // Set when InitializeUI succeeds; cleared on OnDestroy. Non-UI systems (camera, // input handlers) can query IsPointerOverInteractiveHud without taking a direct // reference to HUDController. private static IPanel s_hudPanel; /// /// True if falls over an interactive (non-ignore) /// HUD element. Non-UI systems that consume mouse input (camera scroll-zoom, edge-pan) /// should gate their handling on this so a cursor over the minimap, command grid, or /// any other interactive HUD region doesn't drive both the HUD and the world at once. /// /// /// Convention: uses Unity Input System screen coords /// (origin bottom-left, y up). Returns false before the HUD has initialized; safe to /// call from any system at any time. /// public static bool IsPointerOverInteractiveHud(Vector2 screenMousePos) { if (s_hudPanel == null) return false; // Coord convention rabbit hole: // - Screen mouse position: origin bottom-left, y up (Unity Input System). // - UI Toolkit panel coords: origin top-left, y down. // // RuntimePanelUtils.ScreenToPanel converts the SCALE (e.g., reference resolution // vs. actual resolution) but does NOT flip Y. We flip manually using the visual // tree's height so the result works regardless of PanelSettings scale mode. // // Subtle: visualTree.worldBound height may be 0 for one frame on the very first // layout pass. The caller (CameraController) checks the result against "is over // interactive HUD"; a one-frame false positive (camera zooms when it shouldn't) // is harmless and self-corrects the next frame. Vector2 scaled = RuntimePanelUtils.ScreenToPanel(s_hudPanel, screenMousePos); float panelHeight = s_hudPanel.visualTree.worldBound.height; Vector2 panelPos = new Vector2(scaled.x, panelHeight - scaled.y); // panel.Pick returns null when the topmost element under the point has // PickingMode.Ignore (or there's no element there at all). Non-null means an // interactive HUD element is under the cursor. return s_hudPanel.Pick(panelPos) != null; } // ----- Lifecycle -------------------------------------------------- private void Start() { // UIDocument creates its panel in OnEnable, which runs after all // Awake() calls. Accessing rootVisualElement in Awake() is too early. // Start() is safe because all OnEnable() calls have completed by then. InitializeUI(); TryReadyPlacementManager(); // Subscription fallback: if OnEnable couldn't subscribe (SelectionState.Awake // hadn't run yet), Start() is guaranteed to be after all Awake calls in the // scene. Retry here. Without this fallback, the HUD silently misses every // selection event for the rest of the session. TrySubscribeSelection(); // Seed portrait/grid in case the builder already auto-selected before Start. HandleSelectionChanged(SelectionState.Instance?.SelectedObject); } private void InitializeUI() { var doc = GetComponent(); if (doc == null) { Debug.LogError("[HUDController] No UIDocument component found."); return; } var root = doc.rootVisualElement; if (root == null) { Debug.LogError("[HUDController] rootVisualElement is null. " + "Check that Panel Settings and Source Asset are assigned."); return; } // Cache element references — log a warning for any that are missing // so UXML/USS mismatches surface immediately. goldLabel = Require