Files
Emberwild/Assets/External/NV3D/Shatter Stone/Shared/Scripts/OreNode.cs
T
2026-06-22 16:18:34 +02:00

203 lines
6.8 KiB
C#

/*******************************************************************************************
* File: OreNode.cs
* Author: NV3D
* Description: Core logic for handling ore node interactions, animations, and drops.
* Copyright © 2025 NV3D. All rights reserved.
* This code is subject to the Unity Asset Store EULA and may not be redistributed or resold.
*******************************************************************************************/
//This script manages behaviour and interactions with the Shatter Stone ore nodes. Requires MiningNodeAudio.cs
using System;
using System.Collections;
using UnityEngine;
using Random = UnityEngine.Random;
namespace ShatterStone
{
/// <summary>
/// A cache for the visual node bounds
/// </summary>
public struct OreNodeBounds
{
public float minX, maxX, minZ, maxZ, centerY;
public OreNodeBounds(float minX, float maxX, float minZ, float maxZ, float centerY)
{
this.minX = minX;
this.maxX = maxX;
this.minZ = minZ;
this.maxZ = maxZ;
this.centerY = centerY;
}
}
/// <summary>
/// Represents a generic ore node that can be gathered via interactions
/// </summary>
public class OreNode : MonoBehaviour
{
#region Serialized Fields
[Header("Drop Settings")]
[SerializeField] protected GameObject pieces;
[SerializeField] protected GameObject refinedPickup;
[SerializeField, Min(0)] protected int dropOnHit;
[SerializeField, Min(1)] protected int hitsToDestroy;
[SerializeField, Min(0)] protected int dropOnDestroy;
[Header("Knockback Settings")]
[SerializeField] protected Vector3 knockAngle;
[SerializeField] protected AnimationCurve knockCurve;
[SerializeField] protected float knockDuration = 1f;
[Header("Respawn Settings")]
[SerializeField] protected bool enableRespawn = true;
[SerializeField] protected float respawnDelay = 30f;
[Header("Configuration")]
[SerializeField] protected bool cacheVisualBoundaries = true;
[SerializeField] protected MiningNodeAudio nodeAudio;
[SerializeField] protected Collider nodeCollider;
[SerializeField] protected Renderer[] childRenderers;
#endregion
#region Private Fields
private OreNodeBounds nodeBounds;
private int hitIndex;
#endregion
protected virtual void Start()
{
if (nodeAudio == null) nodeAudio = GetComponent<MiningNodeAudio>();
if (nodeCollider == null) nodeCollider = GetComponent<Collider>();
if (childRenderers == null || childRenderers.Length == 0)
childRenderers = GetComponentsInChildren<Renderer>();
}
public virtual void Interact() => Interact(1);
public virtual void Interact(int hits)
{
if (ShouldCalculateNodeBounds())
nodeBounds = CalculateNodeBounds();
InflictHit(GetDropCount(hits));
if (hitIndex < hitsToDestroy)
{
StartCoroutine(Animate());
nodeAudio?.PlayImpactSound();
}
else
{
ReplaceNodeVisualsWithBrokenOne();
}
}
[Obsolete("Use Interact(hits) instead")]
public void oreHit() => Interact(1);
protected virtual int GetDropCount(int hits)
{
int total = dropOnHit * hits;
if (hitIndex + hits >= hitsToDestroy)
total += dropOnDestroy;
return total;
}
protected virtual bool ShouldCalculateNodeBounds()
{
return !cacheVisualBoundaries || hitIndex == 0;
}
protected virtual OreNodeBounds CalculateNodeBounds()
{
Renderer renderer = TryGetComponent(out MeshRenderer meshRenderer) ? meshRenderer : GetComponentInChildren<Renderer>();
if (renderer == null)
return new OreNodeBounds();
Bounds bounds = renderer.bounds;
return new OreNodeBounds(bounds.min.x, bounds.max.x, bounds.min.z, bounds.max.z, bounds.center.y);
}
protected virtual void InflictHit(int dropCount)
{
hitIndex++;
for (int i = 0; i < dropCount; i++)
{
Vector3 dropPos = CalculateRandomDropPosition(nodeBounds);
Instantiate(refinedPickup, dropPos, Quaternion.Euler(0, Random.Range(0, 360), 0));
}
}
protected virtual Vector3 CalculateRandomDropPosition(OreNodeBounds bounds)
{
return new Vector3(
Random.Range(bounds.minX, bounds.maxX),
bounds.centerY,
Random.Range(bounds.minZ, bounds.maxZ)
);
}
protected virtual void ReplaceNodeVisualsWithBrokenOne()
{
pieces.transform.localScale = transform.localScale;
Instantiate(pieces, transform.position, transform.rotation);
if (nodeCollider) nodeCollider.enabled = false;
foreach (var renderer in childRenderers) renderer.enabled = false;
nodeAudio?.PlayShatterSound();
if (enableRespawn)
ResetNode(respawnDelay);
else
StartCoroutine(DelayDestroy());
}
protected virtual IEnumerator Animate()
{
if (nodeCollider) nodeCollider.enabled = false;
Quaternion originalRotation = transform.localRotation;
Quaternion knockRotation = Quaternion.Euler(knockAngle);
float t = 0;
while (t < knockDuration)
{
float v = knockCurve.Evaluate(t / knockDuration);
transform.localRotation = originalRotation * Quaternion.Slerp(Quaternion.identity, knockRotation, v);
t += Time.deltaTime;
yield return null;
}
transform.localRotation = originalRotation;
if (nodeCollider) nodeCollider.enabled = true;
}
public virtual void ResetNode(float respawnDelay) => StartCoroutine(ResetAsync(respawnDelay));
public virtual IEnumerator ResetAsync(float respawnDelay)
{
yield return new WaitForSeconds(respawnDelay);
RevertToInitialState();
}
protected virtual void RevertToInitialState()
{
hitIndex = 0;
if (nodeCollider) nodeCollider.enabled = true;
foreach (var rend in childRenderers) rend.enabled = true;
}
private const float DelayDestroySeconds = 5f;
protected virtual IEnumerator DelayDestroy()
{
yield return new WaitForSeconds(DelayDestroySeconds);
Destroy(gameObject);
}
}
}