OpponentBurstChecks.cs
using Sandbox;
using System.Collections.Generic;
namespace PaintballBaddies;
public sealed class OpponentBurstChecks : Component
{
[Property] public bool UseCitizenPresentation { get; set; }
[Property] public string CaptureId { get; set; } = "";
[Property] public float Duration { get; set; } = 6;
[Property] public bool EliminateDuringBurst { get; set; }
[Property] public bool CloseStart { get; set; }
private int shotsAtElimination;
private bool eliminated,signalObserved;
private float eliminatedAt;
private CitizenOpponentPresentation presentation;
private ArenaOpponent bot;
private PlayerController player;
private float elapsed;
private readonly List<float> times=new();
private int previous,hitLimit;
private bool input,look,durability;
protected override void OnStart()
{
player=Scene.GetAllComponents<PlayerController>().First();input=player.UseInputControls;look=player.UseLookControls;
player.UseInputControls=player.UseLookControls=false;player.WorldPosition=new Vector3(-950,-500,2);
var go=new GameObject(true,"Burst cadence check");go.WorldPosition=new Vector3(-750,-500,2);go.WorldRotation=Rotation.FromYaw(180);
bot=go.Components.Create<ArenaOpponent>();bot.CombatEnabled=true;
if(CloseStart)
{
go.WorldPosition=new Vector3(-920,-500,2);
Scene.NavMesh.IsEnabled=true;Scene.NavMesh.IncludeStaticBodies=true;
Scene.NavMesh.IncludeKeyframedBodies=false;Scene.NavMesh.AgentHeight=67;Scene.NavMesh.AgentRadius=17;
Scene.NavMesh.SetDirty();
}
}
protected override void OnUpdate()
{
var state=player.Components.Get<PaintballCombatant>();
if(state is null)return;
if(UseCitizenPresentation && !presentation.IsValid() && bot.Body.IsValid())
presentation=bot.Components.Create<CitizenOpponentPresentation>();
if(!durability){hitLimit=state.HitLimit;state.HitLimit=1000;durability=true;}
elapsed+=Time.Delta;
if(bot.Components.Get<NavMeshAgent>() is {} agent)agent.MaxSpeed=CloseStart ? 140 : 0;
if(bot.ShotsFired>previous){times.Add(elapsed);previous=bot.ShotsFired;}
if(EliminateDuringBurst && !eliminated && bot.ShotsFired>=7)
{
shotsAtElimination=bot.ShotsFired;eliminatedAt=elapsed;
var combatant=bot.Components.Get<PaintballCombatant>();
for(var hit=0;hit<combatant.HitLimit;hit++)combatant.RegisterHit(0);
eliminated=true;
}
if(eliminated && presentation.IsValid() && presentation.ShowingOutSignal)signalObserved=true;
if(elapsed<Duration)return;
bool pass=times.Count>=6;
if(pass)for(int i=1;i<6;i++){float gap=times[i]-times[i-1];pass &= i%3==0 ? gap>=.8f && gap<1.1f : gap>=.17f && gap<.26f;}
Log.Info($"AI_BURST {(pass?"PASS":"FAIL")} native six-shot cadence: times={string.Join(",",times)}");
if(UseCitizenPresentation)
Log.Info("CITIZEN_AI_FIRE "+Json.Serialize(new {capture_id=CaptureId,pass,
ready=presentation.IsReady,anchor=bot.PresentationAnchor.IsValid(),shots=bot.ShotsFired,
hits=state.PaintHits,alignment=bot.MinimumShotAlignment,reloads=bot.Magazine.Reloads,
ammo=bot.Magazine.Ammo,reserve=bot.Magazine.Reserve,pod_held=presentation.PodHeld,
eliminated,shots_at_elimination=shotsAtElimination,signal_observed=signalObserved,
signal_finished=eliminated && !presentation.ShowingOutSignal,
seconds_after_elimination=eliminated ? elapsed-eliminatedAt : 0,times}));
if(CloseStart)Log.Info("CITIZEN_CLOSE_RANGE "+Json.Serialize(new {capture_id=CaptureId,
shots=bot.ShotsFired,hits=state.PaintHits,distance=(bot.WorldPosition-player.WorldPosition).WithZ(0).Length,
travelled=bot.DistanceTravelled}));
Destroy();
}
protected override void OnDestroy()
{
if(bot.IsValid())bot.GameObject.Destroy();
if(player.IsValid()){player.UseInputControls=input;player.UseLookControls=look;if(durability)player.Components.Get<PaintballCombatant>().HitLimit=hitLimit;}
}
}