using UnityEngine; namespace ProjectM.Client { /// /// Shared asset-free procedural-FX toolkit for the client presentation FEEDBACK systems (Combat / Structure / /// World / Ambient), which previously each carried a near-identical private copy. Pure UnityEngine (no ECS), so /// it stays a plain static helper. MakeBurst takes the owning FX-root transform + the per-use gravity / shape /// radius / size-tail that were the ONLY real differences between the old copies (Combat 0/0.06/0.2, /// Structure 0.3/0.18/0.15, World 0.25/0.10/0.2). MakeClip folds in the noise + envelope-decay knobs that were /// the only differences between the CombatFeedback MakeClip and the AmbientAudio MakeSting. /// public static class FeedbackFx { public static AudioClip MakeClip(string name, float f0, float f1, float dur, float vol, bool noise = false, float decay = 5f) { const int rate = 44100; int len = Mathf.Max(16, (int)(dur * rate)); var clip = AudioClip.Create(name, len, 1, rate, false); var data = new float[len]; var rng = new System.Random(name.Length * 9973 + 7); float phase = 0f; for (int i = 0; i < len; i++) { float t = i / (float)len; float env = Mathf.Exp(-decay * t); float freq = Mathf.Lerp(f0, f1, t); phase += 2f * Mathf.PI * freq / rate; float s = noise ? (float)(rng.NextDouble() * 2.0 - 1.0) : Mathf.Sin(phase); data[i] = s * env * vol; } clip.SetData(data, 0); return clip; } public static Material MakeParticleMaterial(string name = "FeedbackParticle") { // Sprites/Default is an always-included transparent vertex-coloured shader (reliable billboarded sparks; // HDR start colours still push past the bloom threshold); fall back through URP unlit / Unlit/Color. Shader sh = Shader.Find("Sprites/Default"); if (sh == null) sh = Shader.Find("Universal Render Pipeline/Particles/Unlit"); if (sh == null) sh = Shader.Find("Unlit/Color"); return new Material(sh) { name = name }; } public static ParticleSystem MakeBurst(Transform parent, string name, Material mat, Color color, float size, float speed, float life, int max, float gravity = 0f, float shapeRadius = 0.06f, float sizeTail = 0.2f) { var go = new GameObject(name); go.transform.SetParent(parent, false); var ps = go.AddComponent(); var main = ps.main; main.loop = false; main.playOnAwake = false; main.startLifetime = life; main.startSpeed = speed; main.startSize = size; main.startColor = color; main.maxParticles = max; main.gravityModifier = gravity; main.simulationSpace = ParticleSystemSimulationSpace.World; var emission = ps.emission; emission.enabled = false; // manual Emit(count) var shape = ps.shape; shape.enabled = true; shape.shapeType = ParticleSystemShapeType.Sphere; shape.radius = shapeRadius; var colOverLife = ps.colorOverLifetime; colOverLife.enabled = true; var grad = new Gradient(); grad.SetKeys( new[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(0f, 1f) }); colOverLife.color = new ParticleSystem.MinMaxGradient(grad); var sizeOverLife = ps.sizeOverLifetime; sizeOverLife.enabled = true; sizeOverLife.size = new ParticleSystem.MinMaxCurve(1f, AnimationCurve.Linear(0f, 1f, 1f, sizeTail)); var renderer = ps.GetComponent(); renderer.material = mat; renderer.renderMode = ParticleSystemRenderMode.Billboard; return ps; } public static void EmitTinted(ParticleSystem ps, Vector3 pos, int count, Color tint) { if (ps == null) return; var main = ps.main; main.startColor = tint; ps.transform.position = pos; ps.Emit(count); } public static void EmitAt(ParticleSystem ps, Vector3 pos, int count) { if (ps == null) return; ps.transform.position = pos; ps.Emit(count); } public static void PlayClip(AudioClip clip, Vector3 pos, float vol) { if (clip == null) return; AudioSource.PlayClipAtPoint(clip, pos, vol * GameVolume.Sfx); } } }