using System; using UnityEngine; namespace HotUpdate.Gameplay { /// Authored bounded VFX/audio pools; consumes resolved facts without touching battle state. public sealed class EnemyCombatPresentation : MonoBehaviour { [Serializable] public sealed class EffectPool { public ParticleSystem[] instances; [NonSerialized] public int next; } [SerializeField] EffectPool[] _effects; [SerializeField] AudioClip[] _clips; [SerializeField] AudioSource[] _voices; [SerializeField, Range(0, 1)] float _mixBudget = .7f; [SerializeField, Range(0, 1)] float _voiceVolume = .4f; readonly float[] _lastSound = new float[8]; int _voice; bool _paused; float _speed = 1; public int PlayedEffects { get; private set; } public int PlayedSounds { get; private set; } public int EffectCount => _effects?.Length ?? 0; public AudioClip Clip(int index) => _clips[index]; public int VoiceCount => _voices?.Length ?? 0; void OnEnable() { PrototypeClientPrefs.AudioPreferencesChanged += ApplyAudio; ResetPlayback(); } void OnDisable() { PrototypeClientPrefs.AudioPreferencesChanged -= ApplyAudio; ResetPlayback(); } void ApplyAudio() { if (!PrototypeClientPrefs.Sfx) foreach (var v in _voices) v.Stop(); } public void ResetPlayback() { if (_effects != null) foreach (var pool in _effects) foreach (var ps in pool.instances) ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); if (_voices != null) foreach (var v in _voices) v.Stop(); for (int i = 0; i < _lastSound.Length; i++) _lastSound[i] = -100; _paused = false; _speed = 1; PlayedEffects = PlayedSounds = 0; } public void SetPlayback(bool paused, float speed) { if (_paused == paused && Mathf.Approximately(_speed, speed)) return; bool speedChanged = !Mathf.Approximately(_speed, speed); _speed = speed; foreach (var pool in _effects) foreach (var ps in pool.instances) { if (speedChanged) foreach (var child in ps.GetComponentsInChildren()) { var m = child.main; m.simulationSpeed = speed; } if (paused && !_paused) ps.Pause(true); else if (!paused && _paused && ps.IsAlive(true)) ps.Play(true); } if (paused != _paused) foreach (var v in _voices) { if (paused) v.Pause(); else v.UnPause(); } _paused = paused; } public void Play(PrototypeBattle.CombatCue cue,bool sound=true) { if (_paused || _effects == null) return; int index = cue.Kind == PrototypeBattle.CueKind.EnemyAttack ? Mathf.Clamp(cue.Type, 0, 4) : (cue.Type >= 2 ? 6 : 5); Burst(index, cue.Kind == PrototypeBattle.CueKind.EnemyAttack ? cue.AttackPosition : cue.Position, cue.Direction,sound); if (cue.Kind == PrototypeBattle.CueKind.EnemyAttack) Burst(cue.Shield ? 7 : 5, cue.Position, -cue.Direction,sound); } void Burst(int index, Vector3 position, Vector3 direction,bool sound) { var pool = _effects[index]; var ps = pool.instances[pool.next++ % pool.instances.Length]; ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); ps.transform.position = transform.TransformPoint(position); ps.transform.rotation = Quaternion.LookRotation(direction); foreach (var child in ps.GetComponentsInChildren()) { var m = child.main; m.simulationSpeed = _speed; } ps.Play(true); PlayedEffects++; // Mix budget and same-cue guard prevent a wave of hits becoming a wall of sound. if (!sound || !PrototypeClientPrefs.Sfx || Time.unscaledTime - _lastSound[index] < .075f) return; _lastSound[index] = Time.unscaledTime; var voice = _voices[_voice++ % _voices.Length]; voice.Stop(); voice.clip = _clips[index]; voice.Play(); PlayedSounds++; int active = 0; foreach (var v in _voices) if (v.isPlaying) active++; float gain = Mathf.Min(_voiceVolume, _mixBudget / Mathf.Max(1, active)); foreach (var v in _voices) if (v.isPlaying) v.volume = gain; } } }