// 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