(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
+188 -54
View File
@@ -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
}
}