using UnityEngine; using Ashwild.Harvesting; using Ashwild.Inventory; namespace Ashwild.Player { /// /// Held-item logic for tools, placed on the tool's hand prefab (axe, pickaxe, ...). /// While the prefab is equipped it listens to the use input and harvests whatever /// the player aims at, scaled by the tool's own ItemData (ToolPower, HarvestType). /// No animation here — the FPS arm model plays its own; this is pure logic. /// [DisallowMultipleComponent] public class ToolBehaviour : MonoBehaviour, IHeldItemBehaviour { #region Serialized Fields [Header("Harvesting")] /// /// Maximum distance, in metres, at which a harvestable can be hit. /// [SerializeField] private float harvestRange = 3f; /// /// Layers the harvest ray is allowed to hit. /// [SerializeField] private LayerMask harvestMask = ~0; [Header("Damage")] /// /// Base damage before the tool's ToolPower multiplier is applied. /// [SerializeField] private float baseDamage = 10f; /// /// Damage multiplier when the tool does not match the target type (0 = strict). /// [SerializeField, Range(0f, 1f)] private float wrongToolMultiplier = 0.25f; [Header("Cooldown")] /// /// Minimum time, in seconds, between two swings. /// [SerializeField] private float swingCooldown = 0.5f; [Header("Impact")] /// /// Delay after which a swing resolves on its own when the animation never reports its contact /// frame. Safety net for an item whose attack clip carries no AnimAttackImpact event. /// [SerializeField] private float impactFallbackDelay = 0.2f; #endregion #region State /// /// Aim ray injected by the player when this item is drawn. /// private Transform raycastOrigin; /// /// The ItemData this held prefab represents, used for damage scaling. /// private ItemData item; /// /// Earliest time, in seconds, the next swing is allowed. /// private float nextSwingTime; /// /// True between a swing starting and its hit being resolved. /// private bool swingPending; /// /// Time at which a pending swing resolves itself without an impact event. /// private float impactDeadline; #endregion #region Unity Lifecycle /// /// Subscribes to the use input and to the animation's contact frame for as long as this item is held. /// private void OnEnable() { PlayerEvents.AttackPressed += HandleAttackPressed; PlayerEvents.AttackImpact += HandleAttackImpact; } /// /// Unsubscribes when the item is put away (the prefab is destroyed). /// private void OnDisable() { PlayerEvents.AttackPressed -= HandleAttackPressed; PlayerEvents.AttackImpact -= HandleAttackImpact; } /// /// Resolves a swing whose animation never announced its contact frame, so an item with no impact /// event still harvests instead of swinging forever without effect. Runs only while a swing is in /// flight. /// private void Update() { if (!swingPending) return; if (Time.time < impactDeadline) return; ResolveSwing(); } #endregion #region IHeldItemBehaviour /// /// Links this tool to the player: stores the aim ray and the tool's ItemData. /// public void Setup(HeldItemContext context, ItemData item) { raycastOrigin = context != null ? context.RaycastOrigin : null; this.item = item; } #endregion #region Event Handlers /// /// Starts a swing on use input (gated by lock, setup and cooldown). The hit is deliberately not /// resolved here: it waits for the animation's contact frame, so the tree only loses a chunk once /// the blade has actually reached it. 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; swingPending = true; impactDeadline = Time.time + impactFallbackDelay; PlayerEvents.RaiseAttackSwung(); } /// /// The swing animation reached its contact frame. Ignored when no swing of ours is in flight, so /// an impact event belonging to a previous item cannot make a freshly drawn tool hit for free. /// private void HandleAttackImpact() { if (!swingPending) return; ResolveSwing(); } #endregion #region Swing Resolution /// /// Closes an in-flight swing and applies its hit. Clearing the pending flag first makes the swing /// resolve exactly once whether the impact event or the fallback timer got here first. /// private void ResolveSwing() { swingPending = false; TryHarvest(); } #endregion #region Harvesting /// /// Raycasts forward and applies a hit to the harvestable found, or a blocked /// "clunk" when this tool deals no damage to that target type. The hit itself goes through /// Harvestable.ApplyHit (the shared path with bare-hand gathering); the tool is only worn when /// that hit actually connects, never on a swing into the air or a blocked clunk. Triggers are /// included so walkable harvestables (e.g. bushes with a trigger collider) can still be hit. /// private void TryHarvest() { if (!Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out RaycastHit hit, harvestRange, harvestMask, QueryTriggerInteraction.Collide)) return; Harvestable harvestable = hit.collider.GetComponentInParent(); if (harvestable == null) return; float damage = CalculateDamage(harvestable.HarvestType); // No proper tool match: the hit is blocked — no resources, no damage, // just a small "clunk" so it reads as "you need a tool for this". if (damage <= 0f) { harvestable.PlayBlockedFeedback(); return; } if (harvestable.ApplyHit(item, damage) && PlayerInventory.Instance != null) PlayerInventory.Instance.ConsumeSelectedToolUse(1); } /// /// Damage against a target type: full when the tool matches, reduced (or zero) /// otherwise. Zero when this prefab has no ItemData. /// private float CalculateDamage(HarvestType targetType) { if (item == null) return 0f; if (item.HarvestType == targetType) return baseDamage * item.ToolPower; // Wrong tool type: reduced damage. Set wrongToolMultiplier to 0 for a strict "right tool only" rule. return baseDamage * item.ToolPower * wrongToolMultiplier; } #endregion } }