SausageGrenade.cs
using Sandbox;
using System;
namespace PaintballBaddies;
// A personal headshot reward: one visible, host-swept explosive projectile.
// The separate grenade action consumes it; clients cannot claim blast victims.
public sealed class SausageGrenade : Component
{
[Sync(SyncFlags.FromHost)] public Vector3 Velocity {get;set;}
[Sync(SyncFlags.FromHost)] public GameObject Shooter {get;set;}
[Sync(SyncFlags.FromHost)] public bool Freezing {get;set;}
[Sync(SyncFlags.FromHost)] public int Round {get;set;}
public const float BlastRadius=190;
public const float FuseSeconds=2;
private bool resting;
private float age;
private bool resolved;
public static int Launched {get;private set;}
public static int Exploded {get;private set;}
public static void Fire(PaintballCombatant actor,Vector3 origin,Vector3 aim,bool freeze=false)
{
if(!MultiplayerSession.Authority)return;
_=OverdriveAssets.LoadGrenade();
var go=new GameObject(false,"Headshot reward grenade"){WorldPosition=origin};go.Tags.Add("paintball_debris");
var grenade=go.Components.Create<SausageGrenade>();grenade.Shooter=actor.GameObject;grenade.Freezing=freeze;
grenade.Round=MultiplayerSession.Find(actor.Scene).RoundId;
grenade.Velocity=(aim-origin).Normal*850+Vector3.Up*70;
go.Enabled=true;Launched++;
if(MultiplayerSession.Online){go.NetworkMode=NetworkMode.Object;go.Network.SetOwnerTransfer(OwnerTransfer.Fixed);go.NetworkSpawn();}
SausageBolt.Cue(origin,"bow",grenade.Round,actor.GameObject);
}
protected override void OnStart()
{
var go=new GameObject(GameObject,true,"Grenade mesh"){NetworkMode=NetworkMode.Never};
var model=OverdriveAssets.GrenadeModel;
if(model is null || model.IsError)model=Model.Load("models/dev/sphere.vmdl");
float scale=7/MathF.Max(.01f,model.Bounds.Size.Length);
go.LocalScale=new Vector3(scale);go.LocalPosition=-model.Bounds.Center*scale;
var r=go.Components.Create<ModelRenderer>();r.Model=model;r.Tint=Freezing ? new Color(.35f,.85f,1) : Color.White;
}
protected override void OnFixedUpdate()
{
if(!MultiplayerSession.Authority || resolved)return;
var s=MultiplayerSession.Find(Scene);
if(s is not {IsOverdrive:true,InRound:true} || s.RoundId!=Round){GameObject.Destroy();return;}
age+=Time.Delta;
if(age>=FuseSeconds){Detonate();return;}
if(resting)return;
Velocity+=Vector3.Down*320*Time.Delta;
var end=WorldPosition+Velocity*Time.Delta;
var ray=Scene.Trace.Sphere(3,WorldPosition,end).WithSurfaceMeshes().WithoutTags("paintball_actor","paintball_debris");
foreach(var shield in Scene.GetAllComponents<ArenaShield>())ray=ray.IgnoreGameObjectHierarchy(shield.GameObject);
var hit=ray.Run();
WorldRotation*=Rotation.From(new Angles(180,110,70)*Time.Delta);
if(hit.Hit)
{
WorldPosition=hit.EndPosition+hit.Normal*.5f;
Velocity=(Velocity-2*Vector3.Dot(Velocity,hit.Normal)*hit.Normal)*.42f;
if(hit.Normal.z>.65f && Velocity.Length<75){Velocity=Vector3.Zero;resting=true;}
}
else WorldPosition=end;
}
internal void Detonate()
{
if(!MultiplayerSession.Authority || resolved)return;
resolved=true;Exploded++;
var actor=Shooter.IsValid() ? Shooter.Components.Get<PaintballCombatant>() : null;
var s=MultiplayerSession.Find(Scene);int kills=0;
if(s is not {IsOverdrive:true,InRound:true} || s.RoundId!=Round){GameObject.Destroy();return;}
foreach(var enemy in s.OverdriveEnemies.Where(e=>(e.AimPoint-WorldPosition).Length<=BlastRadius).ToArray())
{
var wall=Scene.Trace.Ray(WorldPosition,enemy.AimPoint).WithSurfaceMeshes()
.WithoutTags("paintball_actor","paintball_debris","overdrive_enemy","pop_target").Run();
if(!wall.Hit && (Freezing ? enemy.FlashFreeze() : enemy.GrenadeHit((enemy.AimPoint-WorldPosition).Normal,actor)))kills++;
}
Burst(WorldPosition,Round,Freezing);
if(actor.IsValid() && kills>0)HitConfirm.Send(actor.GameObject,true,WorldPosition,Round);
GameObject.Destroy();
}
[Rpc.Broadcast(NetFlags.HostOnly)] private static async void Burst(Vector3 point,int round,bool freezing)
{
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;
if(freezing){CryoBurst.Spawn(scene,point,false);return;}
SausageExplosionEffect.Spawn(scene,point,true);
KitchenCombatAudio.Play("concrete",point);OverdriveAudio.Play("burst",point);
}
}