// Assets/_Project/Scripts/UI/HUDController.cs using System.Collections; using UnityEngine; 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 VisualElement commandGrid; private Label ttTitle; private Label ttDesc; private Label ttStats; private Label ttCost; private Label rejectionLabel; // ----- State ------------------------------------------------------ private Coroutine rejectionFadeCoroutine; private bool gridPopulated; private bool uiInitialized; private MinimapView minimapView; private IPanel myPanel; // tracked separately so OnDestroy only clears the static if it still points at us // ----- 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(); TryPopulateCommandGrid(); } 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