Files
Emberwild/Assets/GAME/Script/Harvesting/Harvestable.cs
T
2026-06-22 16:18:34 +02:00

186 lines
6.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.10.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
}
}