UnityTowerDefense/Assets/_Project/Scripts/VFX/SellEffectSpawner.cs

91 lines
4 KiB
C#

// Assets/_Project/Scripts/VFX/SellEffectSpawner.cs
using UnityEngine;
using TD.Audio;
namespace TD.VFX
{
/// <summary>
/// Scene singleton that plays the "tower sold" feedback — a burst of gold coins plus a
/// coin-rustle sound — at a world position. Mirrors <see cref="TD.UI.FloatingTextSpawner"/>:
/// visual-only, plain <c>MonoBehaviour</c>, invoked on every peer via a ClientRpc so all
/// players see and hear a sale locally.
/// </summary>
/// <remarks>
/// <b>Who calls this:</b> <see cref="TD.Gameplay.TowerPlacementManager.BroadcastSellEffect"/>
/// routes a ClientRpc here when a tower is sold.
///
/// <b>Inspector setup:</b> drop this on a <c>SellEffectSpawner</c> GameObject in each Match
/// scene. The VFX prefab is OPTIONAL — leave it empty to use the built-in
/// <see cref="CoinBurstVfx"/> placeholder, or assign an authored particle/VFX-graph prefab.
/// Assign the coin-rustle clip on <see cref="sellSound"/>.
/// </remarks>
public class SellEffectSpawner : MonoBehaviour
{
// ----- Singleton --------------------------------------------------
public static SellEffectSpawner Instance { get; private set; }
// ----- Inspector --------------------------------------------------
[Tooltip("Optional authored VFX prefab spawned at the sale position. Leave empty to " +
"fall back to the built-in code-generated CoinBurstVfx placeholder.")]
[SerializeField] private GameObject coinBurstPrefab;
[Tooltip("Coin-rustle sound played (2D, via AudioManager) when a tower is sold.")]
[SerializeField] private SoundConfig sellSound;
[Tooltip("Vertical offset above the sale position where the coin burst originates, so " +
"it reads as coming from the tower body rather than the ground.")]
[SerializeField] private float verticalOffset = 0.75f;
// ----- Lifecycle --------------------------------------------------
private void Awake()
{
if (Instance != null && Instance != this)
{
Debug.LogWarning("[SellEffectSpawner] Duplicate instance detected. " +
"Only one should exist per scene.");
return;
}
Instance = this;
}
private void OnDestroy()
{
if (Instance == this) Instance = null;
}
// ----- Public API -------------------------------------------------
/// <summary>Plays the coin burst + rustle sound at <paramref name="worldPos"/>.</summary>
public void Play(Vector3 worldPos)
{
Vector3 spawnPos = worldPos + Vector3.up * verticalOffset;
if (coinBurstPrefab != null)
{
var instance = Instantiate(coinBurstPrefab, spawnPos, Quaternion.identity);
// Force the burst to play. The spawner's whole job is to play this effect, so
// it shouldn't depend on the prefab's ParticleSystem having "Play On Awake"
// ticked — if that's off, the effect would silently never appear. Harmless when
// Play On Awake is already on (a freshly instantiated one-shot just restarts at
// t=0). VFX Graph effects play from their own settings, so a null PS is fine.
var ps = instance.GetComponentInChildren<ParticleSystem>();
if (ps != null) ps.Play(withChildren: true);
}
else
{
// Zero-art fallback: a self-configuring, self-destroying particle burst.
var go = new GameObject("CoinBurst");
go.transform.position = spawnPos;
go.AddComponent<CoinBurstVfx>();
}
if (sellSound.clip != null)
AudioManager.Instance?.Play(sellSound.clip, AudioCategory.UI,
sellSound.RandomPitch(), sellSound.volume);
}
}
}