From 931bc9c9e22de89975213948e455d1b78ed47ac9 Mon Sep 17 00:00:00 2001 From: Mathew Date: Thu, 25 Jun 2026 14:40:21 +0200 Subject: [PATCH] (Feat) Item Dqta --- Assets/GAME/Prefabs/UI/Slot Ui.prefab | 172 +++++++++++++++++- Assets/GAME/Script/Inventory/InventorySlot.cs | 50 ++++- Assets/GAME/Script/Inventory/ItemData.cs | 21 +++ .../GAME/Script/Inventory/PlayerInventory.cs | 93 ++++++++-- Assets/GAME/Script/Inventory/SlotUI.cs | 36 ++++ .../GAME/Script/Network/PickableRegistry.cs | 6 +- .../Script/Player/HeldItems/ToolBehaviour.cs | 8 +- .../GAME/ScriptableObjects/Items/Hammer.asset | 2 + .../ScriptableObjects/Items/Pickaxe.asset | 2 + .../ScriptableObjects/Items/WoodenAxe.asset | 2 + 10 files changed, 372 insertions(+), 20 deletions(-) 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