(Feat) Add Build

This commit is contained in:
2026-07-22 12:56:13 +02:00
parent 3657b870d8
commit 0052b0d98e
244 changed files with 35752 additions and 3112 deletions
+88 -41
View File
@@ -41,17 +41,36 @@ namespace Ashwild.UI
/// </summary>
[SerializeField] private TMP_Text promptLabel;
[Header("Build Cost")]
[Header("Layout")]
/// <summary>
/// Container the per-resource cost entries are parented under while a build is being placed.
/// Its own layout (e.g. a HorizontalLayoutGroup) arranges them.
/// The vertical group stacking the crosshair icon, prompt and cost holder. Its spacing widens in
/// build mode so the build cost readout gets room below the reticle, and tightens back otherwise.
/// </summary>
[SerializeField] private RectTransform costHolder;
[SerializeField] private VerticalLayoutGroup verticalGroup;
/// <summary>
/// Prefab instantiated once per cost line (icon + amount) into the holder. Pooled and reused.
/// Spacing applied to the vertical group outside build mode (the compact reticle).
/// </summary>
[SerializeField] private BuildCostEntryUI costEntryPrefab;
[SerializeField] private float idleSpacing = 0f;
/// <summary>
/// Spacing applied to the vertical group while building, to separate the reticle from the cost.
/// </summary>
[SerializeField] private float buildSpacing = 12f;
[Header("Build Cost")]
/// <summary>
/// The "Ressources Needed" container holding the cost entries. Activated while a build cost is
/// shown and deactivated otherwise, so the whole readout (background, layout) disappears with it.
/// </summary>
[SerializeField] private GameObject costHolder;
/// <summary>
/// The pre-placed cost entries, assigned in the inspector in display order. Authored in the UI
/// (not instantiated at runtime) — this manager only fills and shows/hides them per the active
/// build's cost.
/// </summary>
[SerializeField] private BuildCostEntryUI[] costEntries;
[Header("States")]
/// <summary>
@@ -121,12 +140,6 @@ namespace Ashwild.UI
/// </summary>
private bool isShowingCost;
/// <summary>
/// The instantiated cost entries, pooled and reused across placements — activated up to the
/// current cost's line count and hidden beyond it.
/// </summary>
private readonly List<BuildCostEntryUI> costEntries = new List<BuildCostEntryUI>();
#endregion
#region Unity Lifecycle
@@ -144,6 +157,7 @@ namespace Ashwild.UI
}
Instance = this;
HideCostEntries();
}
/// <summary>
@@ -153,8 +167,10 @@ namespace Ashwild.UI
{
PlayerEvents.InteractableHoverChanged += HandleHoverChanged;
PlayerEvents.BuildCostChanged += HandleBuildCostChanged;
PlayerEvents.BuildModeChanged += HandleBuildModeChanged;
ApplyState(idleState, isHover: false, animated: false);
SetLabel(null, animated: false);
ApplySpacing(PlayerEvents.IsBuilding);
}
/// <summary>
@@ -164,6 +180,7 @@ namespace Ashwild.UI
{
PlayerEvents.InteractableHoverChanged -= HandleHoverChanged;
PlayerEvents.BuildCostChanged -= HandleBuildCostChanged;
PlayerEvents.BuildModeChanged -= HandleBuildModeChanged;
}
/// <summary>
@@ -211,6 +228,12 @@ namespace Ashwild.UI
if (!isShowingCost) SetLabel(currentPrompt, animated: true);
}
/// <summary>
/// Widens the vertical group in build mode (so the cost readout gets room) and tightens it back
/// out of build mode. Pure view reaction to the composite bus state — no build logic here.
/// </summary>
private void HandleBuildModeChanged(bool building) => ApplySpacing(building);
/// <summary>
/// Shows the active build's cost (hiding the prompt label and filling the holder) while placing,
/// or clears it and restores the normal prompt when the list is null/empty (placement ended).
@@ -224,6 +247,7 @@ namespace Ashwild.UI
}
isShowingCost = true;
if (costHolder != null) costHolder.SetActive(true);
SetLabel(null, animated: true);
PopulateCost(cost);
}
@@ -265,60 +289,70 @@ namespace Ashwild.UI
}
/// <summary>
/// Sets the prompt text and fades the label in (non-null) or out (null).
/// Sets the prompt text and shows/hides the label. The label object starts inactive in the
/// prefab, so it is activated when there is a prompt and deactivated when there is none — the
/// alpha fade only runs while it is active.
/// </summary>
private void SetLabel(string prompt, bool animated)
{
if (promptLabel == null) return;
bool show = !string.IsNullOrEmpty(prompt);
if (show)
promptLabel.text = prompt;
promptLabel.gameObject.SetActive(show);
if (!show) return;
promptLabel.text = prompt;
labelTween?.Kill();
float target = show ? 1f : 0f;
if (!animated)
{
promptLabel.alpha = target;
promptLabel.alpha = 1f;
return;
}
promptLabel.alpha = 0f;
labelTween = DOTween.To(() => promptLabel.alpha,
a => promptLabel.alpha = a, target, labelFadeDuration);
a => promptLabel.alpha = a, 1f, labelFadeDuration);
}
/// <summary>
/// Fills the holder with one entry per cost line, reusing pooled entries and only instantiating
/// when the current build needs more lines than any previous one. Entries beyond this build's
/// count are hidden. Logs and no-ops when the holder or prefab reference is missing.
/// Hides the cost holder and every inspector-assigned cost entry at startup, so PopulateCost only
/// ever fills and toggles them. No-op on the parts that are unassigned.
/// </summary>
private void HideCostEntries()
{
if (costHolder != null) costHolder.SetActive(false);
if (costEntries == null) return;
for (int i = 0; i < costEntries.Length; i++)
if (costEntries[i] != null) costEntries[i].gameObject.SetActive(false);
}
/// <summary>
/// Fills and shows one assigned entry per cost line, hiding the entries beyond this build's line
/// count. Warns and caps when a build needs more lines than there are assigned entries — add more
/// entries in the inspector rather than relying on runtime instantiation.
/// </summary>
private void PopulateCost(IReadOnlyList<BuildCostView> cost)
{
if (costHolder == null || costEntryPrefab == null)
if (costEntries == null || costEntries.Length == 0)
{
Debug.LogError("[CrosshairManager] Cost holder or entry prefab not assigned — cannot show the build cost.", this);
Debug.LogError("[CrosshairManager] No cost entries assigned — cannot show the build cost.", this);
return;
}
for (int i = 0; i < cost.Count; i++)
{
BuildCostEntryUI entry;
if (i < costEntries.Count)
{
entry = costEntries[i];
}
else
{
entry = Instantiate(costEntryPrefab, costHolder);
costEntries.Add(entry);
}
int shown = Mathf.Min(cost.Count, costEntries.Length);
if (cost.Count > costEntries.Length)
Debug.LogWarning($"[CrosshairManager] Build needs {cost.Count} cost lines but only {costEntries.Length} entries exist — extra lines are hidden.", this);
entry.gameObject.SetActive(true);
entry.Set(cost[i].Icon, cost[i].Owned, cost[i].Amount, cost[i].Affordable);
for (int i = 0; i < shown; i++)
{
if (costEntries[i] == null) continue;
costEntries[i].gameObject.SetActive(true);
costEntries[i].Set(cost[i].Icon, cost[i].Owned, cost[i].Amount, cost[i].Affordable);
}
for (int i = cost.Count; i < costEntries.Count; i++)
for (int i = shown; i < costEntries.Length; i++)
if (costEntries[i] != null) costEntries[i].gameObject.SetActive(false);
}
@@ -331,12 +365,25 @@ namespace Ashwild.UI
if (!isShowingCost) return;
isShowingCost = false;
for (int i = 0; i < costEntries.Count; i++)
if (costEntries[i] != null) costEntries[i].gameObject.SetActive(false);
if (costEntries != null)
for (int i = 0; i < costEntries.Length; i++)
if (costEntries[i] != null) costEntries[i].gameObject.SetActive(false);
if (costHolder != null) costHolder.SetActive(false);
SetLabel(currentPrompt, animated: true);
}
/// <summary>
/// Sets the vertical group's spacing to the build or idle value. No-op when no group is wired,
/// so the crosshair still works on a rig that has no layout group.
/// </summary>
private void ApplySpacing(bool building)
{
if (verticalGroup == null) return;
verticalGroup.spacing = building ? buildSpacing : idleSpacing;
}
/// <summary>
/// Kills every cached tween so none survive a disable/destroy.
/// </summary>