(Feat) Add Build
This commit is contained in:
@@ -130,15 +130,16 @@ namespace Ashwild.Inventory
|
||||
|
||||
/// <summary>
|
||||
/// Server-side: sends a granted item to this inventory's owning client, where it is added.
|
||||
/// Called after the server authorises a pickup.
|
||||
/// Called after the server authorises a pickup, a cooking result or a chest withdrawal.
|
||||
/// <paramref name="uses"/> restores a specific remaining-uses value, used when a partially used
|
||||
/// instance is picked back up off the ground (negative = grant at full uses).
|
||||
/// <paramref name="preferredIndex"/> is the slot the player actually aimed at (a chest item
|
||||
/// dragged onto a precise inventory cell); it keeps the grant from auto-filling the first free
|
||||
/// slot — which is the hotbar — when the player picked a destination. Negative = auto-place.
|
||||
/// <paramref name="isTransfer"/> marks a container-to-container move (chest) so the HUD does not
|
||||
/// announce it as a gain.
|
||||
/// </summary>
|
||||
public void GrantItemFromServer(ItemData item, int quantity) => GrantItemFromServer(item, quantity, -1);
|
||||
|
||||
/// <summary>
|
||||
/// Server-side grant that also restores a specific remaining-uses value, used when a partially
|
||||
/// used instance is picked back up off the ground. A negative value means "grant at full uses".
|
||||
/// </summary>
|
||||
public void GrantItemFromServer(ItemData item, int quantity, int uses)
|
||||
public void GrantItemFromServer(ItemData item, int quantity = 1, int uses = -1, int preferredIndex = -1, bool isTransfer = false)
|
||||
{
|
||||
if (item == null) return;
|
||||
|
||||
@@ -149,15 +150,15 @@ namespace Ashwild.Inventory
|
||||
return;
|
||||
}
|
||||
|
||||
TargetGrantItem(Owner, id, quantity, uses);
|
||||
TargetGrantItem(Owner, id, quantity, uses, preferredIndex, isTransfer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs on the owning client: resolves the granted item and adds it locally, restoring its
|
||||
/// remaining uses when one was carried across the drop.
|
||||
/// remaining uses and honouring the slot the player aimed at when one was requested.
|
||||
/// </summary>
|
||||
[TargetRpc]
|
||||
private void TargetGrantItem(NetworkConnection conn, ushort itemId, int quantity, int uses)
|
||||
private void TargetGrantItem(NetworkConnection conn, ushort itemId, int quantity, int uses, int preferredIndex, bool isTransfer)
|
||||
{
|
||||
ItemData item = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(itemId) : null;
|
||||
if (item == null)
|
||||
@@ -166,7 +167,7 @@ namespace Ashwild.Inventory
|
||||
return;
|
||||
}
|
||||
|
||||
AddItem(item, quantity, uses);
|
||||
AddItem(item, quantity, uses, preferredIndex, isTransfer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -182,11 +183,92 @@ namespace Ashwild.Inventory
|
||||
public InventorySlot GetSelectedSlot() => slots[selectedHotbarIndex];
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item, stacking into existing slots first. Uses-tracked items never stack: each unit
|
||||
/// takes its own slot with its own uses bar. The optional uses value restores a partially used
|
||||
/// instance (negative = full); it only applies to uses-tracked items.
|
||||
/// Reads a slot as a container-agnostic snapshot, including its remaining uses. This is how the
|
||||
/// shared <see cref="SlotTransfer"/> rules see an inventory slot.
|
||||
/// </summary>
|
||||
public bool AddItem(ItemData item, int quantity = 1, int uses = -1)
|
||||
public SlotContent ReadSlot(int index)
|
||||
{
|
||||
InventorySlot slot = GetSlot(index);
|
||||
if (slot == null || slot.IsEmpty) return SlotContent.Empty;
|
||||
return SlotContent.Of(slot.ItemData, slot.Quantity, slot.CurrentUses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a snapshot back into a slot and notifies the UI. Always goes through the uses-carrying
|
||||
/// Set overload, so a half-worn tool stays half-worn instead of silently repairing itself.
|
||||
/// </summary>
|
||||
public void WriteSlot(int index, SlotContent content)
|
||||
{
|
||||
InventorySlot slot = GetSlot(index);
|
||||
if (slot == null) return;
|
||||
|
||||
if (content.IsEmpty) slot.Clear();
|
||||
else slot.Set(content.Item, content.Quantity, content.Uses);
|
||||
|
||||
NotifySlotChanged(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a stack between two of this inventory's slots — the drag-and-drop operation. Delegates
|
||||
/// the decision (move / merge / swap) to the shared rules, so it behaves exactly like a chest
|
||||
/// transfer.
|
||||
/// </summary>
|
||||
public void MoveSlot(int fromIndex, int toIndex)
|
||||
{
|
||||
if (fromIndex == toIndex) return;
|
||||
if (GetSlot(fromIndex) == null || GetSlot(toIndex) == null) return;
|
||||
|
||||
SlotContent from = ReadSlot(fromIndex);
|
||||
SlotContent to = ReadSlot(toIndex);
|
||||
|
||||
if (!SlotTransfer.Move(ref from, ref to)) return;
|
||||
|
||||
WriteSlot(fromIndex, from);
|
||||
WriteSlot(toIndex, to);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a stack in half into the first empty slot — half stays, half moves. No-op on an empty
|
||||
/// slot, a single unit, or a full inventory. It lives here rather than in the context menu so the
|
||||
/// split goes through the same write path as every other change; done by hand from the UI it
|
||||
/// bypassed NotifySlotChanged and the bus event never fired.
|
||||
/// </summary>
|
||||
public void SplitSlot(int index)
|
||||
{
|
||||
SlotContent source = ReadSlot(index);
|
||||
if (source.IsEmpty || source.Quantity <= 1) return;
|
||||
|
||||
for (int i = 0; i < inventorySize; i++)
|
||||
{
|
||||
if (!slots[i].IsEmpty) continue;
|
||||
|
||||
int moved = source.Quantity / 2;
|
||||
SlotContent half = SlotContent.Of(source.Item, moved, source.Uses);
|
||||
source.Quantity -= moved;
|
||||
|
||||
WriteSlot(index, source);
|
||||
WriteSlot(i, half);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item without the player picking a destination (a pickup, a craft result, a chest
|
||||
/// withdrawal): merges into matching stacks first, then fills empty slots. Uses-tracked items
|
||||
/// never stack, so each unit claims its own slot with its own uses bar; the optional uses value
|
||||
/// restores a partially used instance (negative = full).
|
||||
///
|
||||
/// <paramref name="preferredIndex"/> is the slot the player actually aimed at, tried first so a
|
||||
/// targeted transfer lands where they dropped it instead of the default scan silently claiming a
|
||||
/// hotbar slot. It is only a hint: whatever does not fit there falls through to normal placement,
|
||||
/// so a slot that got occupied in the meantime degrades gracefully rather than losing items.
|
||||
///
|
||||
/// <paramref name="isTransfer"/> marks a stack that merely moved between the player's own
|
||||
/// containers (a chest withdrawal, a refund) rather than being acquired from the world, so the
|
||||
/// HUD does not announce a "gain" for shuffling items around. Craft discovery still counts it —
|
||||
/// pulling an item a co-op partner left in a chest is a genuine first acquisition.
|
||||
/// </summary>
|
||||
public bool AddItem(ItemData item, int quantity = 1, int uses = -1, int preferredIndex = -1, bool isTransfer = false)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
@@ -194,66 +276,64 @@ namespace Ashwild.Inventory
|
||||
return false;
|
||||
}
|
||||
|
||||
int remaining = quantity;
|
||||
SlotContent incoming = SlotContent.Of(item, quantity, uses);
|
||||
|
||||
if (!item.HasUses)
|
||||
{
|
||||
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
||||
{
|
||||
if (!slots[i].IsEmpty && slots[i].ItemData == item && slots[i].CanAccept(item))
|
||||
{
|
||||
remaining = slots[i].AddQuantity(remaining);
|
||||
NotifySlotChanged(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
||||
{
|
||||
if (slots[i].IsEmpty)
|
||||
{
|
||||
if (item.HasUses)
|
||||
{
|
||||
slots[i].Set(item, 1, uses);
|
||||
remaining -= 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
int toPlace = (item.IsStackable && remaining > item.MaxStackSize) ? item.MaxStackSize : remaining;
|
||||
slots[i].Set(item, toPlace);
|
||||
remaining -= toPlace;
|
||||
}
|
||||
NotifySlotChanged(i);
|
||||
}
|
||||
}
|
||||
if (preferredIndex >= 0 && preferredIndex < inventorySize)
|
||||
StackInto(preferredIndex, ref incoming);
|
||||
|
||||
int added = quantity - remaining;
|
||||
for (int i = 0; i < inventorySize && !incoming.IsEmpty; i++)
|
||||
if (!slots[i].IsEmpty) StackInto(i, ref incoming);
|
||||
|
||||
for (int i = 0; i < inventorySize && !incoming.IsEmpty; i++)
|
||||
if (slots[i].IsEmpty) StackInto(i, ref incoming);
|
||||
|
||||
int added = quantity - (incoming.IsEmpty ? 0 : incoming.Quantity);
|
||||
if (added > 0)
|
||||
{
|
||||
onItemAdded?.Invoke(item, added);
|
||||
PlayerEvents.RaiseItemAdded(item, added);
|
||||
PlayerEvents.RaiseItemAdded(item, added, isTransfer);
|
||||
}
|
||||
return remaining <= 0;
|
||||
return incoming.IsEmpty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the given quantity of an item would fit without mutating anything.
|
||||
/// Pushes as much of the incoming stack as one slot accepts, through the shared auto-placement
|
||||
/// rule (never swaps), and writes the slot back when it changed.
|
||||
/// </summary>
|
||||
private void StackInto(int index, ref SlotContent incoming)
|
||||
{
|
||||
SlotContent target = ReadSlot(index);
|
||||
if (!SlotTransfer.TryStack(ref incoming, ref target)) return;
|
||||
WriteSlot(index, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the given quantity would fit, without mutating anything. Dry-runs the exact
|
||||
/// same auto-placement rule the real add uses, against copies of the slots — so this answer can
|
||||
/// never drift from what <see cref="AddItem"/> would actually do, which a hand-written capacity
|
||||
/// calculation eventually would.
|
||||
/// </summary>
|
||||
public bool CanFit(ItemData item, int quantity = 1)
|
||||
{
|
||||
if (item == null) return false;
|
||||
|
||||
int remaining = quantity;
|
||||
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
||||
SlotContent incoming = SlotContent.Of(item, quantity, -1);
|
||||
|
||||
for (int i = 0; i < inventorySize && !incoming.IsEmpty; i++)
|
||||
{
|
||||
if (!slots[i].IsEmpty && slots[i].ItemData == item && slots[i].CanAccept(item))
|
||||
remaining -= Mathf.Min(remaining, item.MaxStackSize - slots[i].Quantity);
|
||||
if (slots[i].IsEmpty) continue;
|
||||
SlotContent target = ReadSlot(i);
|
||||
SlotTransfer.TryStack(ref incoming, ref target);
|
||||
}
|
||||
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
||||
|
||||
for (int i = 0; i < inventorySize && !incoming.IsEmpty; i++)
|
||||
{
|
||||
if (slots[i].IsEmpty)
|
||||
remaining -= Mathf.Min(remaining, item.IsStackable ? item.MaxStackSize : 1);
|
||||
if (!slots[i].IsEmpty) continue;
|
||||
SlotContent target = SlotContent.Empty;
|
||||
SlotTransfer.TryStack(ref incoming, ref target);
|
||||
}
|
||||
return remaining <= 0;
|
||||
|
||||
return incoming.IsEmpty;
|
||||
}
|
||||
|
||||
public void RemoveItem(int index, int quantity = 1)
|
||||
@@ -263,24 +343,6 @@ namespace Ashwild.Inventory
|
||||
NotifySlotChanged(index);
|
||||
}
|
||||
|
||||
public void SwapSlots(int indexA, int indexB)
|
||||
{
|
||||
if (indexA < 0 || indexA >= inventorySize) return;
|
||||
if (indexB < 0 || indexB >= inventorySize) return;
|
||||
|
||||
ItemData tempData = slots[indexA].ItemData;
|
||||
int tempQty = slots[indexA].Quantity;
|
||||
|
||||
if (slots[indexB].IsEmpty) slots[indexA].Clear();
|
||||
else slots[indexA].Set(slots[indexB].ItemData, slots[indexB].Quantity);
|
||||
|
||||
if (tempData == null) slots[indexB].Clear();
|
||||
else slots[indexB].Set(tempData, tempQty);
|
||||
|
||||
NotifySlotChanged(indexA);
|
||||
NotifySlotChanged(indexB);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumes one use of a consumable: applies its restores once (per bite, for multi-use food)
|
||||
/// then either spends a use or removes one unit. A multi-use item is kept at 0 uses (red,
|
||||
|
||||
Reference in New Issue
Block a user