// Assets/_Project/Scripts/VFX/SellEffectSpawner.cs using UnityEngine; using TD.Audio; namespace TD.VFX { /// /// Scene singleton that plays the "tower sold" feedback — a burst of gold coins plus a /// coin-rustle sound — at a world position. Mirrors : /// visual-only, plain MonoBehaviour, invoked on every peer via a ClientRpc so all /// players see and hear a sale locally. /// /// /// Who calls this: /// routes a ClientRpc here when a tower is sold. /// /// Inspector setup: drop this on a SellEffectSpawner GameObject in each Match /// scene. The VFX prefab is OPTIONAL — leave it empty to use the built-in /// placeholder, or assign an authored particle/VFX-graph prefab. /// Assign the coin-rustle clip on . /// 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 ------------------------------------------------- /// Plays the coin burst + rustle sound at . public void Play(Vector3 worldPos) { Vector3 spawnPos = worldPos + Vector3.up * verticalOffset; if (coinBurstPrefab != null) { Instantiate(coinBurstPrefab, spawnPos, Quaternion.identity); } else { // Zero-art fallback: a self-configuring, self-destroying particle burst. var go = new GameObject("CoinBurst"); go.transform.position = spawnPos; go.AddComponent(); } if (sellSound.clip != null) AudioManager.Instance?.Play(sellSound.clip, AudioCategory.UI, sellSound.RandomPitch(), sellSound.volume); } } }