78bfdf2828
# Conflicts: # Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat # Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
228 lines
8.0 KiB
C#
228 lines
8.0 KiB
C#
using UnityEngine;
|
|
using Ashwild.Harvesting;
|
|
using Ashwild.Inventory;
|
|
|
|
namespace Ashwild.Player
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[DisallowMultipleComponent]
|
|
public class ToolBehaviour : MonoBehaviour, IHeldItemBehaviour
|
|
{
|
|
#region Serialized Fields
|
|
|
|
[Header("Harvesting")]
|
|
/// <summary>
|
|
/// Maximum distance, in metres, at which a harvestable can be hit.
|
|
/// </summary>
|
|
[SerializeField] private float harvestRange = 3f;
|
|
|
|
/// <summary>
|
|
/// Layers the harvest ray is allowed to hit.
|
|
/// </summary>
|
|
[SerializeField] private LayerMask harvestMask = ~0;
|
|
|
|
[Header("Damage")]
|
|
/// <summary>
|
|
/// Base damage before the tool's ToolPower multiplier is applied.
|
|
/// </summary>
|
|
[SerializeField] private float baseDamage = 10f;
|
|
|
|
/// <summary>
|
|
/// Damage multiplier when the tool does not match the target type (0 = strict).
|
|
/// </summary>
|
|
[SerializeField, Range(0f, 1f)] private float wrongToolMultiplier = 0.25f;
|
|
|
|
[Header("Cooldown")]
|
|
/// <summary>
|
|
/// Minimum time, in seconds, between two swings.
|
|
/// </summary>
|
|
[SerializeField] private float swingCooldown = 0.5f;
|
|
|
|
[Header("Impact")]
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[SerializeField] private float impactFallbackDelay = 0.2f;
|
|
|
|
#endregion
|
|
|
|
#region State
|
|
|
|
/// <summary>
|
|
/// Aim ray injected by the player when this item is drawn.
|
|
/// </summary>
|
|
private Transform raycastOrigin;
|
|
|
|
/// <summary>
|
|
/// The ItemData this held prefab represents, used for damage scaling.
|
|
/// </summary>
|
|
private ItemData item;
|
|
|
|
/// <summary>
|
|
/// Earliest time, in seconds, the next swing is allowed.
|
|
/// </summary>
|
|
private float nextSwingTime;
|
|
|
|
/// <summary>
|
|
/// True between a swing starting and its hit being resolved.
|
|
/// </summary>
|
|
private bool swingPending;
|
|
|
|
/// <summary>
|
|
/// Time at which a pending swing resolves itself without an impact event.
|
|
/// </summary>
|
|
private float impactDeadline;
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Subscribes to the use input and to the animation's contact frame for as long as this item is held.
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
PlayerEvents.AttackPressed += HandleAttackPressed;
|
|
PlayerEvents.AttackImpact += HandleAttackImpact;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribes when the item is put away (the prefab is destroyed).
|
|
/// </summary>
|
|
private void OnDisable()
|
|
{
|
|
PlayerEvents.AttackPressed -= HandleAttackPressed;
|
|
PlayerEvents.AttackImpact -= HandleAttackImpact;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void Update()
|
|
{
|
|
if (!swingPending) return;
|
|
if (Time.time < impactDeadline) return;
|
|
ResolveSwing();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IHeldItemBehaviour
|
|
|
|
/// <summary>
|
|
/// Links this tool to the player: stores the aim ray and the tool's ItemData.
|
|
/// </summary>
|
|
public void Setup(HeldItemContext context, ItemData item)
|
|
{
|
|
raycastOrigin = context != null ? context.RaycastOrigin : null;
|
|
this.item = item;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void HandleAttackImpact()
|
|
{
|
|
if (!swingPending) return;
|
|
ResolveSwing();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Swing Resolution
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void ResolveSwing()
|
|
{
|
|
swingPending = false;
|
|
TryHarvest();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Harvesting
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void TryHarvest()
|
|
{
|
|
if (!Physics.Raycast(raycastOrigin.position, raycastOrigin.forward, out RaycastHit hit,
|
|
harvestRange, harvestMask, QueryTriggerInteraction.Collide))
|
|
return;
|
|
|
|
Harvestable harvestable = hit.collider.GetComponentInParent<Harvestable>();
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Damage against a target type: full when the tool matches, reduced (or zero)
|
|
/// otherwise. Zero when this prefab has no ItemData.
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|