(Feat) Add Build
This commit is contained in:
@@ -2,24 +2,37 @@ using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using Ashwild.Player;
|
||||
using Ashwild.Storage;
|
||||
using Ashwild.UI;
|
||||
|
||||
namespace Ashwild.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// The inventory window. It is a local UI panel (opening/closing is purely local — the item
|
||||
/// data itself lives in the networked <see cref="PlayerInventory"/>), so it is driven by the
|
||||
/// GameUIManager panel stack like any other panel. The controller object stays active so it can
|
||||
/// build its grid when the local player spawns; Show/Hide only toggle the visual window.
|
||||
/// The inventory window and the manager of its right-side modules. It is a local UI panel
|
||||
/// (opening/closing is purely local — the item data lives in the networked <see cref="PlayerInventory"/>),
|
||||
/// driven by the GameUIManager panel stack. The left grid + hotbar are always the player's inventory;
|
||||
/// the right area swaps between two modules: the hover description (normal browsing) and the chest
|
||||
/// module (<see cref="ChestPanelUI"/>) when a chest is opened. Opening a chest reuses this same window
|
||||
/// — the inventory and hotbar cells double as the deposit source, so nothing is rebuilt — and every
|
||||
/// drop is routed here into the right operation, chest transfers going through the chest's
|
||||
/// server-authoritative RPCs so two players sharing a chest stay in sync.
|
||||
/// </summary>
|
||||
public class InventoryUI : UIPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks this panel as the inventory: input is locked and the cursor shows, but the world
|
||||
/// keeps running (unlike the pause menu).
|
||||
/// keeps running (unlike the pause menu). A chest reuses this same window/kind.
|
||||
/// </summary>
|
||||
public override PanelKind Kind => PanelKind.Inventory;
|
||||
|
||||
/// <summary>
|
||||
/// The single inventory window in the scene, reached by a chest's Interact() and by slot cells
|
||||
/// routing their drops here.
|
||||
/// </summary>
|
||||
public static InventoryUI Instance { get; private set; }
|
||||
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private GameObject inventoryPanel;
|
||||
[SerializeField] private Transform slotContainer;
|
||||
@@ -33,8 +46,12 @@ namespace Ashwild.Inventory
|
||||
[Header("Context Menu")]
|
||||
[SerializeField] private SlotContextMenu contextMenu;
|
||||
|
||||
[Header("Hover Description")]
|
||||
[Header("Right Modules")]
|
||||
[Tooltip("Holder of the hover-description module — shown while browsing, hidden while a chest is open.")]
|
||||
[SerializeField] private GameObject descriptionModule;
|
||||
[SerializeField] private HoverDescriptionUI hoverDescription;
|
||||
[Tooltip("The chest module shown in place of the description while a chest is open.")]
|
||||
[SerializeField] private ChestPanelUI chestPanel;
|
||||
|
||||
[Header("Categories")]
|
||||
[SerializeField] private InventoryCategoryManager categoryManager;
|
||||
@@ -42,10 +59,32 @@ namespace Ashwild.Inventory
|
||||
[Header("Hotbar")]
|
||||
[SerializeField] private HotbarUI hotbarUI;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private SlotUI[] slotUIs;
|
||||
private PlayerInventory inventory;
|
||||
private bool bound;
|
||||
|
||||
/// <summary>
|
||||
/// The chest bound while the window is in chest mode (null during normal inventory browsing).
|
||||
/// </summary>
|
||||
private Chest boundChest;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Registers the singleton (in addition to the base panel setup).
|
||||
/// </summary>
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the networked local player to spawn before building the inventory grid.
|
||||
/// </summary>
|
||||
@@ -64,7 +103,7 @@ namespace Ashwild.Inventory
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Setup shared ghost for all SlotUIs (hotbar + inventory) — does not need the player.
|
||||
// Setup shared ghost for all SlotUIs (hotbar + inventory + chest) — does not need the player.
|
||||
SlotUI.SetupGhost(ghostObject, ghostIcon, ghostQuantityText);
|
||||
inventoryPanel.SetActive(false);
|
||||
|
||||
@@ -73,6 +112,20 @@ namespace Ashwild.Inventory
|
||||
BuildInventory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the listener and the singleton on teardown.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (inventory != null)
|
||||
inventory.onSlotChanged.RemoveListener(RefreshSlot);
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Build
|
||||
|
||||
/// <summary>
|
||||
/// Builds the grid as soon as the local player has spawned on the network.
|
||||
/// </summary>
|
||||
@@ -96,7 +149,8 @@ namespace Ashwild.Inventory
|
||||
int slotIndex = inventory.HotbarSize + i;
|
||||
GameObject slotGO = Instantiate(slotPrefab, slotContainer);
|
||||
SlotUI slotUI = slotGO.GetComponent<SlotUI>();
|
||||
slotUI.Initialize(slotIndex, OnSwapRequested, OnSlotClicked, OnSlotHoverEnter, OnSlotHoverExit);
|
||||
slotUI.Initialize(SlotContainer.Inventory, slotIndex,
|
||||
HandleSlotDrop, OnSlotClicked, OnSlotHoverEnter, OnSlotHoverExit);
|
||||
slotUIs[i] = slotUI;
|
||||
}
|
||||
|
||||
@@ -104,29 +158,59 @@ namespace Ashwild.Inventory
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
#endregion
|
||||
|
||||
#region Panel Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Binds a chest and opens this window in chest mode through the panel stack. Called from a
|
||||
/// chest's Interact(); the chest is picked up in Show().
|
||||
/// </summary>
|
||||
public void OpenChest(Chest chest)
|
||||
{
|
||||
if (inventory != null)
|
||||
inventory.onSlotChanged.RemoveListener(RefreshSlot);
|
||||
if (chest == null) return;
|
||||
boundChest = chest;
|
||||
|
||||
if (UIManager.Instance != null)
|
||||
UIManager.Instance.OpenPanel(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the inventory window (called by the GameUIManager panel stack).
|
||||
/// Opens the inventory window. Enters chest mode when a chest was bound (right side shows the
|
||||
/// chest module, category is forced to and locked on the inventory), otherwise normal browsing
|
||||
/// (right side shows the description). Always lands on the inventory category, never craft.
|
||||
/// </summary>
|
||||
public override void Show()
|
||||
{
|
||||
inventoryPanel.SetActive(true);
|
||||
RefreshAll();
|
||||
bool chestMode = boundChest != null;
|
||||
|
||||
if (categoryManager != null)
|
||||
{
|
||||
categoryManager.Open();
|
||||
categoryManager.SetLocked(chestMode);
|
||||
}
|
||||
|
||||
if (chestMode)
|
||||
{
|
||||
if (descriptionModule != null) descriptionModule.SetActive(false);
|
||||
if (chestPanel != null) chestPanel.Bind(boundChest);
|
||||
PlayerEvents.RaiseChestOpenChanged(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (chestPanel != null) chestPanel.Hide();
|
||||
if (descriptionModule != null) descriptionModule.SetActive(true);
|
||||
}
|
||||
|
||||
RefreshAll();
|
||||
|
||||
if (hotbarUI != null)
|
||||
hotbarUI.OnInventoryOpen();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the inventory window and tears down its transient UI (ghost, context menu).
|
||||
/// Closes the window and tears down its transient UI (ghost, context menu, chest binding).
|
||||
/// </summary>
|
||||
public override void Hide()
|
||||
{
|
||||
@@ -140,11 +224,16 @@ namespace Ashwild.Inventory
|
||||
hoverDescription.Hide();
|
||||
|
||||
if (categoryManager != null)
|
||||
{
|
||||
categoryManager.SetLocked(false);
|
||||
categoryManager.Close();
|
||||
}
|
||||
|
||||
if (hotbarUI != null)
|
||||
hotbarUI.OnInventoryClose();
|
||||
|
||||
CloseChestMode();
|
||||
|
||||
inventoryPanel.SetActive(false);
|
||||
}
|
||||
|
||||
@@ -156,54 +245,99 @@ namespace Ashwild.Inventory
|
||||
if (ghostObject != null)
|
||||
ghostObject.SetActive(false);
|
||||
|
||||
CloseChestMode();
|
||||
|
||||
inventoryPanel.SetActive(false);
|
||||
}
|
||||
|
||||
// Called by any SlotUI (inventory or hotbar) when a drag-drop completes
|
||||
public static void OnSwapRequested(int fromIndex, int toIndex)
|
||||
/// <summary>
|
||||
/// Leaves chest mode: hides the chest module, releases the chest and puts the description module
|
||||
/// back. Shared by both close paths so they can never drift apart — HideInstant used to forget
|
||||
/// the description and left the right side blank on the next open.
|
||||
/// </summary>
|
||||
private void CloseChestMode()
|
||||
{
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
InventorySlot fromSlot = inv.GetSlot(fromIndex);
|
||||
InventorySlot toSlot = inv.GetSlot(toIndex);
|
||||
if (boundChest == null) return;
|
||||
|
||||
// Same stackable item: merge
|
||||
if (!fromSlot.IsEmpty && !toSlot.IsEmpty
|
||||
&& fromSlot.ItemData == toSlot.ItemData
|
||||
&& toSlot.CanAccept(fromSlot.ItemData))
|
||||
{
|
||||
int leftover = toSlot.AddQuantity(fromSlot.Quantity);
|
||||
if (leftover <= 0)
|
||||
fromSlot.Clear();
|
||||
else
|
||||
fromSlot.Set(fromSlot.ItemData, leftover);
|
||||
|
||||
inv.onSlotChanged?.Invoke(fromIndex);
|
||||
inv.onSlotChanged?.Invoke(toIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Swap
|
||||
inv.SwapSlots(fromIndex, toIndex);
|
||||
}
|
||||
if (chestPanel != null) chestPanel.Hide();
|
||||
boundChest = null;
|
||||
if (descriptionModule != null) descriptionModule.SetActive(true);
|
||||
PlayerEvents.RaiseChestOpenChanged(false);
|
||||
}
|
||||
|
||||
private void OnSlotClicked(int index, bool rightClick)
|
||||
#endregion
|
||||
|
||||
#region Transfer Routing
|
||||
|
||||
/// <summary>
|
||||
/// Routes a slot drop to whoever owns the slots: the inventory itself when both ends are local,
|
||||
/// otherwise the chest (which reconciles it server-side). Both paths end up in the same
|
||||
/// <see cref="SlotTransfer.Move"/> rules, so a chest drag behaves exactly like an inventory drag.
|
||||
/// Static so every cell (inventory, hotbar, chest) reports here without a per-cell reference.
|
||||
/// </summary>
|
||||
public static void HandleSlotDrop(SlotContainer fromContainer, int fromIndex, SlotContainer toContainer, int toIndex)
|
||||
{
|
||||
if (rightClick)
|
||||
if (fromContainer == SlotContainer.Inventory && toContainer == SlotContainer.Inventory)
|
||||
{
|
||||
if (contextMenu != null)
|
||||
contextMenu.Show(index);
|
||||
else
|
||||
inventory.UseItem(index);
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
if (inv != null) inv.MoveSlot(fromIndex, toIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
Chest chest = Instance != null ? Instance.boundChest : null;
|
||||
if (chest == null) return;
|
||||
|
||||
// Paint the chest cell as emptied now: its clear replicates on the SyncList (end of tick)
|
||||
// while the grant is an immediate TargetRpc, so without this the stack visibly lands in the
|
||||
// inventory before it leaves the chest.
|
||||
if (fromContainer == SlotContainer.Chest && Instance.chestPanel != null)
|
||||
Instance.chestPanel.PredictEmptied(fromIndex);
|
||||
|
||||
chest.RequestMove(fromContainer, fromIndex, toContainer, toIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the description payload for the hovered slot and shows the side panel. Empty slots
|
||||
/// keep the panel hidden. The duration bar reflects the item's remaining uses/durability.
|
||||
/// Quick-transfers a stack to the other container (auto-placed), for right-clicks. Exposed so the
|
||||
/// hotbar — which cannot reach the private chest binding — can route its own cells here.
|
||||
/// </summary>
|
||||
private void OnSlotHoverEnter(int index)
|
||||
public void RequestQuickTransfer(SlotContainer container, int index)
|
||||
{
|
||||
if (boundChest != null)
|
||||
boundChest.RequestQuickTransfer(container, index);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Right-click on an inventory cell: a quick deposit while a chest is open, otherwise the slot
|
||||
/// context menu.
|
||||
/// </summary>
|
||||
private void OnSlotClicked(SlotContainer container, int index, bool rightClick)
|
||||
{
|
||||
if (!rightClick) return;
|
||||
|
||||
if (boundChest != null)
|
||||
{
|
||||
boundChest.RequestQuickTransfer(SlotContainer.Inventory, index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (contextMenu != null)
|
||||
contextMenu.Show(index);
|
||||
else if (inventory != null)
|
||||
inventory.UseItem(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the description payload for the hovered slot and shows the side panel. Suppressed in
|
||||
/// chest mode (the description module is hidden) and for empty slots. The duration bar reflects
|
||||
/// the item's remaining uses/durability.
|
||||
/// </summary>
|
||||
private void OnSlotHoverEnter(SlotContainer container, int index)
|
||||
{
|
||||
if (boundChest != null) return;
|
||||
if (hoverDescription == null || inventory == null) return;
|
||||
|
||||
InventorySlot slot = inventory.GetSlot(index);
|
||||
@@ -213,13 +347,7 @@ namespace Ashwild.Inventory
|
||||
return;
|
||||
}
|
||||
|
||||
ItemData item = slot.ItemData;
|
||||
bool hasDuration = item.HasUses;
|
||||
float fill = hasDuration ? (float)slot.CurrentUses / item.MaxUses : 0f;
|
||||
string durationText = hasDuration ? $"{slot.CurrentUses} / {item.MaxUses}" : string.Empty;
|
||||
|
||||
hoverDescription.Show(new ItemDescriptionView(
|
||||
item.Icon, item.ItemName, item.Description, hasDuration, fill, durationText));
|
||||
hoverDescription.Show(ItemDescriptionView.From(slot));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -231,6 +359,10 @@ namespace Ashwild.Inventory
|
||||
hoverDescription.Hide();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Refresh
|
||||
|
||||
private void RefreshSlot(int index)
|
||||
{
|
||||
if (slotUIs == null) return;
|
||||
@@ -250,5 +382,7 @@ namespace Ashwild.Inventory
|
||||
slotUIs[i].UpdateVisual(inventory.GetSlot(slotIndex));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user