diff --git a/Assets/GAME/Prefabs/UI/Slot Ui.prefab b/Assets/GAME/Prefabs/UI/Slot Ui.prefab
index c34fcf07..e16f0f7e 100644
--- a/Assets/GAME/Prefabs/UI/Slot Ui.prefab
+++ b/Assets/GAME/Prefabs/UI/Slot Ui.prefab
@@ -104,7 +104,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
- m_Sprite: {fileID: 0}
+ m_Sprite: {fileID: 21300000, guid: 2237e4ae38354ff459a9b61781694de6, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
@@ -114,6 +114,171 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
+--- !u!1 &5573310796372301174
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 6703316971934351753}
+ - component: {fileID: 4480407019944074660}
+ - component: {fileID: 2465922160796796964}
+ m_Layer: 5
+ m_Name: Image (1)
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!224 &6703316971934351753
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5573310796372301174}
+ m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 5909651071786952382}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0}
+ m_AnchorMax: {x: 0.5, y: 0}
+ m_AnchoredPosition: {x: 0, y: -0.00010586}
+ m_SizeDelta: {x: 80.3081, y: 8.2002}
+ m_Pivot: {x: 0.5, y: 0}
+--- !u!222 &4480407019944074660
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5573310796372301174}
+ m_CullTransparentMesh: 1
+--- !u!114 &2465922160796796964
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5573310796372301174}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
+ m_Material: {fileID: 0}
+ m_Color: {r: 0.4862745, g: 0.9882354, b: 0, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 21300000, guid: 89cc97401eb588a408315f5fa0148929, type: 3}
+ m_Type: 3
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 0
+ m_FillAmount: 0.76
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 2.8
+--- !u!1 &5983078061427925415
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 5909651071786952382}
+ - component: {fileID: 4758672237807855567}
+ - component: {fileID: 6423672678987582984}
+ - component: {fileID: 5188521778652969812}
+ m_Layer: 5
+ m_Name: Mask Durability
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 0
+--- !u!224 &5909651071786952382
+RectTransform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5983078061427925415}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 6703316971934351753}
+ m_Father: {fileID: 4729973786354734962}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+ m_AnchorMin: {x: 0.5, y: 0}
+ m_AnchorMax: {x: 0.5, y: 0}
+ m_AnchoredPosition: {x: 0, y: 9.200001}
+ m_SizeDelta: {x: 80.3081, y: 8.2001}
+ m_Pivot: {x: 0.5, y: 0}
+--- !u!222 &4758672237807855567
+CanvasRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5983078061427925415}
+ m_CullTransparentMesh: 1
+--- !u!114 &6423672678987582984
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5983078061427925415}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
+ m_Material: {fileID: 0}
+ m_Color: {r: 0.3056603, g: 0.3056603, b: 0.3056603, a: 1}
+ m_RaycastTarget: 1
+ m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
+ m_Maskable: 1
+ m_OnCullStateChanged:
+ m_PersistentCalls:
+ m_Calls: []
+ m_Sprite: {fileID: 21300000, guid: b3f0d976f6d6802479d6465d11b3aa68, type: 3}
+ m_Type: 1
+ m_PreserveAspect: 0
+ m_FillCenter: 1
+ m_FillMethod: 4
+ m_FillAmount: 1
+ m_FillClockwise: 1
+ m_FillOrigin: 0
+ m_UseSpriteMesh: 0
+ m_PixelsPerUnitMultiplier: 7.79
+--- !u!114 &5188521778652969812
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 5983078061427925415}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Mask
+ m_ShowMaskGraphic: 1
--- !u!1 &6747954976634286854
GameObject:
m_ObjectHideFlags: 0
@@ -219,6 +384,7 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 850401354156706093}
+ - {fileID: 5909651071786952382}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@@ -241,6 +407,10 @@ MonoBehaviour:
iconImage: {fileID: 6241670606430176389}
quantityText: {fileID: 2578968024406161774}
highlightImage: {fileID: 9078470808336705615}
+ usageBarRoot: {fileID: 5983078061427925415}
+ usageBarFill: {fileID: 2465922160796796964}
+ normalTint: {r: 0.4862745, g: 0.9882354, b: 0, a: 1}
+ depletedTint: {r: 1, g: 0, b: 0, a: 1}
--- !u!1 &8681194172915985056
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/GAME/Script/Inventory/InventorySlot.cs b/Assets/GAME/Script/Inventory/InventorySlot.cs
index be13477c..f26888b2 100644
--- a/Assets/GAME/Script/Inventory/InventorySlot.cs
+++ b/Assets/GAME/Script/Inventory/InventorySlot.cs
@@ -1,31 +1,63 @@
namespace Ashwild.Inventory
{
+ ///
+ /// 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.
+ ///
[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;
+ ///
+ /// 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.
+ ///
+ public bool IsDepleted => itemData != null && itemData.HasUses && currentUses <= 0;
+
+ ///
+ /// Places an item in the slot, resetting its uses to full for uses-tracked items.
+ ///
public void Set(ItemData data, int qty)
{
itemData = data;
quantity = qty;
+ currentUses = (data != null && data.HasUses) ? data.MaxUses : 0;
+ }
+
+ ///
+ /// 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".
+ ///
+ 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();
}
+ ///
+ /// Spends uses on this instance (a tool hit, a food bite), clamped at 0. Has no effect on
+ /// items that do not track uses.
+ ///
+ public void ConsumeUse(int amount = 1)
+ {
+ if (itemData == null || !itemData.HasUses) return;
+ currentUses -= amount;
+ if (currentUses < 0) currentUses = 0;
+ }
+
+ ///
+ /// Uses-tracked items are non-stackable, so they never merge into an existing slot —
+ /// every instance keeps its own uses bar.
+ ///
public bool CanAccept(ItemData data)
{
if (IsEmpty) return true;
+ if (data != null && data.HasUses) return false;
return itemData == data && itemData.IsStackable && quantity < itemData.MaxStackSize;
}
}
diff --git a/Assets/GAME/Script/Inventory/ItemData.cs b/Assets/GAME/Script/Inventory/ItemData.cs
index 9c5f4ae0..c2b6706a 100644
--- a/Assets/GAME/Script/Inventory/ItemData.cs
+++ b/Assets/GAME/Script/Inventory/ItemData.cs
@@ -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;
+
+ ///
+ /// 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.
+ ///
+ public bool KeepWhenDepleted => keepWhenDepleted;
+
+ ///
+ /// 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.
+ ///
+ public bool HasUses => maxUses > 0;
+
public HarvestType HarvestType => harvestType;
public float ToolPower => toolPower;
public float HealthRestore => healthRestore;
diff --git a/Assets/GAME/Script/Inventory/PlayerInventory.cs b/Assets/GAME/Script/Inventory/PlayerInventory.cs
index c21cd332..7ddee90f 100644
--- a/Assets/GAME/Script/Inventory/PlayerInventory.cs
+++ b/Assets/GAME/Script/Inventory/PlayerInventory.cs
@@ -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.
///
- public void GrantItemFromServer(ItemData item, int quantity)
+ public void GrantItemFromServer(ItemData item, int quantity) => GrantItemFromServer(item, quantity, -1);
+
+ ///
+ /// 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".
+ ///
+ 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);
}
///
- /// 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.
///
[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)
+ ///
+ /// 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.
+ ///
+ 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);
}
+ ///
+ /// 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.
+ ///
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);
+ }
+ }
+
+ ///
+ /// 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).
+ ///
+ 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);
}
diff --git a/Assets/GAME/Script/Inventory/SlotUI.cs b/Assets/GAME/Script/Inventory/SlotUI.cs
index 2edaecf2..cae702c1 100644
--- a/Assets/GAME/Script/Inventory/SlotUI.cs
+++ b/Assets/GAME/Script/Inventory/SlotUI.cs
@@ -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 onSwapRequested;
private Action onClicked;
@@ -40,12 +50,17 @@ namespace Ashwild.Inventory
onClicked = clickCallback;
}
+ ///
+ /// 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.
+ ///
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);
}
}
+ ///
+ /// 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.
+ ///
+ 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)
diff --git a/Assets/GAME/Script/Network/PickableRegistry.cs b/Assets/GAME/Script/Network/PickableRegistry.cs
index 94352332..bbd86b5b 100644
--- a/Assets/GAME/Script/Network/PickableRegistry.cs
+++ b/Assets/GAME/Script/Network/PickableRegistry.cs
@@ -24,6 +24,7 @@ namespace Ashwild.Network
{
public ushort itemId;
public int quantity;
+ public int uses;
public Vector3 position;
public Quaternion rotation;
}
@@ -135,7 +136,7 @@ namespace Ashwild.Network
ItemData item = ItemDatabase.Instance != null ? ItemDatabase.Instance.GetItem(record.itemId) : null;
activeDrops.Remove(id);
- if (item != null) inventory.GrantItemFromServer(item, record.quantity);
+ if (item != null) inventory.GrantItemFromServer(item, record.quantity, record.uses);
}
}
@@ -143,7 +144,7 @@ namespace Ashwild.Network
/// Owner-side entry: asks the server to spawn a dropped item into the world.
///
[ServerRpc(RequireOwnership = false)]
- public void RequestDropServerRpc(ushort itemId, int quantity, Vector3 position, Quaternion rotation, NetworkConnection conn = null)
+ public void RequestDropServerRpc(ushort itemId, int quantity, int uses, Vector3 position, Quaternion rotation, NetworkConnection conn = null)
{
if (ItemDatabase.Instance == null || ItemDatabase.Instance.GetItem(itemId) == null) return;
@@ -152,6 +153,7 @@ namespace Ashwild.Network
{
itemId = itemId,
quantity = quantity,
+ uses = uses,
position = position,
rotation = rotation
});
diff --git a/Assets/GAME/Script/Player/HeldItems/ToolBehaviour.cs b/Assets/GAME/Script/Player/HeldItems/ToolBehaviour.cs
index 87aa31aa..50413858 100644
--- a/Assets/GAME/Script/Player/HeldItems/ToolBehaviour.cs
+++ b/Assets/GAME/Script/Player/HeldItems/ToolBehaviour.cs
@@ -102,12 +102,14 @@ namespace Ashwild.Player
///
/// Swings on use input (gated by lock, setup and cooldown) and harvests the
- /// aimed target.
+ /// aimed target. A depleted but non-destroyed tool (a worn-out repairable axe) is
+ /// unusable: the swing is blocked until it is repaired.
///
private void HandleAttackPressed()
{
if (PlayerEvents.InputLocked) return;
if (raycastOrigin == null) return;
+ if (PlayerInventory.Instance != null && PlayerInventory.Instance.GetSelectedSlot().IsDepleted) return;
if (Time.time < nextSwingTime) return;
nextSwingTime = Time.time + swingCooldown;
@@ -160,6 +162,10 @@ namespace Ashwild.Player
HarvestableRegistry.Instance.RequestHitServerRpc(harvestable.Id, damage, toolId, hitDirection);
PlayerEvents.RaiseHarvestableHit(harvestable, damage);
+
+ // Wear the tool only on a real connecting hit, never on a swing into the air or a blocked clunk.
+ if (PlayerInventory.Instance != null)
+ PlayerInventory.Instance.ConsumeSelectedToolUse(1);
}
///
diff --git a/Assets/GAME/ScriptableObjects/Items/Hammer.asset b/Assets/GAME/ScriptableObjects/Items/Hammer.asset
index 093dd2db..b3ab99f1 100644
--- a/Assets/GAME/ScriptableObjects/Items/Hammer.asset
+++ b/Assets/GAME/ScriptableObjects/Items/Hammer.asset
@@ -18,6 +18,8 @@ MonoBehaviour:
isStackable: 0
maxStackSize: 64
itemType: 1
+ maxUses: 60
+ keepWhenDepleted: 1
harvestType: 0
toolPower: 1
healthRestore: 0
diff --git a/Assets/GAME/ScriptableObjects/Items/Pickaxe.asset b/Assets/GAME/ScriptableObjects/Items/Pickaxe.asset
index 540cfd0c..a06fab2d 100644
--- a/Assets/GAME/ScriptableObjects/Items/Pickaxe.asset
+++ b/Assets/GAME/ScriptableObjects/Items/Pickaxe.asset
@@ -18,6 +18,8 @@ MonoBehaviour:
isStackable: 0
maxStackSize: 64
itemType: 1
+ maxUses: 100
+ keepWhenDepleted: 1
harvestType: 2
toolPower: 1
healthRestore: 0
diff --git a/Assets/GAME/ScriptableObjects/Items/WoodenAxe.asset b/Assets/GAME/ScriptableObjects/Items/WoodenAxe.asset
index 3ba0c086..7607113b 100644
--- a/Assets/GAME/ScriptableObjects/Items/WoodenAxe.asset
+++ b/Assets/GAME/ScriptableObjects/Items/WoodenAxe.asset
@@ -18,6 +18,8 @@ MonoBehaviour:
isStackable: 0
maxStackSize: 64
itemType: 1
+ maxUses: 80
+ keepWhenDepleted: 1
harvestType: 1
toolPower: 1
healthRestore: 0