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); } // ---- ground DECAL primitives (Bundle 2 — explosion scorch, cover cracks, room scars) ---- // Transparent unlit ground-decal material: Sprites/Default honours vertex colour × the per-renderer _Color // MPB, blends alpha, is double-sided (Cull Off) and writes no depth (ZWrite Off) — so flat, overlapping // ground decals never z-fight and never cast/receive shadows. Same shader family as the fuse disc. public static Material MakeDecalMaterial(string name = "GroundDecal") { Shader sh = Shader.Find("Sprites/Default"); if (sh == null) sh = Shader.Find("Universal Render Pipeline/Particles/Unlit"); if (sh == null) sh = Shader.Find("Unlit/Transparent"); return new Material(sh) { name = name, renderQueue = 3000 }; // Transparent } // Irregular soft radial disc on the XZ plane (already flat — scale x/z, spin about Y): vertex alpha 1 at the // centre fading to 0 at a JITTERED rim, so it reads as an organic scorch blob with NO hard rectangle edge // (the feathered rim is what keeps it from ghosting over fog the way an opaque quad did). White vertex colour // so the caller's material/MPB tint (dark char) shows through. public static Mesh BuildScorchMesh(int segments = 24, float jitter = 0.3f, int seed = 1) { if (segments < 6) segments = 6; var rng = new System.Random(seed * 6151 + 13); var m = new Mesh { name = "ScorchDecal" }; var v = new Vector3[segments + 1]; var col = new Color[segments + 1]; var tris = new int[segments * 3]; v[0] = Vector3.zero; col[0] = new Color(1f, 1f, 1f, 1f); for (int i = 0; i < segments; i++) { float a = i / (float)segments * Mathf.PI * 2f; float r = 1f - jitter * (float)rng.NextDouble(); v[i + 1] = new Vector3(Mathf.Cos(a) * r, 0f, Mathf.Sin(a) * r); col[i + 1] = new Color(1f, 1f, 1f, 0f); // transparent feathered rim int n = (i + 1) % segments; tris[i * 3] = 0; tris[i * 3 + 1] = n + 1; tris[i * 3 + 2] = i + 1; } m.vertices = v; m.colors = col; m.triangles = tris; m.RecalculateBounds(); return m; } // A jagged crack star on the XZ plane: `spokes` thin tapered triangles radiating from the centre at random // angles/lengths, opaque at the base and feathered to nothing at the tip. Reads as surface cracking, visually // distinct from the scorch blob. White vertex colour; caller tints dark and scales it to the piece. public static Mesh BuildCrackMesh(int spokes = 3, int seed = 1) { if (spokes < 1) spokes = 1; var rng = new System.Random(seed * 9277 + 5); var m = new Mesh { name = "CrackDecal" }; var v = new Vector3[spokes * 3]; var col = new Color[spokes * 3]; var tris = new int[spokes * 3]; for (int s = 0; s < spokes; s++) { float a = (s / (float)spokes) * Mathf.PI * 2f + (float)(rng.NextDouble() - 0.5) * 1.2f; float len = 0.6f + 0.4f * (float)rng.NextDouble(); float w = 0.06f + 0.05f * (float)rng.NextDouble(); var dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)); var perp = new Vector3(-dir.z, 0f, dir.x) * w; int b = s * 3; v[b] = perp; v[b + 1] = -perp; v[b + 2] = dir * len; col[b] = new Color(1f, 1f, 1f, 1f); col[b + 1] = new Color(1f, 1f, 1f, 1f); col[b + 2] = new Color(1f, 1f, 1f, 0f); // feathered tip tris[b] = b; tris[b + 1] = b + 2; tris[b + 2] = b + 1; } m.vertices = v; m.colors = col; m.triangles = tris; m.RecalculateBounds(); return m; } // Unit-radius upward-facing disc fan on the XZ plane (centre vertex + rim) for ground telegraph / blast-radius // rings — scale x/z to the radius. No vertex colours (white default) so the material or a per-renderer _Color // MPB fully tints + fades it. Promoted from WorldFeedbackSystem so the barrel fuse ring + the geyser telegraph // share ONE primitive (review M6). public static Mesh BuildDisc(int segments) { if (segments < 6) segments = 6; var m = new Mesh { name = "Disc" }; var v = new Vector3[segments + 1]; var tris = new int[segments * 3]; v[0] = Vector3.zero; for (int i = 0; i < segments; i++) { float a = i / (float)segments * Mathf.PI * 2f; v[i + 1] = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)); int n = (i + 1) % segments; tris[i * 3] = 0; tris[i * 3 + 1] = n + 1; tris[i * 3 + 2] = i + 1; } m.vertices = v; m.triangles = tris; m.RecalculateBounds(); return m; } // Unit-radius thin RING (annulus) on the XZ plane — the always-on rim of a zone telegraph (rim = the TRUE // damage radius, guidelines G2) while a separate fill disc grows inside it. Scale x/z to the radius; inner // edge fixed at innerFrac of the outer. No vertex colours (white) so an MPB _Color fully tints + fades it. public static Mesh BuildRing(int segments, float innerFrac = 0.92f) { if (segments < 6) segments = 6; innerFrac = Mathf.Clamp(innerFrac, 0.05f, 0.98f); var m = new Mesh { name = "Ring" }; var v = new Vector3[segments * 2]; var tris = new int[segments * 6]; for (int i = 0; i < segments; i++) { float a = i / (float)segments * Mathf.PI * 2f; var dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a)); v[i * 2] = dir * innerFrac; v[i * 2 + 1] = dir; int n = (i + 1) % segments; int b = i * 6; tris[b] = i * 2; tris[b + 1] = n * 2 + 1; tris[b + 2] = i * 2 + 1; tris[b + 3] = i * 2; tris[b + 4] = n * 2; tris[b + 5] = n * 2 + 1; } m.vertices = v; m.triangles = tris; m.RecalculateBounds(); return m; } } }