(Feat) Add Build
This commit is contained in:
+182
-212
@@ -10,8 +10,8 @@ namespace Ashwild.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// A networked storage chest and the single IInteractable that opens it. Interacting opens the
|
||||
/// shared <see cref="ChestUI"/> (the player's inventory on the left, this chest on the right); all
|
||||
/// item moves are requested through server RPCs.
|
||||
/// inventory window in chest mode (the player's inventory on the left, this chest's module on the
|
||||
/// right); all item moves are requested through server RPCs.
|
||||
///
|
||||
/// Multiplayer: the chest is **server-authoritative** so two players browsing the same chest can
|
||||
/// never clobber or duplicate items. The server owns the rich model (<see cref="InventorySlot"/>[])
|
||||
@@ -72,7 +72,7 @@ namespace Ashwild.Storage
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a slot's replicated view changes so the open <see cref="ChestUI"/> can refresh
|
||||
/// Fired when a slot's replicated view changes so the open <see cref="ChestPanelUI"/> can refresh
|
||||
/// just that cell. An index of -1 means the whole collection changed (a full refresh).
|
||||
/// </summary>
|
||||
public event Action<int> SlotViewChanged;
|
||||
@@ -154,16 +154,17 @@ namespace Ashwild.Storage
|
||||
public string InteractionPrompt => $"Ouvrir {DisplayName}";
|
||||
|
||||
/// <summary>
|
||||
/// Opens the shared chest window bound to this chest. Runs on the interacting client only.
|
||||
/// Opens the inventory window in chest mode, bound to this chest. Runs on the interacting client
|
||||
/// only.
|
||||
/// </summary>
|
||||
public void Interact()
|
||||
{
|
||||
if (ChestUI.Instance == null)
|
||||
if (InventoryUI.Instance == null)
|
||||
{
|
||||
Debug.LogError("[Chest] No ChestUI found in the scene — cannot open the chest.", this);
|
||||
Debug.LogError("[Chest] No InventoryUI found in the scene — cannot open the chest.", this);
|
||||
return;
|
||||
}
|
||||
ChestUI.Instance.Open(this);
|
||||
InventoryUI.Instance.OpenChest(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -198,52 +199,94 @@ namespace Ashwild.Storage
|
||||
#region Client Requests
|
||||
|
||||
/// <summary>
|
||||
/// Deposits the whole stack in the given inventory slot into a specific chest slot. When the
|
||||
/// target chest slot holds a different item this becomes a swap, so it first checks the
|
||||
/// displaced stack still fits in the inventory (server refunds anything the target can't hold).
|
||||
/// Pass a negative chest slot to let the server auto-place into the first fitting slot.
|
||||
/// The one drag-and-drop operation for anything involving this chest: chest→inventory,
|
||||
/// inventory→chest and chest→chest, in either direction. It never decides *what* happens — the
|
||||
/// shared <see cref="SlotTransfer.Move"/> rules do — so dropping a stack on a chest cell behaves
|
||||
/// exactly like dropping it on an inventory cell (move, merge or swap).
|
||||
///
|
||||
/// For a cross-container move the player's slot is read and cleared locally (the inventory is
|
||||
/// client-authoritative) and sent along as a payload; the server reconciles it against the chest
|
||||
/// slot and grants whatever comes back into that same slot, which is why an item never lands
|
||||
/// somewhere unexpected like the first free hotbar cell.
|
||||
/// </summary>
|
||||
public void RequestDepositToSlot(int inventoryIndex, int chestSlot)
|
||||
public void RequestMove(SlotContainer fromContainer, int fromIndex, SlotContainer toContainer, int toIndex)
|
||||
{
|
||||
if (fromContainer == SlotContainer.Chest && toContainer == SlotContainer.Chest)
|
||||
{
|
||||
if (fromIndex != toIndex) MoveWithinServerRpc(fromIndex, toIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
if (inv == null) return;
|
||||
|
||||
bool chestIsSource = fromContainer == SlotContainer.Chest;
|
||||
int chestIndex = chestIsSource ? fromIndex : toIndex;
|
||||
int inventoryIndex = chestIsSource ? toIndex : fromIndex;
|
||||
|
||||
if (chestIndex < 0 || chestIndex >= slotViews.Count) return;
|
||||
if (inv.GetSlot(inventoryIndex) == null) return;
|
||||
|
||||
SlotContent payload = inv.ReadSlot(inventoryIndex);
|
||||
bool chestSlotFilled = TryGetSlot(chestIndex, out _, out _, out _);
|
||||
|
||||
// Nothing to move: the side the player dragged from is empty.
|
||||
if (chestIsSource ? !chestSlotFilled : payload.IsEmpty) return;
|
||||
|
||||
ushort id = 0;
|
||||
if (!payload.IsEmpty)
|
||||
{
|
||||
id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(payload.Item) : (ushort)0;
|
||||
if (id == 0)
|
||||
{
|
||||
Debug.LogError($"[Chest] '{payload.Item.ItemName}' is not in the ItemDatabase — cannot store. Run Rebuild Item Database.", this);
|
||||
return;
|
||||
}
|
||||
inv.WriteSlot(inventoryIndex, SlotContent.Empty);
|
||||
}
|
||||
|
||||
MoveCrossServerRpc(chestIndex, inventoryIndex, chestIsSource, id, payload.Quantity, payload.Uses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quick transfer (right-click): sends a stack to the other container without the player picking a
|
||||
/// destination, so it is auto-placed into the first slot that accepts it. Deliberately a different
|
||||
/// operation from <see cref="RequestMove"/> — here the player expressed no target.
|
||||
/// </summary>
|
||||
public void RequestQuickTransfer(SlotContainer fromContainer, int index)
|
||||
{
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
if (inv == null) return;
|
||||
|
||||
InventorySlot slot = inv.GetSlot(inventoryIndex);
|
||||
if (slot == null || slot.IsEmpty) return;
|
||||
|
||||
ItemData item = slot.ItemData;
|
||||
int quantity = slot.Quantity;
|
||||
int uses = item.HasUses ? slot.CurrentUses : -1;
|
||||
|
||||
ushort id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(item) : (ushort)0;
|
||||
if (id == 0)
|
||||
if (fromContainer == SlotContainer.Chest)
|
||||
{
|
||||
Debug.LogError($"[Chest] '{item.ItemName}' is not in the ItemDatabase — cannot store. Run Rebuild Item Database.", this);
|
||||
if (!TryGetSlot(index, out ItemData item, out int quantity, out _)) return;
|
||||
if (!inv.CanFit(item, quantity))
|
||||
{
|
||||
Debug.LogWarning($"[Chest] Inventaire plein — impossible de retirer '{item.ItemName}'.", this);
|
||||
return;
|
||||
}
|
||||
QuickWithdrawServerRpc(index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (chestSlot >= 0 && chestSlot < slotViews.Count)
|
||||
SlotContent content = inv.ReadSlot(index);
|
||||
if (content.IsEmpty) return;
|
||||
|
||||
ushort id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(content.Item) : (ushort)0;
|
||||
if (id == 0)
|
||||
{
|
||||
ChestSlotView target = slotViews[chestSlot];
|
||||
if (target.rawId != 0)
|
||||
{
|
||||
ItemData targetItem = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(target.rawId) : null;
|
||||
bool sameStackable = targetItem == item && item.IsStackable && !item.HasUses;
|
||||
if (!sameStackable && targetItem != null && !inv.CanFit(targetItem, target.quantity))
|
||||
{
|
||||
Debug.LogWarning($"[Chest] Inventaire plein — impossible d'échanger avec '{targetItem.ItemName}'.", this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Debug.LogError($"[Chest] '{content.Item.ItemName}' is not in the ItemDatabase — cannot store. Run Rebuild Item Database.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
inv.RemoveItem(inventoryIndex, quantity);
|
||||
RequestDepositServerRpc(chestSlot, id, quantity, uses);
|
||||
inv.WriteSlot(index, SlotContent.Empty);
|
||||
QuickDepositServerRpc(id, content.Quantity, content.Uses, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deposits every non-empty inventory slot into the chest, auto-placing each. Safe: the server
|
||||
/// refunds anything that does not fit back to the same player.
|
||||
/// Stores every inventory stack in the chest, auto-placing each. Safe: the server refunds whatever
|
||||
/// does not fit back to the slot it came from.
|
||||
/// </summary>
|
||||
public void RequestDepositAll()
|
||||
{
|
||||
@@ -251,50 +294,18 @@ namespace Ashwild.Storage
|
||||
if (inv == null) return;
|
||||
|
||||
for (int i = 0; i < inv.InventorySize; i++)
|
||||
{
|
||||
InventorySlot slot = inv.GetSlot(i);
|
||||
if (slot == null || slot.IsEmpty) continue;
|
||||
RequestDepositToSlot(i, -1);
|
||||
}
|
||||
RequestQuickTransfer(SlotContainer.Inventory, i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Withdraws a chest slot's whole stack into the inventory, after a local CanFit pre-check so a
|
||||
/// full inventory leaves the items in the chest instead of losing them.
|
||||
/// </summary>
|
||||
public void RequestWithdraw(int chestSlot)
|
||||
{
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
if (inv == null) return;
|
||||
|
||||
if (!TryGetSlot(chestSlot, out ItemData item, out int quantity, out _)) return;
|
||||
if (!inv.CanFit(item, quantity))
|
||||
{
|
||||
Debug.LogWarning($"[Chest] Inventaire plein — impossible de retirer '{item.ItemName}'.", this);
|
||||
return;
|
||||
}
|
||||
|
||||
RequestWithdrawServerRpc(chestSlot, quantity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Withdraws every non-empty chest slot into the inventory. Each slot is CanFit-checked as it
|
||||
/// goes. Because the grants come back asynchronously, the checks do not yet account for items
|
||||
/// still in flight, so a chest larger than the free inventory space may leave some items behind.
|
||||
/// Takes every chest stack into the inventory. Each slot is CanFit-checked as it goes; because the
|
||||
/// grants come back asynchronously the checks do not account for items still in flight, so a chest
|
||||
/// larger than the free inventory space may leave some items behind.
|
||||
/// </summary>
|
||||
public void RequestWithdrawAll()
|
||||
{
|
||||
for (int i = 0; i < slotViews.Count; i++)
|
||||
if (slotViews[i].rawId != 0)
|
||||
RequestWithdraw(i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves/merges one chest slot into another (a drag within the chest grid).
|
||||
/// </summary>
|
||||
public void RequestMoveWithin(int fromSlot, int toSlot)
|
||||
{
|
||||
RequestMoveWithinServerRpc(fromSlot, toSlot);
|
||||
RequestQuickTransfer(SlotContainer.Chest, i);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -302,110 +313,80 @@ namespace Ashwild.Storage
|
||||
#region Server RPCs
|
||||
|
||||
/// <summary>
|
||||
/// Server-side deposit. Places the incoming stack in the requested slot — filling an empty
|
||||
/// slot, merging into a matching stack, or swapping with a different item (the displaced stack
|
||||
/// is granted back to the player) — and refunds any leftover. A negative slot auto-places.
|
||||
/// Server-side chest-to-chest move: both slots are ours, so the shared rules run straight on the
|
||||
/// authoritative model.
|
||||
/// </summary>
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
private void RequestDepositServerRpc(int chestSlot, ushort id, int quantity, int uses, NetworkConnection conn = null)
|
||||
private void MoveWithinServerRpc(int fromIndex, int toIndex)
|
||||
{
|
||||
ItemData item = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(id) : null;
|
||||
if (item == null || quantity <= 0) return;
|
||||
if (fromIndex < 0 || fromIndex >= slots.Length) return;
|
||||
if (toIndex < 0 || toIndex >= slots.Length) return;
|
||||
|
||||
if (chestSlot < 0 || chestSlot >= slots.Length)
|
||||
SlotContent from = ReadServerSlot(fromIndex);
|
||||
SlotContent to = ReadServerSlot(toIndex);
|
||||
|
||||
if (!SlotTransfer.Move(ref from, ref to)) return;
|
||||
|
||||
WriteServerSlot(fromIndex, from);
|
||||
WriteServerSlot(toIndex, to);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side inventory↔chest move, both directions. The player's slot arrives as a payload (empty
|
||||
/// when they dragged onto an empty cell); the same rules that drive an inventory-to-inventory drag
|
||||
/// decide between move, merge and swap, then whatever ends up on the player's side is granted back
|
||||
/// into the exact slot they used. An out-of-range chest index refunds the payload rather than
|
||||
/// swallowing it.
|
||||
/// </summary>
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
private void MoveCrossServerRpc(int chestIndex, int inventoryIndex, bool chestIsSource, ushort id, int quantity, int uses, NetworkConnection conn = null)
|
||||
{
|
||||
ItemData payloadItem = id != 0 && ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(id) : null;
|
||||
SlotContent playerContent = SlotContent.Of(payloadItem, quantity, uses);
|
||||
|
||||
if (chestIndex < 0 || chestIndex >= slots.Length)
|
||||
{
|
||||
ServerDepositAuto(item, quantity, uses, conn);
|
||||
GrantBack(conn, playerContent, inventoryIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
InventorySlot target = slots[chestSlot];
|
||||
SlotContent chestContent = ReadServerSlot(chestIndex);
|
||||
|
||||
if (target.IsEmpty)
|
||||
{
|
||||
int place = PlaceableCount(item, quantity);
|
||||
target.Set(item, place, uses);
|
||||
WriteView(chestSlot);
|
||||
GrantBack(conn, item, quantity - place, uses);
|
||||
}
|
||||
else if (!item.HasUses && target.ItemData == item && target.CanAccept(item))
|
||||
{
|
||||
int leftover = target.AddQuantity(quantity);
|
||||
WriteView(chestSlot);
|
||||
GrantBack(conn, item, leftover, uses);
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemData displaced = target.ItemData;
|
||||
int displacedQty = target.Quantity;
|
||||
int displacedUses = displaced != null && displaced.HasUses ? target.CurrentUses : -1;
|
||||
if (chestIsSource) SlotTransfer.Move(ref chestContent, ref playerContent);
|
||||
else SlotTransfer.Move(ref playerContent, ref chestContent);
|
||||
|
||||
int place = PlaceableCount(item, quantity);
|
||||
target.Set(item, place, uses);
|
||||
WriteView(chestSlot);
|
||||
|
||||
GrantBack(conn, item, quantity - place, uses);
|
||||
GrantBack(conn, displaced, displacedQty, displacedUses);
|
||||
}
|
||||
WriteServerSlot(chestIndex, chestContent);
|
||||
GrantBack(conn, playerContent, inventoryIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side withdraw. Removes the whole stack from the chest slot and grants it to the
|
||||
/// requesting player.
|
||||
/// Server-side quick deposit: auto-places the incoming stack and refunds any leftover to the slot
|
||||
/// it was taken from.
|
||||
/// </summary>
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
private void RequestWithdrawServerRpc(int chestSlot, int quantity, NetworkConnection conn = null)
|
||||
private void QuickDepositServerRpc(ushort id, int quantity, int uses, int originIndex, NetworkConnection conn = null)
|
||||
{
|
||||
if (chestSlot < 0 || chestSlot >= slots.Length) return;
|
||||
ItemData item = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(id) : null;
|
||||
SlotContent incoming = SlotContent.Of(item, quantity, uses);
|
||||
if (incoming.IsEmpty) return;
|
||||
|
||||
InventorySlot s = slots[chestSlot];
|
||||
if (s.IsEmpty || quantity <= 0) return;
|
||||
|
||||
ItemData item = s.ItemData;
|
||||
int take = Mathf.Min(quantity, s.Quantity);
|
||||
int uses = item.HasUses ? s.CurrentUses : -1;
|
||||
|
||||
s.RemoveQuantity(take);
|
||||
WriteView(chestSlot);
|
||||
|
||||
GrantBack(conn, item, take, uses);
|
||||
AutoPlace(ref incoming);
|
||||
GrantBack(conn, incoming, originIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side move within the chest: merges into a matching stack, otherwise swaps the two
|
||||
/// slots (uses preserved). Never touches any inventory.
|
||||
/// Server-side quick withdraw: empties the chest slot and lets the player's inventory auto-place it.
|
||||
/// </summary>
|
||||
[ServerRpc(RequireOwnership = false)]
|
||||
private void RequestMoveWithinServerRpc(int fromSlot, int toSlot, NetworkConnection conn = null)
|
||||
private void QuickWithdrawServerRpc(int chestIndex, NetworkConnection conn = null)
|
||||
{
|
||||
if (fromSlot < 0 || fromSlot >= slots.Length) return;
|
||||
if (toSlot < 0 || toSlot >= slots.Length) return;
|
||||
if (fromSlot == toSlot) return;
|
||||
if (chestIndex < 0 || chestIndex >= slots.Length) return;
|
||||
|
||||
InventorySlot a = slots[fromSlot];
|
||||
if (a.IsEmpty) return;
|
||||
InventorySlot b = slots[toSlot];
|
||||
SlotContent content = ReadServerSlot(chestIndex);
|
||||
if (content.IsEmpty) return;
|
||||
|
||||
if (!b.IsEmpty && !a.ItemData.HasUses && b.ItemData == a.ItemData && b.CanAccept(a.ItemData))
|
||||
{
|
||||
int leftover = b.AddQuantity(a.Quantity);
|
||||
if (leftover <= 0) a.Clear();
|
||||
else a.Set(a.ItemData, leftover);
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemData ai = a.ItemData;
|
||||
int aq = a.Quantity;
|
||||
int au = ai != null && ai.HasUses ? a.CurrentUses : -1;
|
||||
ItemData bi = b.ItemData;
|
||||
int bq = b.Quantity;
|
||||
int bu = bi != null && bi.HasUses ? b.CurrentUses : -1;
|
||||
|
||||
if (bi == null) a.Clear(); else a.Set(bi, bq, bu);
|
||||
if (ai == null) b.Clear(); else b.Set(ai, aq, au);
|
||||
}
|
||||
|
||||
WriteView(fromSlot);
|
||||
WriteView(toSlot);
|
||||
WriteServerSlot(chestIndex, SlotContent.Empty);
|
||||
GrantBack(conn, content, -1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -413,84 +394,73 @@ namespace Ashwild.Storage
|
||||
#region Server Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Auto-places an incoming stack: merges into matching non-full slots first, then fills empty
|
||||
/// slots (one instance per slot for uses-tracked items), and refunds any leftover.
|
||||
/// Auto-places a stack into the chest through the shared rule: merge into matching stacks first,
|
||||
/// then fill empty slots. Never swaps — the player picked no destination. Whatever does not fit is
|
||||
/// left in <paramref name="incoming"/> for the caller to refund.
|
||||
/// </summary>
|
||||
private void ServerDepositAuto(ItemData item, int quantity, int uses, NetworkConnection conn)
|
||||
private void AutoPlace(ref SlotContent incoming)
|
||||
{
|
||||
int remaining = quantity;
|
||||
for (int i = 0; i < slots.Length && !incoming.IsEmpty; i++)
|
||||
if (!slots[i].IsEmpty) StackIntoSlot(i, ref incoming);
|
||||
|
||||
if (!item.HasUses)
|
||||
{
|
||||
for (int i = 0; i < slots.Length && remaining > 0; i++)
|
||||
{
|
||||
if (!slots[i].IsEmpty && slots[i].ItemData == item && slots[i].CanAccept(item))
|
||||
{
|
||||
remaining = slots[i].AddQuantity(remaining);
|
||||
WriteView(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < slots.Length && remaining > 0; i++)
|
||||
{
|
||||
if (!slots[i].IsEmpty) continue;
|
||||
|
||||
if (item.HasUses)
|
||||
{
|
||||
slots[i].Set(item, 1, uses);
|
||||
remaining -= 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
int place = item.IsStackable ? Mathf.Min(remaining, item.MaxStackSize) : 1;
|
||||
slots[i].Set(item, place);
|
||||
remaining -= place;
|
||||
}
|
||||
WriteView(i);
|
||||
}
|
||||
|
||||
GrantBack(conn, item, remaining, uses);
|
||||
for (int i = 0; i < slots.Length && !incoming.IsEmpty; i++)
|
||||
if (slots[i].IsEmpty) StackIntoSlot(i, ref incoming);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How many units of an item can sit in a single fresh slot: one for uses-tracked or
|
||||
/// non-stackable items, otherwise up to the max stack size.
|
||||
/// Pushes as much of the incoming stack as one chest slot accepts, writing it back when it changed.
|
||||
/// </summary>
|
||||
private int PlaceableCount(ItemData item, int quantity)
|
||||
private void StackIntoSlot(int index, ref SlotContent incoming)
|
||||
{
|
||||
int cap = item.HasUses ? 1 : (item.IsStackable ? item.MaxStackSize : 1);
|
||||
return Mathf.Min(quantity, cap);
|
||||
SlotContent target = ReadServerSlot(index);
|
||||
if (!SlotTransfer.TryStack(ref incoming, ref target)) return;
|
||||
WriteServerSlot(index, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the server model of a slot into its replicated view.
|
||||
/// Reads a chest slot from the authoritative model as a container-agnostic snapshot.
|
||||
/// </summary>
|
||||
private void WriteView(int index)
|
||||
private SlotContent ReadServerSlot(int index)
|
||||
{
|
||||
InventorySlot s = slots[index];
|
||||
ChestSlotView v;
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
v = new ChestSlotView { rawId = 0, quantity = 0, uses = -1 };
|
||||
}
|
||||
else
|
||||
{
|
||||
ushort id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(s.ItemData) : (ushort)0;
|
||||
v = new ChestSlotView { rawId = id, quantity = s.Quantity, uses = s.ItemData.HasUses ? s.CurrentUses : -1 };
|
||||
}
|
||||
slotViews[index] = v;
|
||||
if (s.IsEmpty) return SlotContent.Empty;
|
||||
return SlotContent.Of(s.ItemData, s.Quantity, s.CurrentUses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grants an item back to the requesting player's (client-authoritative) inventory. No-op for
|
||||
/// a null item or a non-positive quantity.
|
||||
/// Writes a snapshot into the authoritative model and pushes it into the replicated view in one
|
||||
/// step, so the two can never drift apart. Carries the remaining uses, so a worn tool stored in a
|
||||
/// chest comes back out just as worn.
|
||||
/// </summary>
|
||||
private void GrantBack(NetworkConnection conn, ItemData item, int quantity, int uses)
|
||||
private void WriteServerSlot(int index, SlotContent content)
|
||||
{
|
||||
if (item == null || quantity <= 0) return;
|
||||
InventorySlot s = slots[index];
|
||||
|
||||
if (content.IsEmpty)
|
||||
{
|
||||
s.Clear();
|
||||
slotViews[index] = new ChestSlotView { rawId = 0, quantity = 0, uses = -1 };
|
||||
return;
|
||||
}
|
||||
|
||||
s.Set(content.Item, content.Quantity, content.Uses);
|
||||
|
||||
ushort id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(content.Item) : (ushort)0;
|
||||
slotViews[index] = new ChestSlotView { rawId = id, quantity = content.Quantity, uses = content.Uses };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grants a stack to the requesting player's (client-authoritative) inventory. No-op when empty.
|
||||
/// <paramref name="preferredIndex"/> is the slot the player used, so a refund or a swapped-out
|
||||
/// stack returns exactly there instead of auto-filling the first free slot (the hotbar). Negative
|
||||
/// means auto-place. Always flagged as a transfer: taking a stack out of a chest is shuffling
|
||||
/// items between the player's own containers, not a gain, so the HUD must not announce it.
|
||||
/// </summary>
|
||||
private void GrantBack(NetworkConnection conn, SlotContent content, int preferredIndex)
|
||||
{
|
||||
if (content.IsEmpty) return;
|
||||
PlayerInventory inv = ResolveInventory(conn);
|
||||
if (inv != null) inv.GrantItemFromServer(item, quantity, uses);
|
||||
if (inv != null) inv.GrantItemFromServer(content.Item, content.Quantity, content.Uses, preferredIndex, isTransfer: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using Ashwild.Inventory;
|
||||
|
||||
namespace Ashwild.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// The chest module of the inventory window: the right-side grid that shows the opened chest's
|
||||
/// contents, mirror of the hover-description module it replaces while a chest is open. It is a pure
|
||||
/// view driven by <see cref="InventoryUI"/> — it builds its grid of <see cref="SlotUI"/> cells from
|
||||
/// the bound chest's replicated view and refreshes them live, but it never routes transfers itself:
|
||||
/// every cell reports its drop to <see cref="InventoryUI.HandleSlotDrop"/>, which turns it into the
|
||||
/// matching server-authoritative deposit/withdraw/move on the <see cref="Chest"/>.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class ChestPanelUI : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Window")]
|
||||
[Tooltip("Root holder toggled on/off as the module is shown or hidden.")]
|
||||
[SerializeField] private GameObject root;
|
||||
[SerializeField] private TextMeshProUGUI titleText;
|
||||
|
||||
[Header("Grid")]
|
||||
[Tooltip("Parent the chest cells are laid out under.")]
|
||||
[SerializeField] private Transform slotContainer;
|
||||
[Tooltip("Prefab carrying a SlotUI — the same cell used by the inventory grid and the hotbar.")]
|
||||
[SerializeField] private GameObject slotPrefab;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private SlotUI[] slots;
|
||||
private Chest boundChest;
|
||||
|
||||
/// <summary>
|
||||
/// The chest currently shown (null while the module is hidden). Read by InventoryUI to route
|
||||
/// transfers.
|
||||
/// </summary>
|
||||
public Chest Chest => boundChest;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Parks the module hidden so it only appears once a chest is opened.
|
||||
/// </summary>
|
||||
private void Awake()
|
||||
{
|
||||
if (root != null) root.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unbinds the chest event if the module is torn down while open.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Show / Hide
|
||||
|
||||
/// <summary>
|
||||
/// Binds a chest and shows the module: (re)builds the grid to the chest's slot count, subscribes
|
||||
/// to its replicated view so cells refresh live, sets the title and draws every cell.
|
||||
/// </summary>
|
||||
public void Bind(Chest chest)
|
||||
{
|
||||
if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged;
|
||||
|
||||
boundChest = chest;
|
||||
if (root != null) root.SetActive(true);
|
||||
|
||||
BuildGrid();
|
||||
|
||||
if (boundChest != null)
|
||||
{
|
||||
boundChest.SlotViewChanged += HandleChestSlotChanged;
|
||||
if (titleText != null) titleText.text = boundChest.DisplayName;
|
||||
}
|
||||
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the module, unbinds the chest and releases it.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged;
|
||||
boundChest = null;
|
||||
if (root != null) root.SetActive(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Grid Building
|
||||
|
||||
/// <summary>
|
||||
/// Builds (or rebuilds when the size differs) the grid to match the bound chest's slot count, so
|
||||
/// opening a small chest then a large one lays out the right number of cells.
|
||||
/// </summary>
|
||||
private void BuildGrid()
|
||||
{
|
||||
int count = boundChest != null ? boundChest.SlotCount : 0;
|
||||
if (slots != null && slots.Length == count) return;
|
||||
|
||||
if (slots != null)
|
||||
foreach (SlotUI s in slots)
|
||||
if (s != null) Destroy(s.gameObject);
|
||||
|
||||
slots = new SlotUI[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
SlotUI slot = Instantiate(slotPrefab, slotContainer).GetComponent<SlotUI>();
|
||||
slot.Initialize(SlotContainer.Chest, i, InventoryUI.HandleSlotDrop, HandleChestSlotClicked);
|
||||
slots[i] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interaction
|
||||
|
||||
/// <summary>
|
||||
/// Right-click on a chest cell quick-transfers its whole stack into the inventory (auto-placed).
|
||||
/// </summary>
|
||||
private void HandleChestSlotClicked(SlotContainer container, int index, bool rightClick)
|
||||
{
|
||||
if (!rightClick || boundChest == null) return;
|
||||
|
||||
PredictEmptied(index);
|
||||
boundChest.RequestQuickTransfer(SlotContainer.Chest, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws a cell as emptied straight away, while its move is still in flight to the server.
|
||||
///
|
||||
/// Emptying the chest travels on the SyncList (flushed at end of tick) but the matching grant is
|
||||
/// a TargetRpc (sent immediately), so the item would otherwise appear in the inventory a moment
|
||||
/// *before* leaving the chest — a visible double. This only repaints the cell: the authoritative
|
||||
/// model is untouched, so if the server disagrees (a co-op partner grabbed the stack first) the
|
||||
/// next replicated update simply paints the item back.
|
||||
/// </summary>
|
||||
public void PredictEmptied(int index)
|
||||
{
|
||||
if (slots == null || index < 0 || index >= slots.Length) return;
|
||||
slots[index].UpdateVisual((ItemData)null, 0, -1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Refresh
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes a single cell, or the whole grid when the collection was (re)seeded (index -1).
|
||||
/// </summary>
|
||||
private void HandleChestSlotChanged(int index)
|
||||
{
|
||||
if (slots == null) return;
|
||||
if (index < 0) { RefreshAll(); return; }
|
||||
if (index < slots.Length) RefreshSlot(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redraws every chest cell.
|
||||
/// </summary>
|
||||
private void RefreshAll()
|
||||
{
|
||||
if (slots == null) return;
|
||||
for (int i = 0; i < slots.Length; i++) RefreshSlot(i);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws one cell from the chest's replicated view, or empty when the slot holds nothing.
|
||||
/// </summary>
|
||||
private void RefreshSlot(int index)
|
||||
{
|
||||
if (boundChest != null && boundChest.TryGetSlot(index, out ItemData item, out int qty, out int uses))
|
||||
slots[index].UpdateVisual(item, qty, uses);
|
||||
else
|
||||
slots[index].UpdateVisual((ItemData)null, 0, -1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b3438aa0c3c5264cbd59c336e56d645
|
||||
@@ -1,245 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using TMPro;
|
||||
using Ashwild.Inventory;
|
||||
|
||||
namespace Ashwild.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// Which container a chest-window cell belongs to, so a drop can be routed to the right transfer.
|
||||
/// </summary>
|
||||
public enum ChestSlotContainer
|
||||
{
|
||||
Inventory,
|
||||
Chest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One draggable cell in the chest window, used for both sides — the player's inventory (left) and
|
||||
/// the chest (right). Unlike the inventory's SlotUI it carries which container and index it maps to,
|
||||
/// so a drop hands the pair to the owning <see cref="ChestUI"/>, which turns it into a deposit,
|
||||
/// withdraw, move-within or inventory swap. It only renders and reports drags; every real mutation
|
||||
/// goes through the chest's server-authoritative RPCs.
|
||||
/// </summary>
|
||||
public class ChestSlotUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IPointerClickHandler
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("References")]
|
||||
[SerializeField] private Image iconImage;
|
||||
[SerializeField] private TextMeshProUGUI quantityText;
|
||||
|
||||
[Header("Uses Bar")]
|
||||
[Tooltip("Root of the uses/durability bar — shown only for items that track uses.")]
|
||||
[SerializeField] private GameObject usageBarRoot;
|
||||
[Tooltip("Filled image whose fillAmount maps to the remaining uses (Image Type = Filled).")]
|
||||
[SerializeField] private Image usageBarFill;
|
||||
[SerializeField] private Color normalTint = Color.white;
|
||||
[SerializeField] private Color depletedTint = Color.red;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private ChestUI owner;
|
||||
private ChestSlotContainer container;
|
||||
private int index;
|
||||
|
||||
public ChestSlotContainer Container => container;
|
||||
public int Index => index;
|
||||
|
||||
// Static drag state shared across every chest-window cell (both grids).
|
||||
private static ChestSlotUI draggedSlot;
|
||||
private static GameObject ghostObject;
|
||||
private static Image ghostIcon;
|
||||
private static TextMeshProUGUI ghostQuantity;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setup
|
||||
|
||||
/// <summary>
|
||||
/// Wires the single shared drag ghost used by every cell in the window.
|
||||
/// </summary>
|
||||
public static void SetupGhost(GameObject ghost, Image icon, TextMeshProUGUI qty)
|
||||
{
|
||||
ghostObject = ghost;
|
||||
ghostIcon = icon;
|
||||
ghostQuantity = qty;
|
||||
if (ghostObject != null) ghostObject.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds this cell to a container and index within the owning window.
|
||||
/// </summary>
|
||||
public void Initialize(ChestUI ownerUI, ChestSlotContainer slotContainer, int slotIndex)
|
||||
{
|
||||
owner = ownerUI;
|
||||
container = slotContainer;
|
||||
index = slotIndex;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rendering
|
||||
|
||||
/// <summary>
|
||||
/// Redraws the cell from whichever container it belongs to (the local inventory or the chest's
|
||||
/// replicated view).
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
if (container == ChestSlotContainer.Inventory)
|
||||
{
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
InventorySlot slot = inv != null ? inv.GetSlot(index) : null;
|
||||
if (slot == null || slot.IsEmpty)
|
||||
{
|
||||
RenderEmpty();
|
||||
return;
|
||||
}
|
||||
Render(slot.ItemData, slot.Quantity, slot.ItemData.HasUses ? slot.CurrentUses : -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (owner != null && owner.Chest != null
|
||||
&& owner.Chest.TryGetSlot(index, out ItemData item, out int qty, out int uses))
|
||||
Render(item, qty, uses);
|
||||
else
|
||||
RenderEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cell to its empty look.
|
||||
/// </summary>
|
||||
private void RenderEmpty()
|
||||
{
|
||||
iconImage.gameObject.SetActive(false);
|
||||
quantityText.gameObject.SetActive(false);
|
||||
if (usageBarRoot != null) usageBarRoot.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws an item's icon, quantity and uses bar. Keeps the mid-drag fade on the cell currently
|
||||
/// being dragged, but otherwise forces full opacity.
|
||||
/// </summary>
|
||||
private void Render(ItemData item, int quantity, int uses)
|
||||
{
|
||||
iconImage.gameObject.SetActive(true);
|
||||
iconImage.sprite = item.Icon;
|
||||
|
||||
bool showQty = quantity > 1;
|
||||
quantityText.gameObject.SetActive(showQty);
|
||||
if (showQty) quantityText.text = quantity.ToString();
|
||||
|
||||
bool hasUses = item.HasUses;
|
||||
if (usageBarRoot != null) usageBarRoot.SetActive(hasUses);
|
||||
|
||||
bool depleted = hasUses && uses <= 0;
|
||||
Color tint = depleted ? depletedTint : normalTint;
|
||||
tint.a = draggedSlot == this ? iconImage.color.a : 1f;
|
||||
iconImage.color = tint;
|
||||
|
||||
if (hasUses && usageBarFill != null && item.MaxUses > 0)
|
||||
usageBarFill.fillAmount = (float)Mathf.Max(0, uses) / item.MaxUses;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Drag & Drop
|
||||
|
||||
/// <summary>
|
||||
/// Starts dragging this cell's stack (left button, non-empty only), showing the shared ghost.
|
||||
/// </summary>
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.button != PointerEventData.InputButton.Left) return;
|
||||
if (!HasItem()) return;
|
||||
|
||||
draggedSlot = this;
|
||||
|
||||
if (ghostObject != null)
|
||||
{
|
||||
ghostObject.SetActive(true);
|
||||
ghostIcon.sprite = iconImage.sprite;
|
||||
ghostIcon.gameObject.SetActive(true);
|
||||
|
||||
bool showQty = quantityText.gameObject.activeSelf;
|
||||
ghostQuantity.gameObject.SetActive(showQty);
|
||||
if (showQty) ghostQuantity.text = quantityText.text;
|
||||
|
||||
ghostObject.transform.position = eventData.position;
|
||||
}
|
||||
|
||||
Color c = iconImage.color;
|
||||
c.a = 0.4f;
|
||||
iconImage.color = c;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the ghost with the pointer.
|
||||
/// </summary>
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (draggedSlot != this) return;
|
||||
if (ghostObject != null) ghostObject.transform.position = eventData.position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends the drag: hides the ghost and restores the source cell's opacity.
|
||||
/// </summary>
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
if (draggedSlot != this) return;
|
||||
|
||||
if (ghostObject != null) ghostObject.SetActive(false);
|
||||
|
||||
Color c = iconImage.color;
|
||||
c.a = 1f;
|
||||
iconImage.color = c;
|
||||
|
||||
draggedSlot = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drop target: hands the dragged cell and this cell to the window to route the transfer.
|
||||
/// </summary>
|
||||
public void OnDrop(PointerEventData eventData)
|
||||
{
|
||||
if (draggedSlot == null || draggedSlot == this) return;
|
||||
if (owner != null) owner.HandleTransfer(draggedSlot, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Right-click: quick-transfers this cell to the other container (deposit or withdraw).
|
||||
/// </summary>
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
if (eventData.dragging) return;
|
||||
if (eventData.button == PointerEventData.InputButton.Right && owner != null)
|
||||
owner.HandleQuickTransfer(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Whether this cell currently holds an item in its container.
|
||||
/// </summary>
|
||||
private bool HasItem()
|
||||
{
|
||||
if (container == ChestSlotContainer.Inventory)
|
||||
{
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
InventorySlot s = inv != null ? inv.GetSlot(index) : null;
|
||||
return s != null && !s.IsEmpty;
|
||||
}
|
||||
return owner != null && owner.Chest != null && owner.Chest.TryGetSlot(index, out _, out _, out _);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2052b2fcd1679c04e8720a51067c8784
|
||||
@@ -1,320 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
using Ashwild.Inventory;
|
||||
using Ashwild.UI;
|
||||
|
||||
namespace Ashwild.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// The chest window: the local player's whole inventory on the left, the opened chest's contents
|
||||
/// on the right. It is a local UI panel — opened by interacting with a <see cref="Chest"/>, closed
|
||||
/// with Escape — driven by the GameUIManager panel stack like the inventory. The inventory grid is
|
||||
/// client-authoritative data; the chest grid renders the chest's replicated view and every move is
|
||||
/// routed through the chest's server-authoritative RPCs, so two players sharing a chest stay in sync.
|
||||
///
|
||||
/// Register this panel in <c>GameUIManager.panels</c> so the stack can open/close it. The controller
|
||||
/// object stays active; Show/Hide only toggle the visual window and (un)bind the change events.
|
||||
/// </summary>
|
||||
public class ChestUI : UIPanel
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks this as a local window: input is locked and the cursor shows, but the world keeps
|
||||
/// running (never pauses), exactly like the inventory.
|
||||
/// </summary>
|
||||
public override PanelKind Kind => PanelKind.Chest;
|
||||
|
||||
/// <summary>
|
||||
/// The single chest window in the scene, reached by a chest's Interact().
|
||||
/// </summary>
|
||||
public static ChestUI Instance { get; private set; }
|
||||
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Window")]
|
||||
[Tooltip("Visual window toggled on open/close; the controller object itself stays active.")]
|
||||
[SerializeField] private GameObject windowRoot;
|
||||
[SerializeField] private TextMeshProUGUI titleText;
|
||||
|
||||
[Header("Grids")]
|
||||
[Tooltip("Parent the player-inventory cells are laid out under (left side).")]
|
||||
[SerializeField] private Transform inventoryContainer;
|
||||
[Tooltip("Parent the chest cells are laid out under (right side).")]
|
||||
[SerializeField] private Transform chestContainer;
|
||||
[Tooltip("Prefab with a ChestSlotUI, instantiated for every cell on both sides.")]
|
||||
[SerializeField] private GameObject slotPrefab;
|
||||
|
||||
[Header("Drag Ghost")]
|
||||
[SerializeField] private GameObject ghostObject;
|
||||
[SerializeField] private Image ghostIcon;
|
||||
[SerializeField] private TextMeshProUGUI ghostQuantityText;
|
||||
|
||||
[Header("Transfer Buttons")]
|
||||
[SerializeField] private Button depositAllButton;
|
||||
[SerializeField] private Button withdrawAllButton;
|
||||
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
|
||||
private ChestSlotUI[] inventorySlots;
|
||||
private ChestSlotUI[] chestSlots;
|
||||
private Chest boundChest;
|
||||
|
||||
/// <summary>
|
||||
/// The chest currently displayed on the right side (null while closed).
|
||||
/// </summary>
|
||||
public Chest 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>
|
||||
/// Sets up the shared ghost, parks the window closed and wires the transfer-all buttons.
|
||||
/// </summary>
|
||||
private void Start()
|
||||
{
|
||||
ChestSlotUI.SetupGhost(ghostObject, ghostIcon, ghostQuantityText);
|
||||
if (windowRoot != null) windowRoot.SetActive(false);
|
||||
|
||||
if (depositAllButton != null) depositAllButton.onClick.AddListener(HandleDepositAll);
|
||||
if (withdrawAllButton != null) withdrawAllButton.onClick.AddListener(HandleWithdrawAll);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the singleton and drops the button listeners on teardown.
|
||||
/// </summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (depositAllButton != null) depositAllButton.onClick.RemoveListener(HandleDepositAll);
|
||||
if (withdrawAllButton != null) withdrawAllButton.onClick.RemoveListener(HandleWithdrawAll);
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Open / Panel Lifecycle
|
||||
|
||||
/// <summary>
|
||||
/// Binds a chest and opens the window through the panel stack (cursor + input lock handled by
|
||||
/// the GameUIManager). Called from a chest's Interact().
|
||||
/// </summary>
|
||||
public void Open(Chest chest)
|
||||
{
|
||||
if (chest == null) return;
|
||||
boundChest = chest;
|
||||
|
||||
if (UIManager.Instance != null)
|
||||
UIManager.Instance.OpenPanel(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the window: builds the grids for the bound chest, binds change events and refreshes.
|
||||
/// </summary>
|
||||
public override void Show()
|
||||
{
|
||||
if (windowRoot != null) windowRoot.SetActive(true);
|
||||
|
||||
EnsureInventoryGrid();
|
||||
BuildChestGrid();
|
||||
BindChanges();
|
||||
|
||||
if (titleText != null && boundChest != null) titleText.text = boundChest.DisplayName;
|
||||
|
||||
RefreshInventory();
|
||||
RefreshChest();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the window, unbinds change events and releases the chest.
|
||||
/// </summary>
|
||||
public override void Hide()
|
||||
{
|
||||
if (ghostObject != null) ghostObject.SetActive(false);
|
||||
UnbindChanges();
|
||||
boundChest = null;
|
||||
if (windowRoot != null) windowRoot.SetActive(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instant close used when the manager initializes panels — just parks the window closed.
|
||||
/// </summary>
|
||||
public override void HideInstant()
|
||||
{
|
||||
if (ghostObject != null) ghostObject.SetActive(false);
|
||||
UnbindChanges();
|
||||
boundChest = null;
|
||||
if (windowRoot != null) windowRoot.SetActive(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Grid Building
|
||||
|
||||
/// <summary>
|
||||
/// Builds the left grid from the local player's full inventory once; reused across opens since
|
||||
/// the inventory size never changes.
|
||||
/// </summary>
|
||||
private void EnsureInventoryGrid()
|
||||
{
|
||||
if (inventorySlots != null) return;
|
||||
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
if (inv == null) return;
|
||||
|
||||
int count = inv.InventorySize;
|
||||
inventorySlots = new ChestSlotUI[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ChestSlotUI slot = Instantiate(slotPrefab, inventoryContainer).GetComponent<ChestSlotUI>();
|
||||
slot.Initialize(this, ChestSlotContainer.Inventory, i);
|
||||
inventorySlots[i] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds (or rebuilds when the size differs) the right grid to match the bound chest's slot
|
||||
/// count, so opening a small chest then a large one lays out the right number of cells.
|
||||
/// </summary>
|
||||
private void BuildChestGrid()
|
||||
{
|
||||
int count = boundChest != null ? boundChest.SlotCount : 0;
|
||||
if (chestSlots != null && chestSlots.Length == count) return;
|
||||
|
||||
if (chestSlots != null)
|
||||
foreach (ChestSlotUI s in chestSlots)
|
||||
if (s != null) Destroy(s.gameObject);
|
||||
|
||||
chestSlots = new ChestSlotUI[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ChestSlotUI slot = Instantiate(slotPrefab, chestContainer).GetComponent<ChestSlotUI>();
|
||||
slot.Initialize(this, ChestSlotContainer.Chest, i);
|
||||
chestSlots[i] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Change Binding
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the inventory's and the chest's change notifications so cells refresh live.
|
||||
/// </summary>
|
||||
private void BindChanges()
|
||||
{
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
if (inv != null) inv.onSlotChanged.AddListener(HandleInventorySlotChanged);
|
||||
if (boundChest != null) boundChest.SlotViewChanged += HandleChestSlotChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes — mirrors BindChanges exactly.
|
||||
/// </summary>
|
||||
private void UnbindChanges()
|
||||
{
|
||||
PlayerInventory inv = PlayerInventory.Instance;
|
||||
if (inv != null) inv.onSlotChanged.RemoveListener(HandleInventorySlotChanged);
|
||||
if (boundChest != null) boundChest.SlotViewChanged -= HandleChestSlotChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes a single inventory cell when its slot changes.
|
||||
/// </summary>
|
||||
private void HandleInventorySlotChanged(int index)
|
||||
{
|
||||
if (inventorySlots != null && index >= 0 && index < inventorySlots.Length)
|
||||
inventorySlots[index].Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes a single chest cell, or the whole grid when the collection was (re)seeded (-1).
|
||||
/// </summary>
|
||||
private void HandleChestSlotChanged(int index)
|
||||
{
|
||||
if (chestSlots == null) return;
|
||||
if (index < 0) { RefreshChest(); return; }
|
||||
if (index < chestSlots.Length) chestSlots[index].Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redraws every inventory cell.
|
||||
/// </summary>
|
||||
private void RefreshInventory()
|
||||
{
|
||||
if (inventorySlots == null) return;
|
||||
for (int i = 0; i < inventorySlots.Length; i++) inventorySlots[i].Refresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redraws every chest cell.
|
||||
/// </summary>
|
||||
private void RefreshChest()
|
||||
{
|
||||
if (chestSlots == null) return;
|
||||
for (int i = 0; i < chestSlots.Length; i++) chestSlots[i].Refresh();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Transfer Routing
|
||||
|
||||
/// <summary>
|
||||
/// Routes a drag-drop between two cells into the matching operation: inventory swap, deposit,
|
||||
/// withdraw, or move within the chest.
|
||||
/// </summary>
|
||||
public void HandleTransfer(ChestSlotUI from, ChestSlotUI to)
|
||||
{
|
||||
if (from == null || to == null || boundChest == null) return;
|
||||
|
||||
bool fromInv = from.Container == ChestSlotContainer.Inventory;
|
||||
bool toInv = to.Container == ChestSlotContainer.Inventory;
|
||||
|
||||
if (fromInv && toInv) InventoryUI.OnSwapRequested(from.Index, to.Index);
|
||||
else if (fromInv) boundChest.RequestDepositToSlot(from.Index, to.Index);
|
||||
else if (toInv) boundChest.RequestWithdraw(from.Index);
|
||||
else boundChest.RequestMoveWithin(from.Index, to.Index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Right-click quick transfer: an inventory cell deposits (auto-placed), a chest cell withdraws.
|
||||
/// </summary>
|
||||
public void HandleQuickTransfer(ChestSlotUI slot)
|
||||
{
|
||||
if (slot == null || boundChest == null) return;
|
||||
|
||||
if (slot.Container == ChestSlotContainer.Inventory)
|
||||
boundChest.RequestDepositToSlot(slot.Index, -1);
|
||||
else
|
||||
boundChest.RequestWithdraw(slot.Index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "Deposit all" button: stores every inventory item in the chest.
|
||||
/// </summary>
|
||||
private void HandleDepositAll()
|
||||
{
|
||||
if (boundChest != null) boundChest.RequestDepositAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "Take all" button: withdraws every chest item into the inventory.
|
||||
/// </summary>
|
||||
private void HandleWithdrawAll()
|
||||
{
|
||||
if (boundChest != null) boundChest.RequestWithdrawAll();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5e351717263d20488f62645e8ffdff3
|
||||
Reference in New Issue
Block a user