diff --git a/Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs b/Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
index d180657..b558e2e 100644
--- a/Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
+++ b/Assets/_Project/Scripts/Gameplay/BuildSiteVisual.cs
@@ -4,6 +4,7 @@ using Unity.Netcode;
using UnityEngine;
using TD.Core;
using TD.Towers;
+using TD.UI;
namespace TD.Gameplay
{
@@ -186,6 +187,20 @@ namespace TD.Gameplay
/// Accumulated construction time (across pause/resume cycles). Used on resume.
public float AccumulatedConstructionTime => accumulatedConstructionTime.Value;
+ ///
+ /// Returns [0,1] normalized construction progress. Safe to call on any client.
+ /// Returns 0 while Queued; freezes at accumulated fraction while Paused.
+ ///
+ 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) -------------------------------
private string pendingDefName;
@@ -251,6 +266,12 @@ namespace TD.Gameplay
// Apply initial visual state based on the (now-replicated) values.
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().Initialize(this);
}
public override void OnNetworkDespawn()
diff --git a/Assets/_Project/Scripts/Gameplay/Builder.cs b/Assets/_Project/Scripts/Gameplay/Builder.cs
index 18ca781..a2d0c27 100644
--- a/Assets/_Project/Scripts/Gameplay/Builder.cs
+++ b/Assets/_Project/Scripts/Gameplay/Builder.cs
@@ -156,6 +156,17 @@ namespace TD.Gameplay
/// Maximum jobs allowed in the queue.
public int MaxQueueDepth => settings.maxQueueDepth;
+ /// Display name shown in the HUD portrait. Stub until MatchState provides player names.
+ public string DisplayName
+ {
+ get
+ {
+ PlayerSlot slot = OwnerToSlot(OwnerClientId);
+ int n = (int)slot;
+ return n >= 1 && n <= 9 ? $"Builder (P{n})" : "Builder";
+ }
+ }
+
/// True if a tile is currently part of any queued or constructing job.
///
/// Used by TowerPlacementManager to reject placement on tiles already
diff --git a/Assets/_Project/Scripts/UI/BuildProgressBar.cs b/Assets/_Project/Scripts/UI/BuildProgressBar.cs
new file mode 100644
index 0000000..d0caefe
--- /dev/null
+++ b/Assets/_Project/Scripts/UI/BuildProgressBar.cs
@@ -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