OverdriveShotEffects.cs
using Sandbox;
using System;
namespace PaintballBaddies;
public sealed class OverdriveShotEffects : Component
{
private float age;
private bool started;
private Vector3 start,end;
private LineRenderer line;
public static int Tracers {get;private set;}
public static int Impacts {get;private set;}
public static int RenderedFrames {get;private set;}
public static void Tracer(Scene scene,Vector3 origin,Vector3 target)
{
foreach(var old in scene.GetAllComponents<OverdriveShotEffects>().OrderByDescending(e=>e.age).SkipLast(35).ToArray())old.GameObject.Destroy();
var go=new GameObject(false,"Shotgun pellet streak"){NetworkMode=NetworkMode.Never};go.Tags.Add("paintball_debris");
var effect=go.Components.Create<OverdriveShotEffects>();effect.start=origin;effect.end=target;
effect.line=go.Components.Create<LineRenderer>();effect.line.UseVectorPoints=true;effect.line.VectorPoints=[origin,origin+(target-origin).Normal*8];
effect.line.Width=new Curve(new Curve.Frame(0,.08f),new Curve.Frame(1,.32f));
effect.line.Face=SceneLineObject.FaceMode.Camera;effect.line.CastShadows=false;effect.line.Lighting=false;
effect.line.Opaque=false;effect.line.Additive=false;effect.line.Color=Gradient.FromColors(new Color(.85f,.8f,.7f,0),new Color(.92f,.9f,.82f,.55f));
go.Enabled=true;Tracers++;
}
public static void Impact(Scene scene,Vector3 point,Vector3 normal,int variation)
{
// Read the actual receiving surface locally; hit points come from the host.
var trace=scene.Trace.Ray(point+normal*8,point-normal*20).WithSurfaceMeshes().WithoutTags("paintball_actor","paintball_debris","overdrive_enemy","pop_target");
var hit=trace.Run();
if(!hit.Hit)return;
// Convex colliders can sit outside curved barrels and imported props.
// Prefer their actual rendered face so the mark touches the visible mesh.
if(Game.IsEditor)
{
var rendered=trace.UsePhysicsWorld(false).UseRenderMeshes(true).Run();
if(rendered.Hit)hit=rendered;
}
var surface=hit.Surface?.ResourcePath?.ToLowerInvariant() ?? "";
var kind=surface.Contains("metal") ? "metal_bullet" : surface.Contains("plastic") ? "plastic_bullet" : "default-bullet";
var mark=GameObject.Clone($"prefabs/surface/{kind}-decal.prefab",new Transform(hit.HitPosition+hit.Normal*.15f,Rotation.LookAt(-hit.Normal)),startEnabled:false);
mark.NetworkMode=NetworkMode.Never;mark.Name="Shotgun surface impact";mark.Tags.Add("paintball_debris");
if(hit.GameObject.IsValid())mark.Parent=hit.GameObject;
mark.WorldScale=Vector3.One;
mark.WorldPosition=hit.HitPosition+hit.Normal*.15f;
mark.WorldRotation=Rotation.LookAt(-hit.Normal);
foreach(var decal in mark.Components.GetAll<Decal>(FindMode.EverythingInSelfAndDescendants))
{decal.Transient=false;decal.LifeTime=0;decal.Scale=1;decal.Depth=8;decal.AttenuationAngle=.35f;decal.SortLayer=12;decal.Rotation=variation*137.508f;}
foreach(var cleanup in mark.Components.GetAll<TemporaryEffect>(FindMode.EverythingInSelfAndDescendants).ToArray())cleanup.Destroy();
mark.Components.Create<OverdriveImpactLifetime>();mark.Enabled=true;Impacts++;
if(variation==0)
{
var dust=GameObject.Clone($"prefabs/surface/{kind}.prefab",new Transform(hit.HitPosition,Rotation.LookAt(hit.Normal)),startEnabled:false);
dust.NetworkMode=NetworkMode.Never;dust.WorldScale=new Vector3(.3f);dust.Enabled=true;
}
}
protected override void OnUpdate()
{
if(started)age+=Time.Delta;started=true;var delta=end-start;float distance=delta.Length;
// The hit remains immediate. Give its cosmetic streak enough screen time
// to read at 30 fps, without extending it through the receiving surface.
float head=MathF.Min(distance,age*15000+60),tail=MathF.Max(0,head-50);
line.VectorPoints=[start+delta.Normal*tail,start+delta.Normal*head];
float alpha=(1-age/.085f).Clamp(0,1)*.55f;
line.Color=Gradient.FromColors(new Color(.85f,.8f,.7f,0),new Color(.92f,.9f,.82f,alpha));
if(age>=.085f)GameObject.Destroy();
}
protected override void OnPreRender(){if(line.IsValid() && line.VectorPoints.Count==2 && (line.VectorPoints[1]-line.VectorPoints[0]).Length>1)RenderedFrames++;}
}
public sealed class OverdriveImpactLifetime : Component
{
private float age;
protected override void OnStart()
{
foreach(var old in Scene.GetAllComponents<OverdriveImpactLifetime>().OrderByDescending(e=>e.age).SkipLast(127).ToArray())old.GameObject.Destroy();
}
protected override void OnUpdate(){age+=Time.Delta;if(age>75)GameObject.Destroy();}
}