186 lines
6.6 KiB
C#
186 lines
6.6 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
using DG.Tweening;
|
||
using Ashwild.Inventory;
|
||
using Ashwild.Network;
|
||
|
||
namespace Ashwild.Harvesting
|
||
{
|
||
/// <summary>
|
||
/// A harvestable world object (tree, rock, …). A WorldObject backed by the HarvestableRegistry:
|
||
/// the registry owns the authoritative health, loot grant, depletion and respawn, while this
|
||
/// component holds the authored data (max health, drop tables, respawn settings) and plays the
|
||
/// local feedback (squash/stretch on hit, blocked "clunk", fall on depletion).
|
||
/// </summary>
|
||
public class Harvestable : WorldObject
|
||
{
|
||
#region Serialized Fields
|
||
|
||
[Header("Harvestable")]
|
||
[SerializeField] private HarvestType harvestType = HarvestType.Tree;
|
||
[SerializeField] private float maxHealth = 100f;
|
||
|
||
[Header("Drops (per tool)")]
|
||
[Tooltip("Each entry is the loot for one tool. Add an entry with no tool as a fallback for unlisted tools.")]
|
||
[SerializeField] private ToolDropTable[] toolDrops;
|
||
|
||
[Header("Hit Feedback")]
|
||
[Tooltip("Squash & stretch amount on impact (0.1–0.2 reads well).")]
|
||
[SerializeField] private float punchScale = 0.15f;
|
||
[Tooltip("Bend angle (degrees) the object tips away from the hit.")]
|
||
[SerializeField] private float punchAngle = 8f;
|
||
[SerializeField] private float punchDuration = 0.3f;
|
||
[SerializeField] private int punchVibrato = 6;
|
||
[SerializeField, Range(0f, 1f)] private float punchElasticity = 0.5f;
|
||
|
||
[Header("Blocked Feedback (wrong / no tool)")]
|
||
[Tooltip("Tiny wobble played when the hit deals no damage (no proper tool).")]
|
||
[SerializeField] private float blockedAngle = 2.5f;
|
||
[SerializeField] private float blockedDuration = 0.2f;
|
||
|
||
[Header("Respawn")]
|
||
[SerializeField] private bool canRespawn = true;
|
||
[SerializeField] private float respawnDelay = 60f;
|
||
|
||
[Header("Events")]
|
||
public UnityEvent onHit;
|
||
public UnityEvent onDepleted;
|
||
|
||
#endregion
|
||
|
||
#region Public Data (read by the registry, server-side)
|
||
|
||
/// <summary>
|
||
/// What kind of tool harvests this (matched against the tool's HarvestType).
|
||
/// </summary>
|
||
public HarvestType HarvestType => harvestType;
|
||
|
||
/// <summary>
|
||
/// Full health used to lazy-initialise the registry's authoritative health.
|
||
/// </summary>
|
||
public float MaxHealth => maxHealth;
|
||
|
||
/// <summary>
|
||
/// Whether this object comes back after a delay once depleted.
|
||
/// </summary>
|
||
public bool CanRespawn => canRespawn;
|
||
|
||
/// <summary>
|
||
/// Seconds before a depleted object respawns.
|
||
/// </summary>
|
||
public float RespawnDelay => respawnDelay;
|
||
|
||
/// <summary>
|
||
/// Rolls the loot for a hit with the given tool (server-side). Returns the items + quantities
|
||
/// to grant; the registry does the actual granting. No inventory access here.
|
||
/// </summary>
|
||
public IEnumerable<(ItemData item, int quantity)> RollDrops(ItemData tool)
|
||
{
|
||
ResourceDrop[] table = GetDropsFor(tool);
|
||
if (table == null) yield break;
|
||
|
||
for (int i = 0; i < table.Length; i++)
|
||
{
|
||
if (table[i].item == null) continue;
|
||
if (Random.value > table[i].dropChance) continue;
|
||
|
||
int qty = Random.Range(table[i].minQuantity, table[i].maxQuantity + 1);
|
||
if (qty > 0)
|
||
yield return (table[i].item, qty);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Feedback (client-side)
|
||
|
||
/// <summary>
|
||
/// Squash & stretch + bend away from the hit. Played on every client (via the registry) and
|
||
/// locally on the hitter for instant response.
|
||
/// </summary>
|
||
public void PlayHitEffect(Vector3 hitDirection)
|
||
{
|
||
transform.DOComplete();
|
||
|
||
Vector3 tiltAxis = TiltAxisFor(hitDirection);
|
||
transform.DOPunchRotation(tiltAxis * punchAngle, punchDuration, punchVibrato, punchElasticity);
|
||
|
||
Vector3 squash = new Vector3(punchScale, -punchScale, punchScale);
|
||
transform.DOPunchScale(squash, punchDuration, punchVibrato, punchElasticity);
|
||
|
||
onHit?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Small "clunk" when a hit deals no damage (player lacks the proper tool). Local only.
|
||
/// </summary>
|
||
public void PlayBlockedFeedback(Vector3 hitDirection)
|
||
{
|
||
transform.DOComplete();
|
||
Vector3 tiltAxis = TiltAxisFor(hitDirection);
|
||
transform.DOPunchRotation(tiltAxis * blockedAngle, blockedDuration, punchVibrato, punchElasticity);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region WorldObject
|
||
|
||
/// <summary>
|
||
/// This harvestable belongs to the HarvestableRegistry.
|
||
/// </summary>
|
||
protected override WorldObjectRegistry GetRegistry() => HarvestableRegistry.Instance;
|
||
|
||
/// <summary>
|
||
/// Hides the object when depleted; on a real depletion (not catch-up) fires onDepleted first.
|
||
/// </summary>
|
||
public override void HideAsInactive(bool fresh)
|
||
{
|
||
if (fresh) onDepleted?.Invoke();
|
||
gameObject.SetActive(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Brings the object back on respawn.
|
||
/// </summary>
|
||
public override void ShowActive()
|
||
{
|
||
gameObject.SetActive(true);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Internal Helpers
|
||
|
||
/// <summary>
|
||
/// Drop table matching the given tool, or the fallback entry (tool == null) when the tool
|
||
/// isn't listed explicitly. Returns null if nothing matches.
|
||
/// </summary>
|
||
private ResourceDrop[] GetDropsFor(ItemData tool)
|
||
{
|
||
ResourceDrop[] fallback = null;
|
||
for (int i = 0; i < toolDrops.Length; i++)
|
||
{
|
||
if (toolDrops[i].tool == tool) return toolDrops[i].drops;
|
||
if (toolDrops[i].tool == null) fallback = toolDrops[i].drops;
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Rotation axis that tips the object's top in the direction of the hit. Falls back to a fixed
|
||
/// axis when the hit is vertical or has no horizontal component.
|
||
/// </summary>
|
||
private Vector3 TiltAxisFor(Vector3 hitDirection)
|
||
{
|
||
Vector3 horizontal = new Vector3(hitDirection.x, 0f, hitDirection.z);
|
||
if (horizontal.sqrMagnitude < 0.0001f)
|
||
return Vector3.right;
|
||
|
||
return Vector3.Cross(Vector3.up, horizontal.normalized);
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|