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

/// <summary>Reuse the mounted wooden furniture fragments when chair art has no break data.</summary>
public sealed class RestaurantChairDebris : Component
{
    private Vector3 velocity;
    private float age;
    private int bounces,round;
    public static int Spawned {get;private set;}
    public static void Spawn(Scene scene,Vector3 origin,Rotation rotation,int round)=>Spawn(scene,origin,rotation,round,1);
    // A couple of small chips flying off a hit, without the break sound.
    public static void Splinters(Scene scene,Vector3 point,int count,int round)
    {
        for(int i=0;i<count;i++)
        {
            var model=Model.Load($"models/props/trim_sheets/table/coffeetable_a_gib_{i%2+1:00}.vmdl");if(model.IsError)continue;
            var direction=Rotation.FromYaw(Game.Random.Float(0,360)).Forward;
            var go=new GameObject(false,"Wood splinter"){NetworkMode=NetworkMode.Never,WorldPosition=point+Vector3.Up*2,WorldRotation=Rotation.FromYaw(Game.Random.Float(0,360)),WorldScale=new Vector3(.12f)};
            go.Tags.Add("paintball_debris");go.Components.Create<ModelRenderer>().Model=model;
            var debris=go.Components.Create<RestaurantChairDebris>();debris.round=round;debris.velocity=direction*Game.Random.Float(60,120)+Vector3.Up*Game.Random.Float(80,150);
            go.Enabled=true;
        }
    }
    public static void Spawn(Scene scene,Vector3 origin,Rotation rotation,int round,float size)
    {
        foreach(var old in scene.GetAllComponents<RestaurantChairDebris>().OrderByDescending(d=>d.age).SkipLast(29).ToArray())old.GameObject.Destroy();
        for(int i=0;i<6;i++)
        {
            var model=Model.Load($"models/props/trim_sheets/table/coffeetable_a_gib_{i%2+1:00}.vmdl");if(model.IsError)continue;
            float angle=i*MathF.PI/3;var direction=new Vector3(MathF.Cos(angle),MathF.Sin(angle),0);
            var go=new GameObject(false,"Broken wooden chair piece"){NetworkMode=NetworkMode.Never,WorldPosition=origin+direction*6+Vector3.Up*(14+i*3),WorldRotation=rotation*Rotation.FromYaw(i*61),WorldScale=new Vector3((i%2==0 ? .32f : .48f)*size)};
            go.Tags.Add("paintball_debris");go.Components.Create<ModelRenderer>().Model=model;
            var debris=go.Components.Create<RestaurantChairDebris>();debris.round=round;debris.velocity=direction*Game.Random.Float(85,150)+Vector3.Up*Game.Random.Float(90,170);
            go.Enabled=true;Spawned++;
        }
        Sound.Play(new SoundEvent{Sounds=[SoundFile.Load($"sounds/physics/phys-impact-wood-{Game.Random.Int(1,4)}.vsnd")],Volume=.8f,Pitch=1.1f,Distance=900},origin);
    }
    protected override void OnUpdate()
    {
        age+=Time.Delta;
        if(age>3.5f || MultiplayerSession.Find(Scene)?.RoundId!=round){GameObject.Destroy();return;}
        if(bounces>=3)return;
        float dt=MathF.Min(Time.Delta,.035f);velocity+=Vector3.Down*520*dt;
        var end=WorldPosition+velocity*dt;var h=Scene.Trace.Sphere(2,WorldPosition,end).WithoutTags("paintball_actor","overdrive_enemy","paintball_debris").Run();
        WorldPosition=h.Hit ? h.HitPosition+h.Normal*2 : end;
        if(h.Hit){velocity=(velocity-2*velocity.Dot(h.Normal)*h.Normal)*.27f;bounces++;}
        WorldRotation*=Rotation.From(new Angles(220,140,90)*dt);
    }
}