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

/// <summary>Bounded native model particles for counter props without authored gibs.</summary>
public sealed class CounterDebris : Component
{
    public static int BreakBursts {get;private set;}
    public static int ChipBursts {get;private set;}
    private string kind;
    private int round;
    private bool broken;
    private float age;
    private const int MaximumBursts=8;
    public static void Spawn(Scene scene,Vector3 point,string kind,int round,bool broken)
    {
        foreach(var old in scene.GetAllComponents<CounterDebris>().OrderByDescending(x=>x.age).SkipLast(MaximumBursts-1).ToArray())old.GameObject.Destroy();
        var go=new GameObject(false,broken ? "Counter prop fragments" : "Counter impact chips"){NetworkMode=NetworkMode.Never,WorldPosition=point};
        go.Tags.Add("paintball_debris");var fx=go.Components.Create<CounterDebris>();fx.kind=kind;fx.round=round;fx.broken=broken;go.Enabled=true;
        if(broken)BreakBursts++;else ChipBursts++;
    }
    protected override void OnStart()
    {
        bool metal=kind=="counter_metal",glass=kind=="counter_glass",food=kind=="counter_food";
        var particles=Components.Create<ParticleEffect>();particles.MaxParticles=broken ? 10 : 2;particles.Lifetime=2.2f;particles.LocalSpace=0;
        particles.ApplyShape=particles.ApplyRotation=particles.ApplyColor=particles.ApplyAlpha=false;
        particles.Force=true;particles.ForceDirection=Vector3.Down*500;particles.ForceScale=1;particles.ForceSpace=ParticleEffect.SimulationSpace.World;
        particles.Collision=true;particles.CollisionRadius=.6f;particles.Bounce=.25f;particles.Friction=.6f;particles.PushStrength=0;
        particles.CollisionIgnore=new TagSet();particles.CollisionIgnore.Add("paintball_actor");particles.CollisionIgnore.Add("overdrive_enemy");particles.CollisionIgnore.Add("paintball_debris");
        var renderer=Components.Create<ParticleModelRenderer>();renderer.Scale=1;renderer.CastShadows=false;renderer.RotateWithGameObject=false;
        if(food)renderer.Choices=new(){new(){Model=Model.Load("models/restaurant/breakfast/potato_crumb.vmdl")},new(){Model=Model.Load("models/restaurant/breakfast/egg_piece.vmdl")}};
        else if(glass)
        {
            var layout=Json.Deserialize<ShieldBreakEffect.Layout>(FileSystem.Mounted.ReadAllText("models/gear/clear_shield/fracture/layout.json"));
            renderer.Choices=layout.Pieces.Take(6).Select(x=>new ParticleModelRenderer.ModelEntry{Model=Model.Load(x.Model)}).ToList();
        }
        else renderer.Choices=Enumerable.Range(1,4).Select(i=>new ParticleModelRenderer.ModelEntry
        {Model=Model.Load(metal ? $"models/citizen_props/oldoven/oldovengib{i:00}_lod01.vmdl" : $"models/citizen_props/coffeemug01/coffeemug01gib{i:00}_lod01.vmdl")}).ToList();
        particles.OnStep=(p,dt)=>{p.Angles+=new Angles(210,320,170)*dt;p.Alpha=((2.2f-p.Age)/.5f).Clamp(0,1);};
        for(int i=0;i<particles.MaxParticles;i++)
        {
            var direction=Rotation.FromYaw(i*137.5f).Forward;
            var p=particles.Emit(WorldPosition+direction*2+Vector3.Up*2,0);
            p.Size=new Vector3((food ? 1.4f : metal ? .13f : glass ? .13f : .65f)*(broken ? 1 : .4f));
            p.Color=glass ? new Color(.46f,.28f,.12f) : Color.White;
            p.Velocity=direction*Game.Random.Float(60,130)+Vector3.Up*Game.Random.Float(90,170);
        }
        if(food)KitchenCombatAudio.Play("egg",WorldPosition);
        else if(broken && !metal)SausageKitchenAudio.Play(true,WorldPosition);
        else Sound.Play(new SoundEvent{Sounds=[SoundFile.Load(metal ? $"sounds/physics/phys-impact-metal-hollow-{Game.Random.Int(1,3)}.vsnd" : $"sounds/impacts/bullets/glass_bullet_impact-{Game.Random.Int(1,3)}.vsnd")],Volume=broken ? .7f : .32f,Pitch=1.1f,Distance=850},WorldPosition);
    }
    protected override void OnUpdate()
    {
        age+=Time.Delta;
        if(age>2.4f || MultiplayerSession.Find(Scene)?.RoundId!=round)GameObject.Destroy();
    }
}