(Feat) Add Build
This commit is contained in:
@@ -8,8 +8,8 @@ namespace Ashwild.UI
|
||||
/// One resource line of a build's cost, shown near the crosshair while placing: an icon plus an
|
||||
/// "owned / required" count (e.g. 2/3). A pure, data-agnostic view — it is handed an icon, the two
|
||||
/// numbers and an "affordable" flag and renders exactly that (tinting the count to signal a
|
||||
/// shortfall), never resolving any domain data itself. CrosshairManager instantiates one of these
|
||||
/// per cost line into its holder.
|
||||
/// shortfall), never resolving any domain data itself. Authored as pre-placed children of the
|
||||
/// crosshair's cost holder — CrosshairManager fills and shows/hides them, it does not instantiate.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class BuildCostEntryUI : MonoBehaviour
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Ashwild.Network;
|
||||
using Ashwild.Player;
|
||||
|
||||
@@ -25,10 +24,6 @@ namespace Ashwild.UI
|
||||
[Tooltip("The pause menu panel opened by Escape.")]
|
||||
[SerializeField] private UIPanel pausePanel;
|
||||
|
||||
[Header("Quit")]
|
||||
[Tooltip("Scene loaded when leaving the session back to the main menu.")]
|
||||
[SerializeField] private string menuSceneName = "MenuScene";
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
@@ -45,18 +40,16 @@ namespace Ashwild.UI
|
||||
private UIPanel buildPanel;
|
||||
|
||||
/// <summary>
|
||||
/// True while a panel that pauses the world is open — i.e. anything but the inventory, the
|
||||
/// construction menu or the chest, all local windows the world keeps running under.
|
||||
/// True while a panel that pauses the world is open — i.e. anything but the inventory (which a
|
||||
/// chest reuses) or the construction menu, both local windows the world keeps running under.
|
||||
/// </summary>
|
||||
public bool IsPaused => Current != null
|
||||
&& Current.Kind != PanelKind.Inventory
|
||||
&& Current.Kind != PanelKind.Build
|
||||
&& Current.Kind != PanelKind.Chest;
|
||||
&& Current.Kind != PanelKind.Build;
|
||||
|
||||
// Last bus values pushed, so we only fire on real transitions.
|
||||
private bool lastPaused;
|
||||
private bool lastInventoryOpen;
|
||||
private bool lastChestOpen;
|
||||
private bool lastBuildOpen;
|
||||
|
||||
#endregion
|
||||
@@ -92,11 +85,6 @@ namespace Ashwild.UI
|
||||
lastInventoryOpen = false;
|
||||
PlayerEvents.RaiseInventoryOpenChanged(false);
|
||||
}
|
||||
if (lastChestOpen)
|
||||
{
|
||||
lastChestOpen = false;
|
||||
PlayerEvents.RaiseChestOpenChanged(false);
|
||||
}
|
||||
if (lastBuildOpen)
|
||||
{
|
||||
lastBuildOpen = false;
|
||||
@@ -119,7 +107,18 @@ namespace Ashwild.UI
|
||||
#region Escape Policy
|
||||
|
||||
/// <summary>
|
||||
/// Escape: close whatever is open (inventory or sub-panel), otherwise open the pause menu.
|
||||
/// Escape, from the most local context outward: close whatever panel is open (inventory or
|
||||
/// sub-panel), else back out of an in-world build placement, else open the pause menu.
|
||||
///
|
||||
/// The placement step matters because a ghost being positioned is a mode with no panel behind it:
|
||||
/// the construction menu closes as soon as a card is picked, so HasOpenPanel is already false and
|
||||
/// Escape used to fall straight through to the pause menu — leaving the player still holding a
|
||||
/// ghost. It is routed through BuildCancelRequested rather than reaching for BuildManager, so
|
||||
/// ending a placement stays the bus contract the hammer's right-click tap already uses.
|
||||
///
|
||||
/// Demolition mode is deliberately not handled here: it lasts only while right-click is held, so
|
||||
/// it ends on release, and cancelling it from the outside would leave the hammer thinking it is
|
||||
/// still held.
|
||||
/// </summary>
|
||||
protected override void OnEscape()
|
||||
{
|
||||
@@ -132,6 +131,12 @@ namespace Ashwild.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerEvents.IsPlacingBuild)
|
||||
{
|
||||
PlayerEvents.RaiseBuildCancelRequested();
|
||||
return;
|
||||
}
|
||||
|
||||
OpenPause();
|
||||
}
|
||||
|
||||
@@ -172,19 +177,19 @@ namespace Ashwild.UI
|
||||
|
||||
/// <summary>
|
||||
/// Leaves the session and returns to the main menu scene behind the loading curtain. The
|
||||
/// transition is owned by the persistent NetworkSessionManager (it survives the scene swap,
|
||||
/// this manager does not); falls back to a direct, uncovered load only if it is missing.
|
||||
/// transition — and the menu scene name — is owned by the persistent NetworkSessionManager (it
|
||||
/// survives the scene swap and is the single source of truth for scene names; this manager does
|
||||
/// neither). Logs and no-ops if it is somehow missing rather than tearing down uncovered.
|
||||
/// </summary>
|
||||
public void QuitToMenu()
|
||||
{
|
||||
if (NetworkSessionManager.Instance != null)
|
||||
if (NetworkSessionManager.Instance == null)
|
||||
{
|
||||
NetworkSessionManager.Instance.ReturnToMenu(menuSceneName);
|
||||
Debug.LogError("[GameUIManager] NetworkSessionManager missing — cannot return to menu.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning("[GameUIManager] NetworkSessionManager missing — loading menu directly without the loading curtain.", this);
|
||||
SceneManager.LoadScene(menuSceneName);
|
||||
NetworkSessionManager.Instance.ReturnToMenu();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -204,9 +209,9 @@ namespace Ashwild.UI
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't open the inventory on top of the pause menu, the construction menu or a chest.
|
||||
// Don't open the inventory on top of the pause menu or the construction menu.
|
||||
if (IsPaused) return;
|
||||
if (Current != null && (Current.Kind == PanelKind.Build || Current.Kind == PanelKind.Chest)) return;
|
||||
if (Current != null && Current.Kind == PanelKind.Build) return;
|
||||
|
||||
if (inventoryPanel != null)
|
||||
OpenPanel(inventoryPanel);
|
||||
@@ -228,7 +233,7 @@ namespace Ashwild.UI
|
||||
}
|
||||
|
||||
if (IsPaused) return;
|
||||
if (Current != null && (Current.Kind == PanelKind.Inventory || Current.Kind == PanelKind.Chest)) return;
|
||||
if (Current != null && Current.Kind == PanelKind.Inventory) return;
|
||||
|
||||
if (buildPanel != null)
|
||||
OpenPanel(buildPanel);
|
||||
@@ -260,19 +265,20 @@ namespace Ashwild.UI
|
||||
|
||||
/// <summary>
|
||||
/// Drives the side effects of the open panel: cursor for any panel, world freeze + GamePaused
|
||||
/// for pausing panels, and the InventoryOpenChanged / ChestOpenChanged / BuildMenuOpenChanged
|
||||
/// bus states for the three local windows. The inventory, the chest and the construction menu
|
||||
/// are windows the world keeps running under, so only "real" menus pause. All four bus states
|
||||
/// feed PlayerEvents.InputLocked, so player control is cut whenever a panel is open.
|
||||
/// for pausing panels, and the InventoryOpenChanged / BuildMenuOpenChanged bus states for the
|
||||
/// two local windows. The inventory (which a chest reuses) and the construction menu are windows
|
||||
/// the world keeps running under, so only "real" menus pause. The ChestOpenChanged sub-state is
|
||||
/// owned by the inventory window itself, since a chest is just the inventory panel in chest mode.
|
||||
/// All these bus states feed PlayerEvents.InputLocked, so player control is cut whenever a panel
|
||||
/// is open.
|
||||
/// </summary>
|
||||
private void ApplyUIState(bool force = false)
|
||||
{
|
||||
UIPanel cur = Current;
|
||||
bool anyOpen = cur != null;
|
||||
bool inventory = anyOpen && cur.Kind == PanelKind.Inventory;
|
||||
bool chest = anyOpen && cur.Kind == PanelKind.Chest;
|
||||
bool build = anyOpen && cur.Kind == PanelKind.Build;
|
||||
bool paused = anyOpen && !inventory && !chest && !build;
|
||||
bool paused = anyOpen && !inventory && !build;
|
||||
|
||||
Cursor.lockState = anyOpen ? CursorLockMode.None : CursorLockMode.Locked;
|
||||
Cursor.visible = anyOpen;
|
||||
@@ -287,11 +293,6 @@ namespace Ashwild.UI
|
||||
lastInventoryOpen = inventory;
|
||||
PlayerEvents.RaiseInventoryOpenChanged(inventory);
|
||||
}
|
||||
if (force || chest != lastChestOpen)
|
||||
{
|
||||
lastChestOpen = chest;
|
||||
PlayerEvents.RaiseChestOpenChanged(chest);
|
||||
}
|
||||
if (force || build != lastBuildOpen)
|
||||
{
|
||||
lastBuildOpen = build;
|
||||
|
||||
@@ -85,7 +85,16 @@ namespace Ashwild.UI
|
||||
PlayerEvents.FuelAdded -= OnItemSpent;
|
||||
}
|
||||
|
||||
private void OnItemAdded(ItemData item, int quantity) => Push(item, quantity);
|
||||
/// <summary>
|
||||
/// Announces a gain — but never a transfer: pulling a stack out of a chest only shuffles items
|
||||
/// between the player's own containers, so popping a "+N" for it would be noise.
|
||||
/// </summary>
|
||||
private void OnItemAdded(ItemData item, int quantity, bool isTransfer)
|
||||
{
|
||||
if (isTransfer) return;
|
||||
Push(item, quantity);
|
||||
}
|
||||
|
||||
private void OnItemDropped(ItemData item, int quantity) => Push(item, -quantity);
|
||||
private void OnItemConsumed(ItemData item) => Push(item, -1);
|
||||
private void OnRecipeDiscovered(Sprite icon, string recipeName) => PushDiscovery(icon, recipeName);
|
||||
|
||||
@@ -6,17 +6,16 @@ namespace Ashwild.UI
|
||||
/// <summary>
|
||||
/// Classifies how a panel affects gameplay when open, so a UI manager can apply the right
|
||||
/// side effects: Default = plain menu panel, Pause = freezes the world / raises GamePaused,
|
||||
/// Inventory = a local UI window where the world keeps running, Build = the construction
|
||||
/// menu, an inventory-like window that locks input and shows the cursor but never pauses,
|
||||
/// Chest = the storage window (inventory + chest), the same kind of local, non-pausing window.
|
||||
/// Inventory = a local UI window where the world keeps running (a chest reuses this same window
|
||||
/// in "chest mode"), Build = the construction menu, an inventory-like window that locks input and
|
||||
/// shows the cursor but never pauses.
|
||||
/// </summary>
|
||||
public enum PanelKind
|
||||
{
|
||||
Default,
|
||||
Pause,
|
||||
Inventory,
|
||||
Build,
|
||||
Chest
|
||||
Build
|
||||
}
|
||||
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
|
||||
Reference in New Issue
Block a user