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

// Shooter-only hit and kill confirmation for Sausage Survival. The host has
// already decided damage; this only tells the local player that it landed.
public static class HitConfirm
{
    public const string SoundPath="sounds/paintball/hit_confirm.sound";
    public static int Hits {get;private set;}
    public static int Kills {get;private set;}
    [Rpc.Broadcast(NetFlags.HostOnly)]
    public static async void Send(GameObject shooter,bool kill,Vector3 point,int round)
    {
        var scene=Game.ActiveScene;await GameTask.MainThread();
        if(!scene.IsValid() || scene!=Game.ActiveScene || MultiplayerSession.Find(scene) is not {IsOverdrive:true} s || s.RoundId!=round)return;
        Local(scene,shooter,kill,point);
    }
    // A one-off callout for the player who caused a moment (e.g. a smashed stack).
    [Rpc.Broadcast(NetFlags.HostOnly)]
    public static async void Callout(GameObject shooter,string text,int round)
    {
        var scene=Game.ActiveScene;await GameTask.MainThread();
        if(!scene.IsValid() || scene!=Game.ActiveScene || MultiplayerSession.Find(scene) is not {IsOverdrive:true} s || s.RoundId!=round)return;
        var local=MultiplayerSession.LocalPlayer(scene);
        if(shooter.IsValid() && local.IsValid() && local.GameObject==shooter)KillStreak.Say(text);
    }
    internal static void Local(Scene scene,GameObject shooter,bool kill,Vector3 point,int killCount=1)
    {
        var local=MultiplayerSession.LocalPlayer(scene);
        if(!shooter.IsValid() || !local.IsValid() || local.GameObject!=shooter)return;
        local.Components.Get<PaintballMarker>()?.ConfirmOverdriveHit(kill);
        Hits++;if(kill){Kills++;KillStreak.AddKills(Math.Max(1,killCount));}
        // Played at the listener so distance never swallows the confirmation.
        var at=scene.Camera.IsValid() ? scene.Camera.WorldPosition : point;
        var tick=Sound.Play(SoundPath,at);
        if(tick is not null){tick.Volume=kill ? .75f : .42f;tick.Pitch=kill ? .82f : Game.Random.Float(1.08f,1.16f);}
        if(kill && scene.Camera.IsValid())scene.Camera.AddShake(.3f,22,.1f);
    }
}

// Short, unshadowed light burst at a muzzle. Purely local presentation.
public sealed class ShotFlash : Component
{
    private PointLight light;
    private Color color;
    private float age,life;
    public static int Spawned {get;private set;}
    public static void Spawn(Scene scene,Vector3 at,Color color,float radius,float life)
    {
        foreach(var old in scene.GetAllComponents<ShotFlash>().OrderByDescending(f=>f.age).SkipLast(7).ToArray())old.GameObject.Destroy();
        var go=new GameObject(true,"Muzzle flash light"){NetworkMode=NetworkMode.Never};
        go.WorldPosition=at;go.Tags.Add("paintball_debris");
        var flash=go.Components.Create<ShotFlash>();flash.color=color;flash.life=life;
        flash.light=go.Components.Create<PointLight>();flash.light.Radius=radius;flash.light.Shadows=false;flash.light.LightColor=color;
        Spawned++;
    }
    protected override void OnUpdate()
    {
        age+=Time.Delta;
        float t=MathF.Max(0,1-age/life);
        if(light.IsValid())light.LightColor=color*(t*t);
        if(age>=life)GameObject.Destroy();
    }
}