CryoBurst.cs
using Sandbox;
using System;
using System.Threading.Tasks;
namespace PaintballBaddies;

// Reuses native particles, the existing cut sausage meshes and library frost.
// Cosmetic pieces never apply damage or become networked physics objects.
public sealed class CryoBurst : Component
{
    public static Material Frost {get;private set;}
    private static Task loading;
    public static Task Load()=>loading ??= LoadFrost();
    private static async Task LoadFrost()
    {
        try{Frost=await Cloud.Load<Material>("tiled.frozen_ice");}
        catch(Exception e){Log.Warning($"Frost material unavailable: {e.Message}");}
        if(Frost is null || Frost.IsError)Frost=Material.Load("materials/glass/glass_a.vmat");
        Frost=Frost.CreateCopy();Frost.Set("g_vTexCoordScale",new Vector4(4,4,0,0));Frost.Set("g_vColorTint",new Vector4(1.1f,1.25f,1.4f,1));

    }
    private float age;
    private bool shatter,small;
    public static int Shatters {get;private set;}
    public static void Spawn(Scene scene,Vector3 at,bool shatter,bool small=false)
    {
        if(!scene.IsValid() || scene!=Game.ActiveScene)return;
        _=Load();
        foreach(var old in scene.GetAllComponents<CryoBurst>().OrderByDescending(x=>x.age).SkipLast(7).ToArray())old.GameObject.Destroy();
        var go=new GameObject(false,shatter ? "Ice shatter" : "Flash freeze mist"){NetworkMode=NetworkMode.Never,WorldPosition=at};
        go.Tags.Add("paintball_debris");var c=go.Components.Create<CryoBurst>();c.shatter=shatter;c.small=small;go.Enabled=true;
        if(shatter)
        {
            Shatters++;Sound.Play("sounds/paintball/shield_break.sound",at);
            if(scene.Camera.IsValid())scene.Camera.AddShake((1-(scene.Camera.WorldPosition-at).Length/650).Clamp(0,1)*.55f,24,.12f);
        }
        else if(!small)FeelAudio.Play("freeze_blast",1,at,.8f,950);
    }
    protected override void OnStart()
    {
        var mist=Components.Create<ParticleEffect>();mist.MaxParticles=small ? 9 : 22;mist.Lifetime=.65f;mist.LocalSpace=0;
        mist.ApplyColor=false;mist.ApplyAlpha=false;mist.ApplyShape=false;mist.ApplyRotation=false;
        var sprite=Components.Create<ParticleSpriteRenderer>();sprite.Sprite=new Sprite{Animations=[new Sprite.Animation{Name="Default",Frames=[new Sprite.Frame{Texture=RagePuff.Cloud()}]}]};
        sprite.Lighting=true;sprite.Shadows=false;sprite.Additive=false;sprite.Scale=1;
        mist.OnStep=(p,dt)=>{p.Alpha=(1-p.Age/.65f).Clamp(0,1)*.2f;p.Size+=new Vector3(dt*25);};
        for(int i=0;i<mist.MaxParticles;i++)
        {
            var dir=Rotation.From(new Angles(Game.Random.Float(-70,70),i*137.5f,0)).Forward;
            var p=mist.Emit(WorldPosition+dir*(small ? 9 : 18),0);p.Color=new Color(.6f,.86f,1);p.Size=new Vector3(small ? 11 : 20);p.Alpha=.2f;
            p.Velocity=dir*(small ? 35 : 145)+Vector3.Up*15;
        }
        if(!shatter)return;
        // Scatter pieces through the whole frozen silhouette, not just its head.
        for(int i=0;i<20;i++)CryoShard.Spawn(GameObject,WorldPosition+new Vector3(Game.Random.Float(-10,10),Game.Random.Float(-10,10),Game.Random.Float(-30,31)),i);
    }
    protected override void OnUpdate(){age+=Time.Delta;if(age>2.8f)GameObject.Destroy();}
}

public sealed class CryoShard : Component
{
    private Vector3 velocity;
    private Angles spin;
    private float age;
    private ModelRenderer render;
    public static void Spawn(GameObject parent,Vector3 point,int index)
    {
        var go=new GameObject(parent,true,"Frosted fractured chunk"){NetworkMode=NetworkMode.Never,WorldPosition=point};
        go.Tags.Add("paintball_debris");
        var c=go.Components.Create<CryoShard>();c.render=go.Components.Create<ModelRenderer>();
        c.render.Model=Model.Load($"models/sausage/grumpy/head_piece_0_{index%3}.vmdl");c.render.MaterialOverride=CryoBurst.Frost;
        c.render.Tint=new Color(.5f,.82f,1);go.WorldScale=new Vector3(Game.Random.Float(.3f,.8f));
        c.velocity=Rotation.FromYaw(index*137.5f).Forward*Game.Random.Float(90,260)+Vector3.Up*Game.Random.Float(70,220);
        c.spin=new Angles(Game.Random.Float(-220,220),Game.Random.Float(-220,220),Game.Random.Float(-220,220));
    }
    protected override void OnUpdate()
    {
        age+=Time.Delta;velocity+=Vector3.Down*430*Time.Delta;
        var end=WorldPosition+velocity*Time.Delta;
        var hit=Scene.Trace.Sphere(.7f,WorldPosition,end).WithoutTags("paintball_actor","paintball_debris","overdrive_enemy").Run();
        if(hit.Hit){WorldPosition=hit.EndPosition+hit.Normal;velocity=(velocity-2*Vector3.Dot(velocity,hit.Normal)*hit.Normal)*.28f;}
        else WorldPosition=end;
        WorldRotation*=Rotation.From(spin*Time.Delta);
        render.Tint=new Color(.5f,.82f,1,((2.7f-age)/.5f).Clamp(0,1));
    }
}