using ProjectM.Simulation; using Unity.Entities; using Unity.NetCode; using UnityEngine; namespace ProjectM.Client { /// /// Client-only AMBIENT audio + cycle-phase stingers. A managed presentation /// (, main thread, no Burst) that OBSERVES the replicated /// and never touches the simulation. On start it plays a low, seamless-looping /// procedural drone (asset-free, AudioClip.Create like CombatFeedbackSystem.MakeClip); each /// time the cycle phase changes it plays a short procedural stinger and eases the drone's intensity by phase /// (calmer at base, tenser during Defend / "wave incoming"). Lives only in the client world, so the server /// never creates audio and nothing here affects determinism. Volumes are deliberately conservative + tunable. /// [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] [UpdateInGroup(typeof(PresentationSystemGroup))] public partial class AmbientAudioSystem : SystemBase { AudioSource _ambient; AudioClip _ambientClip; AudioClip _stingExpedition; AudioClip _stingDefend; AudioClip _stingBuild; GameObject _root; byte _lastPhase; bool _phaseInit; const float AmbientBaseVolume = 0.10f; // low bed; Defend eases up to ~1.7x protected override void OnCreate() { _ambientClip = MakeDrone(); _stingExpedition = MakeSting(520f, 880f, 0.45f, 0.30f); // airy rising "deploy" _stingDefend = MakeSting(300f, 140f, 0.55f, 0.42f); // tense falling "wave incoming" _stingBuild = MakeSting(440f, 660f, 0.40f, 0.26f); // soft confirm } protected override void OnStartRunning() { if (_root != null) return; _root = new GameObject("~AmbientAudio"); _ambient = _root.AddComponent(); _ambient.clip = _ambientClip; _ambient.loop = true; _ambient.playOnAwake = false; _ambient.spatialBlend = 0f; // 2D bed _ambient.volume = AmbientBaseVolume; _ambient.Play(); } protected override void OnDestroy() { if (_root != null) Object.Destroy(_root); } protected override void OnUpdate() { if (_ambient == null) return; if (!SystemAPI.TryGetSingleton(out var cyc)) return; byte phase = cyc.Phase; if (!_phaseInit) { _lastPhase = phase; // adopt the current phase silently (no stinger on first observe) _phaseInit = true; } else if (phase != _lastPhase) { PlaySting(phase); _lastPhase = phase; } // Ease the drone intensity toward the phase target (tenser during Defend). float target = phase == CyclePhase.Defend ? AmbientBaseVolume * 1.7f : AmbientBaseVolume; _ambient.volume = Mathf.MoveTowards(_ambient.volume, target, SystemAPI.Time.DeltaTime * 0.25f); } void PlaySting(byte phase) { AudioClip clip = phase == CyclePhase.Defend ? _stingDefend : phase == CyclePhase.Build ? _stingBuild : _stingExpedition; if (clip != null && _ambient != null) _ambient.PlayOneShot(clip, 0.6f); } // ---- Procedural audio (asset-free; mirrors CombatFeedbackSystem.MakeClip) ---- // A low, seamless-looping pad: each partial completes an integer number of cycles over the buffer // (freq snapped to k/duration) so the loop point has no click. A slow tremolo adds motion. static AudioClip MakeDrone() { const int rate = 44100; const float dur = 4f; int len = (int)(dur * rate); var clip = AudioClip.Create("ambient_drone", len, 1, rate, false); var data = new float[len]; float f0 = Snap(55f, dur); // sub float f1 = Snap(110f, dur); // root float f2 = Snap(164.81f, dur); // fifth-ish float f3 = Snap(220f, dur); float trem = Snap(0.5f, dur); // slow amplitude wobble for (int i = 0; i < len; i++) { float t = i / (float)rate; float s = 0.50f * Mathf.Sin(2f * Mathf.PI * f0 * t) + 0.35f * Mathf.Sin(2f * Mathf.PI * f1 * t) + 0.18f * Mathf.Sin(2f * Mathf.PI * f2 * t) + 0.10f * Mathf.Sin(2f * Mathf.PI * f3 * t); float amp = 0.75f + 0.25f * Mathf.Sin(2f * Mathf.PI * trem * t); data[i] = s * amp * 0.5f; // peak ~0.57, no clipping } clip.SetData(data, 0); return clip; } // freq snapped so freq*dur is an integer -> the waveform closes seamlessly at the loop point. static float Snap(float freq, float dur) { float cycles = Mathf.Max(1f, Mathf.Round(freq * dur)); return cycles / dur; } // Short one-shot tone sweeping f0->f1 with an exponential decay envelope. static AudioClip MakeSting(float f0, float f1, float dur, float vol) { const int rate = 44100; int len = Mathf.Max(16, (int)(dur * rate)); var clip = AudioClip.Create("sting", len, 1, rate, false); var data = new float[len]; float phase = 0f; for (int i = 0; i < len; i++) { float t = i / (float)len; float env = Mathf.Exp(-3.5f * t); float freq = Mathf.Lerp(f0, f1, t); phase += 2f * Mathf.PI * freq / rate; data[i] = Mathf.Sin(phase) * env * vol; } clip.SetData(data, 0); return clip; } } }