FunFeatures.cs
using Sandbox;
using System;
namespace PaintballBaddies;
/// <summary>Local-player callouts: multi-kills inside a short window, and
/// one-off moments such as smashing a sausage's plate stack.</summary>
public static class KillStreak
{
const float Window=3f,Show=1.7f;
private static int count;
private static float last=-100,shownAt=-100;
public static string Callout {get;private set;}="";
public static int Serial {get;private set;}
public static bool Visible=>Now-shownAt<Show;
private static float Now=>(float)RealTime.Now;
public static void AddKills(int kills)
{
if(kills<=0)return;
count=Now-last<Window ? count+kills : kills;last=Now;
string text=count switch {2=>"DOUBLE KILL!",3=>"TRIPLE KILL!",4=>"SAUSAGE PARTY!",>=5=>"FULL ENGLISH RAMPAGE!",_=>""};
if(text!="")Say(text);
}
public static void Say(string text){Callout=text;shownAt=Now;Serial++;}
}
/// <summary>Single player only: the last sausage of an encounter dies in slow
/// motion for a moment, then time eases back to normal.</summary>
public sealed class FinalKillSlowMo : Component
{
const float Hold=.75f,Ease=.35f,Scale=.3f;
private float started;
public static int Plays {get;private set;}
public static void Start(Scene scene)
{
if(MultiplayerSession.Online || scene.GetAllComponents<FinalKillSlowMo>().Any())return;
new GameObject(true,"Final kill slow motion"){NetworkMode=NetworkMode.Never}.Components.Create<FinalKillSlowMo>();
Plays++;KillStreak.Say("LAST SAUSAGE!");
}
protected override void OnStart(){started=(float)RealTime.Now;Scene.TimeScale=Scale;}
protected override void OnUpdate()
{
float t=(float)RealTime.Now-started;
if(t>=Hold+Ease){Scene.TimeScale=1;GameObject.Destroy();return;}
Scene.TimeScale=t<Hold ? Scale : Scale+(1-Scale)*((t-Hold)/Ease);
}
protected override void OnDestroy(){if(Scene.IsValid())Scene.TimeScale=1;}
}
/// <summary>Steam venting off an enraged sausage's head: soft, see-through
/// wisps that rise, spread and fade (camera-facing sprites, not solid balls).</summary>
public sealed class RagePuff : Component
{
private static Texture cloud;
private float age;
const float Life=.9f;
// Soft round cloud, generated once: bright core, feathered, slightly lumpy edge.
internal static Texture Cloud()
{
if(cloud is {IsValid:true})return cloud;
const int N=96;using var bmp=new Bitmap(N,N);var px=new Color[N*N];
for(int y=0;y<N;y++)for(int x=0;x<N;x++)
{
float dx=(x-N/2f)/(N/2f),dy=(y-N/2f)/(N/2f);float r=MathF.Sqrt(dx*dx+dy*dy);
float a=MathF.Atan2(dy,dx);float wobble=.08f*MathF.Sin(a*5+1.3f)+.05f*MathF.Sin(a*9+.4f);
float t=Math.Clamp(1-(r/(.95f+wobble)),0,1);
px[y*N+x]=new Color(1,1,1,t*t*(3-2*t)*.85f);
}
bmp.SetPixels(px);cloud=bmp.ToTexture();return cloud;
}
public static void Spawn(Scene scene,Vector3 at,Vector3 side)
{
var go=new GameObject(true,"Rage steam"){NetworkMode=NetworkMode.Never,WorldPosition=at};go.Tags.Add("paintball_debris");
go.Components.Create<RagePuff>();
var p=go.Components.Create<ParticleEffect>();p.MaxParticles=2;p.Lifetime=Life;p.LocalSpace=0;
p.ApplyAlpha=false;p.ApplyColor=false;p.ApplyRotation=false;p.ApplyShape=false;p.Collision=false;
p.Force=true;p.ForceDirection=Vector3.Up*30;p.ForceScale=1;p.ForceSpace=ParticleEffect.SimulationSpace.World;p.Damping=1.5f;
var r=go.Components.Create<ParticleSpriteRenderer>();
r.Sprite=new Sprite{Animations=[new Sprite.Animation{Name="Default",Frames=[new Sprite.Frame{Texture=Cloud()}]}]};
r.Scale=1;r.Additive=false;r.Lighting=true;r.Shadows=false;
// Grow as they rise, fade out; puffs are small near the head and swell.
p.OnStep=(part,dt)=>{float t=(part.Age/Life).Clamp(0,1);part.Size=new Vector3(2.5f+t*7);part.Alpha=MathF.Sin(t*MathF.PI)*.23f;};
for(int i=0;i<2;i++)
{
var part=p.Emit(at+side*i*1.5f,0);part.Size=new Vector3(3);part.Alpha=0;
part.Color=new Color(.95f,.95f,.97f);
part.Velocity=side*Game.Random.Float(14,26)+Vector3.Up*Game.Random.Float(28,44);
}
}
protected override void OnUpdate(){age+=Time.Delta;if(age>Life+.1f)GameObject.Destroy();}
}