Adding HUD!
This commit is contained in:
parent
ff62b1e9f1
commit
f7720a9915
20 changed files with 1482 additions and 92 deletions
|
|
@ -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>
|
||||
|
|
|
|||
321
Assets/_Project/Scripts/UI/HUDController.cs
Normal file
321
Assets/_Project/Scripts/UI/HUDController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Project/Scripts/UI/HUDController.cs.meta
Normal file
2
Assets/_Project/Scripts/UI/HUDController.cs.meta
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1c69cd177f09f0c419288f73b49df886
|
||||
Loading…
Add table
Add a link
Reference in a new issue