91 lines
4 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|