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 } }