Adding HUD!

This commit is contained in:
Matt F 2026-05-08 21:40:15 -07:00
parent ff62b1e9f1
commit f7720a9915
20 changed files with 1482 additions and 92 deletions

View file

@ -594,6 +594,22 @@ namespace TD.Gameplay
return TryGetDefinition(typeId, out var def) ? def : null;
}
/// <summary>
/// Enumerates all valid (definition, typeId) pairs from the current definition list.
/// TypeId 0 is reserved; valid entries start at index 1. Used by the HUD command grid
/// to populate tower buttons. Only meaningful on the instance — check Instance != null
/// before calling.
/// </summary>
public System.Collections.Generic.IEnumerable<(TowerDefinition def, int typeId)>
GetAvailableDefinitions()
{
for (int i = 1; i < towerDefinitions.Length; i++)
{
if (towerDefinitions[i] != null)
yield return (towerDefinitions[i], i);
}
}
/// <summary>
/// Maps a client ID to the PlayerSlot assigned to that client.
/// </summary>

View file

@ -0,0 +1,321 @@
// Assets/_Project/Scripts/UI/HUDController.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UIElements;
using TD.Gameplay;
using TD.Towers;
namespace TD.UI
{
/// <summary>
/// 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.
/// </summary>
[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;
[Header("Minimap")]
[Tooltip("RenderTexture that the minimap camera renders into.")]
[SerializeField] private RenderTexture minimapRenderTexture;
[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;
// ----- 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<UIDocument>();
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<Label>(root, "gold-label");
waveLabel = Require<Label>(root, "wave-label");
portraitName = Require<Label>(root, "portrait-name");
commandGrid = Require<VisualElement>(root, "command-grid");
ttTitle = Require<Label>(root, "tt-title");
ttDesc = Require<Label>(root, "tt-desc");
ttStats = Require<Label>(root, "tt-stats");
ttCost = Require<Label>(root, "tt-cost");
rejectionLabel = Require<Label>(root, "rejection-label");
// Map area and its transparent ancestors must not consume pointer
// events so clicks reach the 3D scene underneath.
SetPickIgnore(root, "hud-root");
SetPickIgnore(root, "main-area");
SetPickIgnore(root, "map-area");
if (rejectionLabel != null)
rejectionLabel.pickingMode = PickingMode.Ignore;
// Upgrade/sell have no backing system yet.
SetEnabled(root, "upgrade-btn", false);
SetEnabled(root, "sell-btn", false);
// Minimap RenderTexture.
if (minimapRenderTexture != null)
{
var minimap = root.Q<VisualElement>("minimap");
if (minimap != null)
minimap.style.backgroundImage =
Background.FromRenderTexture(minimapRenderTexture);
}
uiInitialized = true;
}
private void OnEnable()
{
TowerPlacementController.OnRejectionMessageReady += ShowRejectionMessage;
}
private void OnDisable()
{
TowerPlacementController.OnRejectionMessageReady -= ShowRejectionMessage;
}
private void Update()
{
if (!uiInitialized) return;
if (!gridPopulated)
TryPopulateCommandGrid();
RefreshGoldDisplay();
}
// ----- Gold display -----------------------------------------------
private void RefreshGoldDisplay()
{
if (goldLabel == null) return;
var gm = PlayerGoldManager.Local;
goldLabel.text = gm != null ? $"{gm.CurrentGold:N0} g" : "-- g";
}
// ----- Command grid -----------------------------------------------
private void TryPopulateCommandGrid()
{
if (commandGrid == null) return;
if (placementManager == null)
placementManager = TowerPlacementManager.Instance;
if (placementManager == null) return; // not spawned yet — retry next frame
commandGrid.Clear();
const int COLS = 4;
const int ROWS = 3;
const int MAX = COLS * ROWS;
var defs = new System.Collections.Generic.List<(TowerDefinition def, int typeId)>();
foreach (var entry in placementManager.GetAvailableDefinitions())
{
defs.Add(entry);
if (defs.Count >= MAX) break;
}
for (int row = 0; row < ROWS; row++)
{
var rowEl = new VisualElement();
rowEl.AddToClassList("cmd-row");
for (int col = 0; col < COLS; col++)
{
int index = row * COLS + col;
VisualElement btn = index < defs.Count
? CreateTowerButton(defs[index].def, defs[index].typeId)
: CreateEmptySlot();
rowEl.Add(btn);
}
commandGrid.Add(rowEl);
}
gridPopulated = true;
}
private VisualElement CreateTowerButton(TowerDefinition def, int typeId)
{
var btn = new Button(() =>
{
if (placementController != null)
placementController.BeginPlacement(def, typeId);
else
Debug.LogWarning("[HUDController] No TowerPlacementController assigned.");
});
btn.AddToClassList("cmd-btn");
// Icon placeholder — swap for background-image on btn when art exists.
var iconPlaceholder = new VisualElement();
iconPlaceholder.AddToClassList("cmd-icon-placeholder");
iconPlaceholder.pickingMode = PickingMode.Ignore;
var costLabel = new Label($"{def.GoldCost}g");
costLabel.AddToClassList("cmd-cost");
costLabel.pickingMode = PickingMode.Ignore;
btn.Add(iconPlaceholder);
btn.Add(costLabel);
btn.RegisterCallback<MouseEnterEvent>(_ => ShowTooltip(def));
btn.RegisterCallback<MouseLeaveEvent>(_ => ClearTooltip());
return btn;
}
private static VisualElement CreateEmptySlot()
{
var slot = new VisualElement();
slot.AddToClassList("cmd-btn");
slot.AddToClassList("empty-slot");
slot.pickingMode = PickingMode.Ignore;
return slot;
}
// ----- Portrait / selection context ----------------------------------
/// <summary>
/// Called by the selection system when a unit is selected or deselected.
/// Pass null or empty to clear (nothing selected).
/// </summary>
public void SetSelectedUnitName(string unitName)
{
if (portraitName == null) return;
portraitName.text = string.IsNullOrEmpty(unitName) ? "" : unitName;
}
// ----- Tooltip ----------------------------------------------------
private void ShowTooltip(TowerDefinition def)
{
if (ttTitle == null) return;
ttTitle.text = def.DisplayName;
ttDesc.text = def.Description ?? "";
if (def.Damage > 0 || def.Range > 0)
{
ttStats.text = $"DMG: {def.Damage} · Range: {def.Range:0}";
if (def.FireRate > 0)
ttStats.text += $"\nFire rate: {def.FireRate:0.0}/s";
if (def.SlowFactor < 1f)
ttStats.text += $"\nSlow: {(1f - def.SlowFactor) * 100f:0}%";
}
else
{
ttStats.text = "(stats pending)";
}
int sellValue = Mathf.RoundToInt(def.GoldCost * 0.7f);
ttCost.text = $"Cost: {def.GoldCost}g · Sell: {sellValue}g";
}
private void ClearTooltip()
{
if (ttTitle == null) return;
ttTitle.text = "";
ttDesc.text = "";
ttStats.text = "";
ttCost.text = "";
}
// ----- Rejection messages -----------------------------------------
private void ShowRejectionMessage(string message)
{
if (rejectionLabel == null) return;
rejectionLabel.text = message;
rejectionLabel.style.display = DisplayStyle.Flex;
if (rejectionFadeCoroutine != null)
StopCoroutine(rejectionFadeCoroutine);
rejectionFadeCoroutine = StartCoroutine(HideRejectionAfterDelay());
}
private IEnumerator HideRejectionAfterDelay()
{
yield return new WaitForSeconds(rejectionMessageDuration);
if (rejectionLabel != null)
rejectionLabel.style.display = DisplayStyle.None;
rejectionFadeCoroutine = null;
}
// ----- Helpers ----------------------------------------------------
private static T Require<T>(VisualElement root, string name) where T : VisualElement
{
var el = root.Q<T>(name);
if (el == null)
Debug.LogWarning($"[HUDController] Element not found: '{name}' ({typeof(T).Name}). " +
$"Check that the name matches the UXML.");
return el;
}
private static void SetPickIgnore(VisualElement root, string name)
{
var el = root.Q<VisualElement>(name);
if (el != null) el.pickingMode = PickingMode.Ignore;
}
private static void SetEnabled(VisualElement root, string name, bool enabled)
{
var el = root.Q<Button>(name);
if (el != null) el.SetEnabled(enabled);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1c69cd177f09f0c419288f73b49df886