SuppressionRepositionReview.cs
using Sandbox;
namespace PaintballBaddies;
/// <summary>Opt-in native test: physical cover impacts must cause real relocation.</summary>
public sealed class SuppressionRepositionReview : Component
{
private ArenaOpponent bot;
private PaintballCombatant shooter;
private CoverSurface original,second;
private Vector3 originalPosition;
private float elapsed,stageTime,nextShot;
private int stage,eventsBefore,relocationsBefore;
private bool routeChecked;
public bool Done { get; private set; }
public List<string> Results { get; }=new();
public string State=>bot.IsValid() ? Json.Serialize(new {stage,elapsed,bot.IncomingPaintEvents,
bot.IncomingPressure,bot.PressureRelocations,bot.CoverRelocations,bot.UsingCover,
bot.RepositionPending,bot.RelocatingCover,bot.LastRelocationReason,
Cover=bot.ReservedCover?.GameObject.Name,Position=bot.WorldPosition.ToString(),bot.ShotsFromCover}) : "starting";
protected override void OnStart()
{
// Also run from fresh Practice, before any match has built navigation.
Scene.NavMesh.IsEnabled=true;Scene.NavMesh.IncludeStaticBodies=true;
Scene.NavMesh.IncludeKeyframedBodies=false;Scene.NavMesh.AgentHeight=67;Scene.NavMesh.AgentRadius=17;Scene.NavMesh.SetDirty();
var origin=new GameObject(GameObject){Name="Suppression review shooter",WorldPosition=new(-700,0,1)};
shooter=origin.Components.Create<PaintballCombatant>();shooter.Team=90;shooter.StayInMatch=true;
var go=new GameObject(GameObject){Name="Suppression review opponent",WorldPosition=new(-190,60,1)};
go.Components.Create<CitizenOpponentPresentation>();
bot=go.Components.Create<ArenaOpponent>();bot.Character="imani";bot.CombatEnabled=true;
}
private void Check(bool okay,string name)
{
var text=(okay ? "PASS " : "FAIL ")+name;
Results.Add(text);Log.Info("SUPPRESSION_REPOSITION "+text);
}
private void Next(){stage++;stageTime=0;nextShot=0;}
private void Fire(bool far=false)
{
var from=shooter.WorldPosition.WithZ(bot.WorldPosition.z)+Vector3.Up*(far ? 350 : 55);
var aim=far ? from+Vector3.Forward*650 : bot.CoverDestination+Vector3.Up*30;
var ball=new GameObject(GameObject){Name="Suppression review paintball",WorldPosition=from};
var shot=ball.Components.Create<OpponentPaintball>();shot.Shooter=shooter.GameObject;shot.Team=shooter.Team;
shot.Velocity=PaintballFlight.LaunchVelocity(from,aim);
}
protected override void OnUpdate()
{
if(Done)return;
elapsed+=Time.Delta;stageTime+=Time.Delta;nextShot-=Time.Delta;
if(elapsed>35){Check(false,"timed out: "+State);Done=true;return;}
if(stage==0 && stageTime>.5f)
{
var actor=bot.Components.Get<PaintballCombatant>();
if(!actor.IsValid())return;
actor.StayInMatch=true;bot.Magazine.UnlimitedAmmo=true;
actor.RegisterHit(90,shooter.WorldPosition,shooter);Next();
}
else if(stage==1 && bot.UsingCover)
{
original=bot.ReservedCover;originalPosition=bot.WorldPosition;
Check(original.IsValid(),"entered physical shelter");
eventsBefore=bot.IncomingPaintEvents;Fire(true);Next();
}
else if(stage==2 && stageTime>.8f)
{
Check(bot.IncomingPaintEvents==eventsBefore,"distant paint does not create pressure");
Fire();Next();
}
else if(stage==3 && stageTime>1.1f)
{
Check(bot.IncomingPaintEvents==eventsBefore+1,$"one physical cover shot counted once (before={eventsBefore}, after={bot.IncomingPaintEvents})");
Check(bot.CoverRelocations==0 && bot.ReservedCover==original,"one shot does not force immediate relocation");
Next();
}
else if(stage==4)
{
if(bot.PressureRelocations>0)
{
second=bot.ReservedCover;
Check(second.IsValid() && second!=original,"sustained fire selects a different structure");
Check((bot.CoverDestination-originalPosition).WithZ(0).Length>=140,"relocation leaves the original area");
Check(bot.RecentlyUsed(original),"recent shelter remembered");
Next();
}
else if(nextShot<=0){Fire();nextShot=PaintballFlight.ShotInterval+.02f;}
}
else if(stage==5)
{
// MoveTo submits a request; GetPath may still contain the old path
// that frame. Inspect the active route after navigation updates.
if(!routeChecked && stageTime>.25f)
{
routeChecked=true;
Check(bot.Components.Get<NavMeshAgent>().GetPath().Status==Sandbox.Navigation.NavMeshPathStatus.Complete,"new route reaches its destination");
}
if(bot.UsingCover)
{
Check(bot.ReservedCover==second && (bot.WorldPosition-originalPosition).WithZ(0).Length>100,"opponent physically reaches new shelter");
relocationsBefore=bot.PressureRelocations;Next();
}
}
else if(stage==6)
{
if(bot.PressureRelocations>relocationsBefore)
{
Check(bot.ReservedCover!=original && bot.ReservedCover!=second,"continued fire does not bounce straight back");
Check(bot.ShotsWhileSheltered==0,"no firing through protective cover");
Done=true;
}
else if(nextShot<=0){Fire();nextShot=PaintballFlight.ShotInterval+.02f;}
}
}
}