(Feat) Add build cost
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using FishNet.Object;
|
||||
using UnityEngine;
|
||||
using Ashwild.Inventory;
|
||||
using Ashwild.Player;
|
||||
|
||||
namespace Ashwild.Building
|
||||
@@ -51,6 +52,10 @@ namespace Ashwild.Building
|
||||
[Tooltip("Degrees the ghost yaws per scroll notch while placing.")]
|
||||
[SerializeField] private float rotationStep = 90f;
|
||||
|
||||
[Header("Placement Mode")]
|
||||
[Tooltip("Continuous build: after confirming a placement, immediately re-arm another ghost of the same structure (to chain walls/floors) instead of returning to idle. Off = place one at a time.")]
|
||||
[SerializeField] private bool continuousBuild = false;
|
||||
|
||||
[Header("Snapping")]
|
||||
[Tooltip("Radius around the aim in which a matching existing socket pulls the ghost in.")]
|
||||
[SerializeField] private float snapRadius = 1.25f;
|
||||
@@ -125,6 +130,12 @@ namespace Ashwild.Building
|
||||
|
||||
private readonly Collider[] snapResults = new Collider[16];
|
||||
|
||||
/// <summary>
|
||||
/// Reused buffer for the active build's cost lines, rebuilt and pushed to the crosshair each
|
||||
/// time the price or its affordability changes — so no per-refresh allocation.
|
||||
/// </summary>
|
||||
private readonly List<BuildCostView> costView = new List<BuildCostView>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
@@ -156,6 +167,7 @@ namespace Ashwild.Building
|
||||
PlayerEvents.HotbarScroll += HandleRotate;
|
||||
PlayerEvents.BuildRotatePressed += HandleRotateKey;
|
||||
PlayerEvents.DemolishModeChanged += HandleDemolishModeChanged;
|
||||
PlayerEvents.InventorySlotChanged += HandleInventoryChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -169,6 +181,7 @@ namespace Ashwild.Building
|
||||
PlayerEvents.HotbarScroll -= HandleRotate;
|
||||
PlayerEvents.BuildRotatePressed -= HandleRotateKey;
|
||||
PlayerEvents.DemolishModeChanged -= HandleDemolishModeChanged;
|
||||
PlayerEvents.InventorySlotChanged -= HandleInventoryChanged;
|
||||
|
||||
EndPlacement();
|
||||
ClearDemoTarget();
|
||||
@@ -267,6 +280,7 @@ namespace Ashwild.Building
|
||||
|
||||
PlayerEvents.RaiseBuildMenuToggleRequested();
|
||||
PlayerEvents.RaiseBuildPlacingChanged(true);
|
||||
PublishCostView();
|
||||
BuildRegistry.Instance.RequestSpawnGhost(id, token);
|
||||
}
|
||||
|
||||
@@ -303,8 +317,11 @@ namespace Ashwild.Building
|
||||
|
||||
/// <summary>
|
||||
/// Left-click: demolishes the aimed build while in demolition mode, otherwise commits the
|
||||
/// ghost's pose to the registry (spawned for everyone) and ends the placement. Placement is
|
||||
/// blocked on an invalid spot and on the very frame the ghost was attached.
|
||||
/// ghost's pose to the registry (spawned for everyone) and consumes its resource cost from the
|
||||
/// local inventory (client-authoritative, exactly like crafting). In continuous build mode it
|
||||
/// then re-arms a fresh ghost of the same structure (keeping the current yaw, so pieces chain
|
||||
/// edge-to-edge); otherwise it ends the placement. Blocked on an invalid spot, on the very frame
|
||||
/// the ghost was attached, and when the player cannot afford the cost.
|
||||
/// </summary>
|
||||
private void HandleConfirm()
|
||||
{
|
||||
@@ -318,10 +335,19 @@ namespace Ashwild.Building
|
||||
if (PlayerEvents.InputLocked) return;
|
||||
if (Time.frameCount == placementBeganFrame) return;
|
||||
if (ghostValidity != null && !ghostValidity.IsValid) return;
|
||||
if (active != null && !active.CanAfford(PlayerInventory.Instance)) return;
|
||||
|
||||
if (BuildRegistry.Instance != null)
|
||||
BuildRegistry.Instance.RequestBuild(activeId, ghost.transform.position, ghost.transform.rotation);
|
||||
|
||||
ConsumeCost();
|
||||
|
||||
if (continuousBuild && active != null)
|
||||
{
|
||||
RearmGhost();
|
||||
return;
|
||||
}
|
||||
|
||||
EndPlacement();
|
||||
}
|
||||
|
||||
@@ -435,6 +461,43 @@ namespace Ashwild.Building
|
||||
/// idle. Safe to call when nothing is in flight.
|
||||
/// </summary>
|
||||
private void EndPlacement()
|
||||
{
|
||||
DespawnGhost();
|
||||
|
||||
if (active != null)
|
||||
{
|
||||
active = null;
|
||||
activeId = 0;
|
||||
PlayerEvents.RaiseBuildPlacingChanged(false);
|
||||
PlayerEvents.RaiseBuildCostChanged(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continuous build: after committing a piece, drops the spent ghost and asks the registry for a
|
||||
/// fresh one of the same structure so the player keeps placing without reopening the menu. Keeps
|
||||
/// <see cref="active"/>/<see cref="activeId"/> and the current yaw, and stays "placing" on the
|
||||
/// bus. The token bump makes any in-flight ghost from the old request reject itself in AttachGhost.
|
||||
/// </summary>
|
||||
private void RearmGhost()
|
||||
{
|
||||
DespawnGhost();
|
||||
|
||||
if (BuildRegistry.Instance == null)
|
||||
{
|
||||
EndPlacement();
|
||||
return;
|
||||
}
|
||||
|
||||
int token = ++spawnToken;
|
||||
BuildRegistry.Instance.RequestSpawnGhost(activeId, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Despawns the networked ghost we own and clears its cached components, without touching the
|
||||
/// active buildable or the bus. Safe to call when no ghost is live.
|
||||
/// </summary>
|
||||
private void DespawnGhost()
|
||||
{
|
||||
if (ghost != null)
|
||||
{
|
||||
@@ -444,13 +507,6 @@ namespace Ashwild.Building
|
||||
}
|
||||
ghostValidity = null;
|
||||
ghostSnaps = null;
|
||||
|
||||
if (active != null)
|
||||
{
|
||||
active = null;
|
||||
activeId = 0;
|
||||
PlayerEvents.RaiseBuildPlacingChanged(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -469,6 +525,86 @@ namespace Ashwild.Building
|
||||
|
||||
#endregion
|
||||
|
||||
#region Build Cost
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the crosshair cost display when the inventory changes mid-placement (a resource was
|
||||
/// spent or picked up), so its affordability colouring stays live. No-op while idle.
|
||||
/// </summary>
|
||||
private void HandleInventoryChanged(int slotIndex)
|
||||
{
|
||||
if (active == null) return;
|
||||
PublishCostView();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the active build's cost lines into the reused buffer — each with the local player's
|
||||
/// current affordability — and pushes them to the crosshair via the bus. Clears the display when
|
||||
/// idle or when the build is free (no cost lines).
|
||||
/// </summary>
|
||||
private void PublishCostView()
|
||||
{
|
||||
BuildCost[] cost = active != null ? active.Cost : null;
|
||||
if (cost == null || cost.Length == 0)
|
||||
{
|
||||
PlayerEvents.RaiseBuildCostChanged(null);
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
costView.Clear();
|
||||
foreach (BuildCost line in cost)
|
||||
{
|
||||
if (line.item == null) continue;
|
||||
int owned = inv != null ? inv.CountItem(line.item) : 0;
|
||||
bool affordable = owned >= line.quantity;
|
||||
costView.Add(new BuildCostView(line.item.Icon, owned, line.quantity, affordable));
|
||||
}
|
||||
PlayerEvents.RaiseBuildCostChanged(costView);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spends the active build's cost from the local inventory after a committed placement. Client-
|
||||
/// authoritative like crafting — the inventory change refreshes the cost view through
|
||||
/// <see cref="HandleInventoryChanged"/>. No-op for a free build or when offline.
|
||||
/// </summary>
|
||||
private void ConsumeCost()
|
||||
{
|
||||
BuildCost[] cost = active != null ? active.Cost : null;
|
||||
if (cost == null || cost.Length == 0) return;
|
||||
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
if (inv == null) return;
|
||||
|
||||
foreach (BuildCost line in cost)
|
||||
{
|
||||
if (line.item == null) continue;
|
||||
inv.RemoveItemByData(line.item, line.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Whether confirming a placement immediately re-arms another ghost of the same structure
|
||||
/// (continuous build) instead of returning to idle. Settable so a key or menu toggle can flip it.
|
||||
/// </summary>
|
||||
public bool ContinuousBuild
|
||||
{
|
||||
get => continuousBuild;
|
||||
set => continuousBuild = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flips continuous build on/off — wire this to a key or a menu toggle so the player can switch
|
||||
/// between chaining placements and placing one at a time.
|
||||
/// </summary>
|
||||
public void ToggleContinuousBuild() => continuousBuild = !continuousBuild;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Demolition
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user