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(); + 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(); + 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(); + 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; + } + } +} diff --git a/Assets/_Project/Scripts/UI/HUDController.cs b/Assets/_Project/Scripts/UI/HUDController.cs index 616917c..b89bba2 100644 --- a/Assets/_Project/Scripts/UI/HUDController.cs +++ b/Assets/_Project/Scripts/UI/HUDController.cs @@ -105,6 +105,8 @@ namespace TD.UI // Start() is safe because all OnEnable() calls have completed by then. InitializeUI(); TryPopulateCommandGrid(); + // Seed portrait/grid state in case the builder already auto-selected before Start. + HandleSelectionChanged(SelectionState.Instance?.SelectedBuilder); } private void InitializeUI() @@ -172,11 +174,15 @@ namespace TD.UI private void OnEnable() { TowerPlacementController.OnRejectionMessageReady += ShowRejectionMessage; + if (SelectionState.Instance != null) + SelectionState.Instance.OnSelectionChanged += HandleSelectionChanged; } private void OnDisable() { TowerPlacementController.OnRejectionMessageReady -= ShowRejectionMessage; + if (SelectionState.Instance != null) + SelectionState.Instance.OnSelectionChanged -= HandleSelectionChanged; } private void Update() @@ -304,6 +310,13 @@ namespace TD.UI 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 ---------------------------------------------------- private void ShowTooltip(TowerDefinition def)