PopRiotBurst.cs
using Sandbox;
using System;
namespace PaintballBaddies;

/// <summary>Reuses the bank reward's native ring and particles; bounded independently on each peer.</summary>
public sealed class PopRiotBurst : Component
{
    private float age,radius;
    private Color tint;
    private LineRenderer ring;
    public static int SoundsPlayed {get;private set;}
    private static float nextPopSound;
    public static void Spawn(Scene scene,Vector3 position,float radius,Color tint,bool giant,bool chained)
    {
        if(!scene.IsValid())return;
        var existing=scene.GetAllComponents<PopRiotBurst>().OrderByDescending(x=>x.age).ToArray();
        foreach(var old in existing.Take(Math.Max(0,existing.Length-9)))old.GameObject.Destroy();
        var go=new GameObject(false,"Paint pop shower"){WorldPosition=position,NetworkMode=NetworkMode.Never};
        go.Tags.Add("paintball_debris");var effect=go.Components.Create<PopRiotBurst>();effect.radius=radius;effect.tint=tint;go.Enabled=true;
        if(RealTime.Now>=nextPopSound || giant)
        {
            nextPopSound=RealTime.Now+.055f;
            var cue=PopRiotAudio.Get(giant ? "bank" : "collect");
            var handle=Sound.Play(cue,scene.Camera?.WorldPosition ?? position);
            if(handle?.IsPlaying==true)SoundsPlayed++;
        }
        if(giant && scene.Camera.IsValid())scene.Camera.AddShake(.55f,22,.3f);
    }
    protected override void OnStart()
    {
        ring=ArenaGlow.Ring(GameObject,"Pop ripple",radius,2,tint*1.8f,32);
        // Orient the quick halo to the camera instead of flashing across the whole screen.
        ring.LocalRotation=Rotation.FromPitch(90);
        var particles=Components.Create<ParticleEffect>();particles.MaxParticles=radius>70 ? 80 : 22;particles.Lifetime=.85f;
        particles.LocalSpace=0;particles.ApplyAlpha=false;particles.ApplyColor=false;particles.ApplyRotation=false;particles.ApplyShape=false;
        particles.Force=true;particles.ForceDirection=Vector3.Down*170;particles.ForceScale=1;particles.ForceSpace=ParticleEffect.SimulationSpace.World;particles.Collision=false;
        var renderer=Components.Create<ParticleSpriteRenderer>();
        renderer.Sprite=new Sprite{Animations=[new Sprite.Animation{Name="Default",Frames=[new Sprite.Frame{Texture=PaintImpactSystem.SplashTexture}]}]};
        renderer.Scale=1;renderer.Additive=false;renderer.Lighting=false;renderer.Shadows=false;
        particles.OnStep=(p,dt)=>p.Alpha=((.85f-p.Age)/.4f).Clamp(0,1);
        int count=radius>70 ? 72 : 18;
        for(int i=0;i<count;i++)
        {
            var direction=new Vector3(MathF.Cos(i*2.399f),MathF.Sin(i*2.399f),.3f+(i%5)*.25f).Normal;
            var p=particles.Emit(WorldPosition+direction*radius*.7f,0);
            p.Size=new Vector3(radius>70 ? 13 : 7);p.Color=i%3==0 ? Color.Lerp(tint,Color.White,.4f) : tint;
            p.Velocity=direction*(radius>70 ? 240 : 125);
        }
    }
    protected override void OnUpdate()
    {
        age+=Time.Delta;if(age>=.9f){GameObject.Destroy();return;}
        ring.Enabled=age<.28f;
        ring.LocalScale=Vector3.One*(1+age*2);
        var color=(tint*1.6f).WithAlpha((1-age/.28f).Clamp(0,1));ring.Color=Gradient.FromColors(color,color);
    }
}