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:
parent
6c37e569ab
commit
bc557af624
4 changed files with 135 additions and 0 deletions
|
|
@ -4,6 +4,7 @@ using Unity.Netcode;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using TD.Core;
|
using TD.Core;
|
||||||
using TD.Towers;
|
using TD.Towers;
|
||||||
|
using TD.UI;
|
||||||
|
|
||||||
namespace TD.Gameplay
|
namespace TD.Gameplay
|
||||||
{
|
{
|
||||||
|
|
@ -186,6 +187,20 @@ namespace TD.Gameplay
|
||||||
/// <summary>Accumulated construction time (across pause/resume cycles). Used on resume.</summary>
|
/// <summary>Accumulated construction time (across pause/resume cycles). Used on resume.</summary>
|
||||||
public float AccumulatedConstructionTime => accumulatedConstructionTime.Value;
|
public float AccumulatedConstructionTime => accumulatedConstructionTime.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns [0,1] normalized construction progress. Safe to call on any client.
|
||||||
|
/// Returns 0 while Queued; freezes at accumulated fraction while Paused.
|
||||||
|
/// </summary>
|
||||||
|
public float ComputeProgressNormalized()
|
||||||
|
{
|
||||||
|
float bt = buildTime.Value;
|
||||||
|
if (bt <= 0f) return 0f;
|
||||||
|
float currentRunElapsed = constructionStartServerTime.Value > 0f
|
||||||
|
? (float)NetworkManager.Singleton.ServerTime.Time - constructionStartServerTime.Value
|
||||||
|
: 0f;
|
||||||
|
return Mathf.Clamp01((currentRunElapsed + accumulatedConstructionTime.Value) / bt);
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Pre-spawn init data (server) -------------------------------
|
// ----- Pre-spawn init data (server) -------------------------------
|
||||||
|
|
||||||
private string pendingDefName;
|
private string pendingDefName;
|
||||||
|
|
@ -251,6 +266,12 @@ namespace TD.Gameplay
|
||||||
|
|
||||||
// Apply initial visual state based on the (now-replicated) values.
|
// Apply initial visual state based on the (now-replicated) values.
|
||||||
ApplyStageVisual(currentStage.Value);
|
ApplyStageVisual(currentStage.Value);
|
||||||
|
|
||||||
|
// Attach a local (non-networked) progress bar — each client creates its own.
|
||||||
|
// Destroyed automatically when this NetworkObject is despawned (it's a child).
|
||||||
|
var barHost = new GameObject("ProgressBar");
|
||||||
|
barHost.transform.SetParent(transform, false);
|
||||||
|
barHost.AddComponent<BuildProgressBar>().Initialize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNetworkDespawn()
|
public override void OnNetworkDespawn()
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,17 @@ namespace TD.Gameplay
|
||||||
/// <summary>Maximum jobs allowed in the queue.</summary>
|
/// <summary>Maximum jobs allowed in the queue.</summary>
|
||||||
public int MaxQueueDepth => settings.maxQueueDepth;
|
public int MaxQueueDepth => settings.maxQueueDepth;
|
||||||
|
|
||||||
|
/// <summary>Display name shown in the HUD portrait. Stub until MatchState provides player names.</summary>
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
PlayerSlot slot = OwnerToSlot(OwnerClientId);
|
||||||
|
int n = (int)slot;
|
||||||
|
return n >= 1 && n <= 9 ? $"Builder (P{n})" : "Builder";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>True if a tile is currently part of any queued or constructing job.</summary>
|
/// <summary>True if a tile is currently part of any queued or constructing job.</summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Used by <c>TowerPlacementManager</c> to reject placement on tiles already
|
/// Used by <c>TowerPlacementManager</c> to reject placement on tiles already
|
||||||
|
|
|
||||||
90
Assets/_Project/Scripts/UI/BuildProgressBar.cs
Normal file
90
Assets/_Project/Scripts/UI/BuildProgressBar.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -105,6 +105,8 @@ namespace TD.UI
|
||||||
// Start() is safe because all OnEnable() calls have completed by then.
|
// Start() is safe because all OnEnable() calls have completed by then.
|
||||||
InitializeUI();
|
InitializeUI();
|
||||||
TryPopulateCommandGrid();
|
TryPopulateCommandGrid();
|
||||||
|
// Seed portrait/grid state in case the builder already auto-selected before Start.
|
||||||
|
HandleSelectionChanged(SelectionState.Instance?.SelectedBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeUI()
|
private void InitializeUI()
|
||||||
|
|
@ -172,11 +174,15 @@ namespace TD.UI
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
TowerPlacementController.OnRejectionMessageReady += ShowRejectionMessage;
|
TowerPlacementController.OnRejectionMessageReady += ShowRejectionMessage;
|
||||||
|
if (SelectionState.Instance != null)
|
||||||
|
SelectionState.Instance.OnSelectionChanged += HandleSelectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisable()
|
private void OnDisable()
|
||||||
{
|
{
|
||||||
TowerPlacementController.OnRejectionMessageReady -= ShowRejectionMessage;
|
TowerPlacementController.OnRejectionMessageReady -= ShowRejectionMessage;
|
||||||
|
if (SelectionState.Instance != null)
|
||||||
|
SelectionState.Instance.OnSelectionChanged -= HandleSelectionChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
|
|
@ -304,6 +310,13 @@ namespace TD.UI
|
||||||
portraitName.text = string.IsNullOrEmpty(unitName) ? "" : unitName;
|
portraitName.text = string.IsNullOrEmpty(unitName) ? "" : unitName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleSelectionChanged(Builder builder)
|
||||||
|
{
|
||||||
|
SetSelectedUnitName(builder != null ? builder.DisplayName : null);
|
||||||
|
if (commandGrid != null)
|
||||||
|
commandGrid.SetEnabled(builder != null);
|
||||||
|
}
|
||||||
|
|
||||||
// ----- Tooltip ----------------------------------------------------
|
// ----- Tooltip ----------------------------------------------------
|
||||||
|
|
||||||
private void ShowTooltip(TowerDefinition def)
|
private void ShowTooltip(TowerDefinition def)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue