ShieldBreakEffect.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PaintballBaddies;

/// <summary>The curved panel becomes bounded native model particles, never gameplay colliders.</summary>
public sealed class ShieldBreakEffect : Component
{
    public const float Lifetime=3;
    public const int MaximumBursts=3;
    public Vector3 ImpactDirection {get;set;}
    public int Seed {get;set;}
    public float Age {get;private set;}
    public int Fragments => Components.GetAll<ParticleEffect>(FindMode.EverythingInSelfAndDescendants).Sum(x=>x.ParticleCount);
    public sealed class Piece {public string Model {get;set;} public float[] Center {get;set;}}
    public sealed class Layout {public List<Piece> Pieces {get;set;}}
    private static Layout layout;

    public static void Spawn(Scene scene,Transform transform,Vector3 direction,int seed)
    {
        foreach(var older in scene.GetAllComponents<ShieldBreakEffect>().OrderByDescending(x=>x.Age).SkipLast(MaximumBursts-1).ToArray())
            older.GameObject.Destroy();
        var go=new GameObject(false,"Shield shatter"){WorldTransform=transform,NetworkMode=NetworkMode.Never};
        go.Tags.Add("paintball_debris");
        var effect=go.Components.Create<ShieldBreakEffect>();effect.ImpactDirection=direction;effect.Seed=seed;go.Enabled=true;
    }
    protected override void OnStart()
    {
        layout ??=Json.Deserialize<Layout>(FileSystem.Mounted.ReadAllText("models/gear/clear_shield/fracture/layout.json"));
        var random=new Random(Seed);
        foreach(var piece in layout.Pieces)
        {
            float Rand(float a,float b)=>a+(b-a)*(float)random.NextDouble();
            var center=new Vector3(piece.Center[0],piece.Center[1],piece.Center[2]);
            var go=new GameObject(GameObject){Name="Curved shield fragment",NetworkMode=NetworkMode.Never};go.Tags.Add("paintball_debris");
            var particles=go.Components.Create<ParticleEffect>();
            particles.MaxParticles=1;particles.Lifetime=Lifetime;particles.LocalSpace=0;
            particles.ApplyRotation=false;particles.ApplyShape=false;particles.ApplyAlpha=false;particles.ApplyColor=false;
            particles.Force=true;particles.ForceDirection=Vector3.Down*500;particles.ForceScale=1;
            particles.ForceSpace=ParticleEffect.SimulationSpace.World;
            particles.Damping=.35f;particles.Collision=true;particles.CollisionRadius=.7f;
            particles.CollisionIgnore=new TagSet();particles.CollisionIgnore.Add("paintball_actor");particles.CollisionIgnore.Add("paintball_debris");
            particles.Bounce=.28f;particles.Friction=.6f;particles.PushStrength=0;
            var renderer=go.Components.Create<ParticleModelRenderer>();
            renderer.Choices=new(){new(){Model=Model.Load(piece.Model)}};renderer.Scale=1;renderer.CastShadows=true;renderer.RotateWithGameObject=false;
            var spin=new Angles(Rand(-190,190),Rand(-210,210),Rand(-170,170));
            var start=WorldRotation.Angles();
            particles.OnStep=(p,dt)=>{p.Angles=start+spin*p.Age;p.Alpha=((Lifetime-p.Age)/.55f).Clamp(0,1);};
            var particle=particles.Emit(WorldTransform.PointToWorld(center),0);
            particle.Angles=start;particle.StartAngles=start;particle.Size=Vector3.One;
            particle.Velocity=ImpactDirection.Normal*Rand(95,160)+WorldRotation*new Vector3(Rand(-35,35),center.y*3,center.z*1.5f)+Vector3.Up*Rand(45,100);
        }
    }
    protected override void OnUpdate(){Age+=Time.Delta;if(Age>Lifetime+.15f)GameObject.Destroy();}
}