LongRangePaintReview.cs
using Sandbox;
using System;
namespace PaintballBaddies;
/// <summary>Opt-in native ballistic and real player/AI impact checks.</summary>
public sealed class LongRangePaintReview : Component
{
public List<string> Results {get;}=new();
public bool Done {get;private set;}
public int Stage {get;private set;}
PlayerController player;PaintballMarker marker;GameObject wall,ball;
Vector3 oldPosition,target;Angles oldAngles;bool oldLook,oldInput;
float elapsed;int index;bool ai;IDisposable hook;
readonly float[] distances={2401,2800,3200};
void Check(bool ok,string label)=>Results.Add((ok?"PASS ":"FAIL ")+label);
protected override void OnStart()
{
Scene.GetAllComponents<PaintballControls>().First().StartPractice();
player=MultiplayerSession.LocalPlayer(Scene);marker=player.Components.Get<PaintballMarker>();
player.Components.Get<CoverController>().Leave();
oldPosition=player.WorldPosition;oldAngles=player.EyeAngles;oldLook=player.UseLookControls;oldInput=player.UseInputControls;
player.UseInputControls=player.UseLookControls=false;player.WorldPosition=new(0,0,6002);player.EyeAngles=Angles.Zero;
player.Body.Velocity=Vector3.Zero;player.WishVelocity=Vector3.Zero;marker.ReviewAimOverride=true;
TrainingRange.Box(GameObject,"Range review floor",new(1700,0,5998),new(4000,1000,4),Color.Gray,true);
wall=TrainingRange.Box(GameObject,"Range review wall",new(3202,0,6100),new(4,500,200),Color.Gray,true);
hook=Scene.AddHook(GameObjectSystem.Stage.StartUpdate,-35,()=>
{Input.AnalogMove=Vector3.Zero;Input.AnalogLook=Angles.Zero;Input.SetAction("attack1",false);Input.SetAction("attack2",false);},nameof(LongRangePaintReview),"Temporary range checks");
foreach(float d in new[]{100f,2400,2401,2800,3200})
foreach(float height in new[]{-35f,0,45})
{
var aim=new Vector3(d,0,height);var velocity=PaintballFlight.LaunchVelocity(Vector3.Zero,aim);
float time=d/velocity.x;
var end=velocity*time+Vector3.Down*(PaintballFlight.Gravity*.5f*time*time);
Check((end-aim).Length<.05f&&time<PaintballFlight.Lifetime,$"ballistic endpoint {d} height {height}: error {(end-aim).Length:F4}");
}
foreach(float edge in new[]{2400f,PaintballFlight.SightCompensationRange})
{
var a=PaintballFlight.LaunchVelocity(Vector3.Zero,new(edge-1,0,0));var b=PaintballFlight.LaunchVelocity(Vector3.Zero,new(edge+1,0,0));
Check((a-b).Length<1,$"continuous velocity across {edge}");
}
}
protected override void OnUpdate()
{
if(Done)return;elapsed+=Time.Delta;
if(Stage==0&&elapsed>.7f)
{
var from=marker.Muzzle;target=from+Vector3.Forward*distances[index];
wall.WorldPosition=target+Vector3.Forward*2;
PaintImpactSystem.Find(Scene).Clear();
if(ai)
{
ball=new GameObject(GameObject,true,"Range review AI paintball");ball.WorldPosition=from;
var shot=ball.Components.Create<OpponentPaintball>();shot.Shooter=player.GameObject;shot.Velocity=PaintballFlight.LaunchVelocity(from,target);
}
else Check(marker.FireAt(target),$"player fires at {distances[index]}");
Stage=1;elapsed=0;
}
else if(Stage==1&&elapsed>1.3f)
{
var mark=PaintImpactSystem.Find(Scene).OldestDecal;
float error=mark.IsValid()?(mark.WorldPosition-target).Length:9999;
Check(error<3,$"{(ai?"AI":"player")} hits wall at {distances[index]} (error {error:F2})");
if(ball.IsValid())ball.Destroy();
if(ai){index++;ai=false;}else ai=true;
if(index>=distances.Length){Done=true;return;}Stage=0;elapsed=0;
}
}
protected override void OnDestroy()
{
hook?.Dispose();Input.ReleaseActions();if(ball.IsValid())ball.Destroy();
if(marker.IsValid())marker.ReviewAimOverride=null;
PaintImpactSystem.Find(Scene)?.Clear();
if(player.IsValid()){player.WorldPosition=oldPosition;player.EyeAngles=oldAngles;player.UseInputControls=oldInput;player.UseLookControls=oldLook;player.Body.Velocity=Vector3.Zero;player.WishVelocity=Vector3.Zero;}
}
}