using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using DG.Tweening;
using Ashwild.Inventory;
using Ashwild.Network;
namespace Ashwild.Harvesting
{
///
/// 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).
///
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)
///
/// What kind of tool harvests this (matched against the tool's HarvestType).
///
public HarvestType HarvestType => harvestType;
///
/// Full health used to lazy-initialise the registry's authoritative health.
///
public float MaxHealth => maxHealth;
///
/// Whether this object comes back after a delay once depleted.
///
public bool CanRespawn => canRespawn;
///
/// Seconds before a depleted object respawns.
///
public float RespawnDelay => respawnDelay;
///
/// 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.
///
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)
///
/// Squash & stretch + bend away from the hit. Played on every client (via the registry) and
/// locally on the hitter for instant response.
///
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();
}
///
/// Small "clunk" when a hit deals no damage (player lacks the proper tool). Local only.
///
public void PlayBlockedFeedback(Vector3 hitDirection)
{
transform.DOComplete();
Vector3 tiltAxis = TiltAxisFor(hitDirection);
transform.DOPunchRotation(tiltAxis * blockedAngle, blockedDuration, punchVibrato, punchElasticity);
}
#endregion
#region WorldObject
///
/// This harvestable belongs to the HarvestableRegistry.
///
protected override WorldObjectRegistry GetRegistry() => HarvestableRegistry.Instance;
///
/// Hides the object when depleted; on a real depletion (not catch-up) fires onDepleted first.
///
public override void HideAsInactive(bool fresh)
{
if (fresh) onDepleted?.Invoke();
gameObject.SetActive(false);
}
///
/// Brings the object back on respawn.
///
public override void ShowActive()
{
gameObject.SetActive(true);
}
#endregion
#region Internal Helpers
///
/// Drop table matching the given tool, or the fallback entry (tool == null) when the tool
/// isn't listed explicitly. Returns null if nothing matches.
///
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;
}
///
/// 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.
///
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
}
}