(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
+138 -76
View File
@@ -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,