(Feat) Item Dqta
This commit is contained in:
@@ -1,31 +1,63 @@
|
||||
|
||||
namespace Ashwild.Inventory
|
||||
{
|
||||
/// <summary>
|
||||
/// A single inventory cell: the item it holds, how many, and — for items that track uses
|
||||
/// (tool durability, multi-bite food) — the remaining uses of this specific instance. Items
|
||||
/// with uses never stack, so a slot's uses always describe one concrete instance.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class InventorySlot
|
||||
{
|
||||
private ItemData itemData;
|
||||
private int quantity;
|
||||
private int currentUses;
|
||||
|
||||
public ItemData ItemData => itemData;
|
||||
public int Quantity => quantity;
|
||||
public int CurrentUses => currentUses;
|
||||
public bool IsEmpty => itemData == null;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this slot holds a uses-tracked item whose uses have run out — kept in the
|
||||
/// inventory (shown red and unusable) when the item is not destroyed on depletion.
|
||||
/// </summary>
|
||||
public bool IsDepleted => itemData != null && itemData.HasUses && currentUses <= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Places an item in the slot, resetting its uses to full for uses-tracked items.
|
||||
/// </summary>
|
||||
public void Set(ItemData data, int qty)
|
||||
{
|
||||
itemData = data;
|
||||
quantity = qty;
|
||||
currentUses = (data != null && data.HasUses) ? data.MaxUses : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Places an item with an explicit remaining-uses value, used when restoring a partially
|
||||
/// used instance (e.g. picking a dropped tool back up). A negative value means "full".
|
||||
/// </summary>
|
||||
public void Set(ItemData data, int qty, int uses)
|
||||
{
|
||||
itemData = data;
|
||||
quantity = qty;
|
||||
if (data != null && data.HasUses)
|
||||
currentUses = uses < 0 ? data.MaxUses : uses;
|
||||
else
|
||||
currentUses = 0;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
itemData = null;
|
||||
quantity = 0;
|
||||
currentUses = 0;
|
||||
}
|
||||
|
||||
public int AddQuantity(int amount)
|
||||
{
|
||||
if (itemData == null || !itemData.IsStackable)
|
||||
if (itemData == null || !itemData.IsStackable || itemData.HasUses)
|
||||
return amount;
|
||||
|
||||
int space = itemData.MaxStackSize - quantity;
|
||||
@@ -41,9 +73,25 @@ namespace Ashwild.Inventory
|
||||
Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spends uses on this instance (a tool hit, a food bite), clamped at 0. Has no effect on
|
||||
/// items that do not track uses.
|
||||
/// </summary>
|
||||
public void ConsumeUse(int amount = 1)
|
||||
{
|
||||
if (itemData == null || !itemData.HasUses) return;
|
||||
currentUses -= amount;
|
||||
if (currentUses < 0) currentUses = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses-tracked items are non-stackable, so they never merge into an existing slot —
|
||||
/// every instance keeps its own uses bar.
|
||||
/// </summary>
|
||||
public bool CanAccept(ItemData data)
|
||||
{
|
||||
if (IsEmpty) return true;
|
||||
if (data != null && data.HasUses) return false;
|
||||
return itemData == data && itemData.IsStackable && quantity < itemData.MaxStackSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ namespace Ashwild.Inventory
|
||||
[Header("Type")]
|
||||
[SerializeField] private ItemType itemType;
|
||||
|
||||
[Header("Uses / Durability")]
|
||||
[Tooltip("How many times this item can be used before it is depleted (a tool's hits, a food's bites). 0 = no usage tracking — a normal item without a uses bar.")]
|
||||
[SerializeField] private int maxUses = 0;
|
||||
[Tooltip("When the uses reach 0: checked = the item stays in the inventory, shown red and unusable (repairable tool, refillable container); unchecked = it is destroyed.")]
|
||||
[SerializeField] private bool keepWhenDepleted = false;
|
||||
|
||||
[Header("Tool")]
|
||||
[SerializeField] private HarvestType harvestType = HarvestType.None;
|
||||
[SerializeField] private float toolPower = 1f;
|
||||
@@ -59,6 +65,21 @@ namespace Ashwild.Inventory
|
||||
public bool IsStackable => isStackable;
|
||||
public int MaxStackSize => maxStackSize;
|
||||
public ItemType ItemType => itemType;
|
||||
|
||||
public int MaxUses => maxUses;
|
||||
|
||||
/// <summary>
|
||||
/// When uses run out: true keeps the item (shown red, unusable until repaired/refilled),
|
||||
/// false destroys it. Repairable tools and refillable containers set this true.
|
||||
/// </summary>
|
||||
public bool KeepWhenDepleted => keepWhenDepleted;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this item tracks per-instance uses (a tool's durability, a food's bites). Items
|
||||
/// with uses get a slot bar and are treated as non-stackable so each instance keeps its own value.
|
||||
/// </summary>
|
||||
public bool HasUses => maxUses > 0;
|
||||
|
||||
public HarvestType HarvestType => harvestType;
|
||||
public float ToolPower => toolPower;
|
||||
public float HealthRestore => healthRestore;
|
||||
|
||||
@@ -131,7 +131,13 @@ namespace Ashwild.Inventory
|
||||
/// Server-side: sends a granted item to this inventory's owning client, where it is added.
|
||||
/// Called after the server authorises a pickup.
|
||||
/// </summary>
|
||||
public void GrantItemFromServer(ItemData item, int quantity)
|
||||
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)
|
||||
{
|
||||
if (item == null) return;
|
||||
|
||||
@@ -142,14 +148,15 @@ namespace Ashwild.Inventory
|
||||
return;
|
||||
}
|
||||
|
||||
TargetGrantItem(Owner, id, quantity);
|
||||
TargetGrantItem(Owner, id, quantity, uses);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs on the owning client: resolves the granted item and adds it locally.
|
||||
/// Runs on the owning client: resolves the granted item and adds it locally, restoring its
|
||||
/// remaining uses when one was carried across the drop.
|
||||
/// </summary>
|
||||
[TargetRpc]
|
||||
private void TargetGrantItem(NetworkConnection conn, ushort itemId, int quantity)
|
||||
private void TargetGrantItem(NetworkConnection conn, ushort itemId, int quantity, int uses)
|
||||
{
|
||||
ItemData item = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(itemId) : null;
|
||||
if (item == null)
|
||||
@@ -158,7 +165,7 @@ namespace Ashwild.Inventory
|
||||
return;
|
||||
}
|
||||
|
||||
AddItem(item, quantity);
|
||||
AddItem(item, quantity, uses);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -173,7 +180,12 @@ namespace Ashwild.Inventory
|
||||
|
||||
public InventorySlot GetSelectedSlot() => slots[selectedHotbarIndex];
|
||||
|
||||
public bool AddItem(ItemData item, int quantity = 1)
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public bool AddItem(ItemData item, int quantity = 1, int uses = -1)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
@@ -183,21 +195,32 @@ namespace Ashwild.Inventory
|
||||
|
||||
int remaining = quantity;
|
||||
|
||||
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
||||
if (!item.HasUses)
|
||||
{
|
||||
if (!slots[i].IsEmpty && slots[i].ItemData == item && slots[i].CanAccept(item))
|
||||
for (int i = 0; i < inventorySize && remaining > 0; i++)
|
||||
{
|
||||
remaining = slots[i].AddQuantity(remaining);
|
||||
NotifySlotChanged(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)
|
||||
{
|
||||
int toPlace = (item.IsStackable && remaining > item.MaxStackSize) ? item.MaxStackSize : remaining;
|
||||
slots[i].Set(item, toPlace);
|
||||
remaining -= toPlace;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -257,6 +280,12 @@ namespace Ashwild.Inventory
|
||||
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,
|
||||
/// unusable) when KeepWhenDepleted is set, otherwise it is destroyed. Ignores an already
|
||||
/// depleted item.
|
||||
/// </summary>
|
||||
public void UseItem(int index)
|
||||
{
|
||||
if (index < 0 || index >= inventorySize) return;
|
||||
@@ -264,6 +293,7 @@ namespace Ashwild.Inventory
|
||||
|
||||
ItemData item = slots[index].ItemData;
|
||||
if (item.ItemType != ItemType.Consumable) return;
|
||||
if (slots[index].IsDepleted) return;
|
||||
|
||||
if (PlayerStats.Instance != null)
|
||||
{
|
||||
@@ -273,7 +303,39 @@ namespace Ashwild.Inventory
|
||||
}
|
||||
PlayerEvents.RaiseItemConsumed(item);
|
||||
|
||||
RemoveItem(index, 1);
|
||||
if (item.HasUses)
|
||||
{
|
||||
slots[index].ConsumeUse(1);
|
||||
if (slots[index].IsDepleted && !item.KeepWhenDepleted)
|
||||
RemoveItem(index, 1);
|
||||
else
|
||||
NotifySlotChanged(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveItem(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spends durability on the currently selected hotbar item after a successful tool hit. The
|
||||
/// tool is kept at 0 uses (red, unusable until repaired) when KeepWhenDepleted is set, otherwise
|
||||
/// it is destroyed. Returns false when the selected slot holds a depleted repairable tool, so
|
||||
/// the caller can block the swing; true otherwise (including non-uses items).
|
||||
/// </summary>
|
||||
public bool ConsumeSelectedToolUse(int amount = 1)
|
||||
{
|
||||
InventorySlot slot = slots[selectedHotbarIndex];
|
||||
if (slot.IsEmpty || !slot.ItemData.HasUses) return true;
|
||||
if (slot.IsDepleted) return false;
|
||||
|
||||
ItemData item = slot.ItemData;
|
||||
slot.ConsumeUse(amount);
|
||||
if (slot.IsDepleted && !item.KeepWhenDepleted)
|
||||
RemoveItem(selectedHotbarIndex, 1);
|
||||
else
|
||||
NotifySlotChanged(selectedHotbarIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void DropItem(int index, int quantity = 1)
|
||||
@@ -282,13 +344,14 @@ namespace Ashwild.Inventory
|
||||
if (slots[index].IsEmpty) return;
|
||||
|
||||
ItemData item = slots[index].ItemData;
|
||||
int uses = item.HasUses ? slots[index].CurrentUses : -1;
|
||||
Transform origin = dropOrigin != null ? dropOrigin : transform;
|
||||
if (item.WorldPrefab != null && origin != null && PickableRegistry.Instance != null)
|
||||
{
|
||||
Vector3 dropPos = origin.position + origin.forward * dropForwardDistance;
|
||||
ushort id = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetId(item) : (ushort)0;
|
||||
if (id != 0)
|
||||
PickableRegistry.Instance.RequestDropServerRpc(id, quantity, dropPos, origin.rotation);
|
||||
PickableRegistry.Instance.RequestDropServerRpc(id, quantity, uses, dropPos, origin.rotation);
|
||||
else
|
||||
Debug.LogWarning($"[PlayerInventory] '{item.ItemName}' is not in the ItemDatabase — drop not spawned. Run Rebuild Item Database.", this);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,16 @@ namespace Ashwild.Inventory
|
||||
[SerializeField] private TextMeshProUGUI quantityText;
|
||||
[SerializeField] private Image highlightImage;
|
||||
|
||||
[Header("Uses Bar")]
|
||||
[Tooltip("Root object 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;
|
||||
[Tooltip("Icon tint applied when the item still has uses left.")]
|
||||
[SerializeField] private Color normalTint = Color.white;
|
||||
[Tooltip("Icon (and bar) tint applied when the item is depleted — a broken repairable tool / empty container.")]
|
||||
[SerializeField] private Color depletedTint = Color.red;
|
||||
|
||||
private int slotIndex;
|
||||
private Action<int, int> onSwapRequested;
|
||||
private Action<int, bool> onClicked;
|
||||
@@ -40,12 +50,17 @@ namespace Ashwild.Inventory
|
||||
onClicked = clickCallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Redraws the slot: icon, quantity, and the uses bar. Uses-tracked items (tools, multi-bite
|
||||
/// food) show a fill bar for their remaining uses and turn red once depleted.
|
||||
/// </summary>
|
||||
public void UpdateVisual(InventorySlot slot)
|
||||
{
|
||||
if (slot == null || slot.IsEmpty)
|
||||
{
|
||||
iconImage.gameObject.SetActive(false);
|
||||
quantityText.gameObject.SetActive(false);
|
||||
if (usageBarRoot != null) usageBarRoot.SetActive(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -55,9 +70,30 @@ namespace Ashwild.Inventory
|
||||
quantityText.gameObject.SetActive(showQty);
|
||||
if (showQty)
|
||||
quantityText.text = slot.Quantity.ToString();
|
||||
|
||||
UpdateUsageBar(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the uses bar and tints the icon red when the item is depleted; hides the bar entirely
|
||||
/// for items that do not track uses. Preserves the current icon alpha so a mid-drag fade stays.
|
||||
/// </summary>
|
||||
private void UpdateUsageBar(InventorySlot slot)
|
||||
{
|
||||
bool hasUses = slot.ItemData.HasUses;
|
||||
|
||||
if (usageBarRoot != null)
|
||||
usageBarRoot.SetActive(hasUses);
|
||||
|
||||
Color tint = (hasUses && slot.IsDepleted) ? depletedTint : normalTint;
|
||||
tint.a = iconImage.color.a;
|
||||
iconImage.color = tint;
|
||||
|
||||
if (hasUses && usageBarFill != null)
|
||||
usageBarFill.fillAmount = (float)slot.CurrentUses / slot.ItemData.MaxUses;
|
||||
}
|
||||
|
||||
public void SetSelected(bool selected)
|
||||
{
|
||||
if (highlightImage != null)
|
||||
|
||||
Reference in New Issue
Block a user