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();
}
}