Finish Phase 1.2 HUD: selection wiring, command grid context, build progress bar

- Builder.DisplayName: stub property ("Builder (P{n})") for portrait label; swaps
  to real player name when MatchState lands in Phase 1.3
- HUDController: subscribe SelectionState.OnSelectionChanged in OnEnable/OnDisable;
  HandleSelectionChanged drives portrait label + grays out command grid when nothing
  is selected; Start() seeds initial state in case builder auto-selected before Start
- BuildSiteVisual: ComputeProgressNormalized() public API ([0,1], safe on any client);
  OnNetworkSpawn spawns a non-networked BuildProgressBar child
- BuildProgressBar: new world-space uGUI Canvas bar; green while Constructing, yellow
  while Paused, hidden while Queued; billboards to Camera.main each LateUpdate;
  auto-destroyed when parent BuildSiteVisual despawns

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt F 2026-05-11 18:03:34 -07:00
parent 6c37e569ab
commit bc557af624
4 changed files with 135 additions and 0 deletions

View file

@ -0,0 +1,90 @@
// Assets/_Project/Scripts/UI/BuildProgressBar.cs
using UnityEngine;
using UnityEngine.UI;
using TD.Gameplay;
namespace TD.UI
{
// Local (non-networked) world-space progress bar that tracks a BuildSiteVisual.
// Visible while Constructing (green) or Paused (yellow). Hidden while Queued.
// Billboards to face Camera.main each LateUpdate.
// Destroyed automatically when its parent BuildSiteVisual is despawned.
public class BuildProgressBar : MonoBehaviour
{
private BuildSiteVisual source;
private GameObject canvasGO;
private Image fillImage;
private const float BarWorldWidth = 1.8f;
private const float BarWorldHeight = 0.15f;
private const float HeightAboveSite = 1.5f;
private static readonly Color ColorConstructing = new Color(0.15f, 0.85f, 0.15f, 1f);
private static readonly Color ColorPaused = new Color(0.90f, 0.75f, 0.10f, 1f);
public void Initialize(BuildSiteVisual visual)
{
source = visual;
BuildHierarchy();
}
private void BuildHierarchy()
{
// World-space Canvas — 100 canvas units = 1 world unit via localScale 0.01.
canvasGO = new GameObject("Canvas");
canvasGO.transform.SetParent(transform, false);
var canvas = canvasGO.AddComponent<Canvas>();
canvas.renderMode = RenderMode.WorldSpace;
canvas.sortingOrder = 10;
var rt = (RectTransform)canvasGO.transform;
rt.sizeDelta = new Vector2(BarWorldWidth * 100f, BarWorldHeight * 100f);
rt.localPosition = new Vector3(0f, HeightAboveSite, 0f);
rt.localScale = Vector3.one * 0.01f;
// Background
var bgGO = new GameObject("Background");
bgGO.transform.SetParent(canvasGO.transform, false);
var bgImg = bgGO.AddComponent<Image>();
bgImg.color = new Color(0.05f, 0.05f, 0.05f, 0.85f);
Stretch((RectTransform)bgGO.transform);
// Fill (rendered on top; fillAmount drives visible width)
var fillGO = new GameObject("Fill");
fillGO.transform.SetParent(canvasGO.transform, false);
fillImage = fillGO.AddComponent<Image>();
fillImage.color = ColorConstructing;
fillImage.type = Image.Type.Filled;
fillImage.fillMethod = Image.FillMethod.Horizontal;
fillImage.fillOrigin = 0; // left to right
fillImage.fillAmount = 0f;
Stretch((RectTransform)fillGO.transform);
}
private static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
}
private void LateUpdate()
{
if (source == null) return;
var stage = source.CurrentStage;
bool show = stage == BuildStage.Constructing || stage == BuildStage.Paused;
canvasGO.SetActive(show);
if (!show) return;
fillImage.color = stage == BuildStage.Paused ? ColorPaused : ColorConstructing;
fillImage.fillAmount = source.ComputeProgressNormalized();
var cam = Camera.main;
if (cam != null)
canvasGO.transform.rotation = cam.transform.rotation;
}
}
}