OpponentCoverChecks.cs
using Sandbox;
using System.Linq;
namespace PaintballBaddies;
/// <summary>Opt-in native cover/reload sequence against placed arena geometry.</summary>
public sealed class OpponentCoverChecks : Component
{
[Property] public bool CitizenCharacters { get; set; }
[Property] public bool ReviewFootsteps { get; set; }
[Property] public bool SpoolMode { get; set; }
private bool reachedSpool;
[Property] public bool FlankMode { get; set; }
private bool flanked;
[Property] public bool PressureMode { get; set; }
[Property] public bool TwoOpponents { get; set; }
private ArenaOpponent opponent;
private ArenaOpponent second;
private PlayerController player;
private float elapsed;
private bool arrived,reloaded,crouched,sheltered,reengaged;
private int previousShots,coveredShots;
private bool pressureApplied,secondArrived,durabilityAdjusted;
private int pressureAmmo,originalHitLimit;
private int movingFrames,alignedFrames,animatedFrames;
private void Check(string name,bool pass,string detail)=>Log.Info($"AI_COVER {(pass ? "PASS" : "FAIL")} {name}: {detail}");
protected override void OnStart()
{
player=Scene.GetAllComponents<PlayerController>().First();
player.UseInputControls=player.UseLookControls=false;
player.WorldPosition=SpoolMode ? new Vector3(-950,400,1) : new Vector3(-630,100,1);
Scene.GetAllComponents<TrainingRange>().First().SetTargetsVisible(false);
Scene.NavMesh.IsEnabled=true;Scene.NavMesh.IncludeStaticBodies=true;
Scene.NavMesh.IncludeKeyframedBodies=false;Scene.NavMesh.AgentHeight=67;Scene.NavMesh.AgentRadius=17;Scene.NavMesh.SetDirty();
var go=new GameObject(true,"Tactical cover test opponent");go.WorldPosition=SpoolMode ? new Vector3(-590,520,1) : new Vector3(-190,-40,1);
go.WorldRotation=Rotation.LookAt((player.WorldPosition-go.WorldPosition).WithZ(0));
if(CitizenCharacters)go.Components.Create<CitizenOpponentPresentation>();
opponent=go.Components.Create<ArenaOpponent>();opponent.CombatEnabled=true;
// Pressure mode keeps ammunition above the low-ammo trigger while
// leaving room to validate a real reload (a full magazine cannot reload).
for(int i=0;i<(PressureMode ? 3 : 24);i++) { opponent.Magazine.TryFire();opponent.Magazine.Advance(1); }
if(TwoOpponents)
{
var teammate=new GameObject(true,"Second tactical cover test opponent");teammate.WorldPosition=new Vector3(-190,80,1);
teammate.WorldRotation=Rotation.LookAt((player.WorldPosition-teammate.WorldPosition).WithZ(0));
if(CitizenCharacters)teammate.Components.Create<CitizenOpponentPresentation>();
second=teammate.Components.Create<ArenaOpponent>();second.CombatEnabled=true;second.Character="leilani";
for(int i=0;i<24;i++) { second.Magazine.TryFire();second.Magazine.Advance(1); }
}
}
protected override void OnUpdate()
{
var state=player?.Components.Get<PaintballCombatant>();
if(state is null || !opponent.IsValid())return;
if(!durabilityAdjusted) { originalHitLimit=state.HitLimit;state.HitLimit=1000;durabilityAdjusted=true; }
if(opponent.Components.Get<MovementFootsteps>() is { } steps)steps.ReviewContacts=ReviewFootsteps;
elapsed+=Time.Delta;
if(elapsed>1 && opponent.MovementSpeed>20)
{
movingFrames++;
if(opponent.MovementFacingDot>.8f)alignedFrames++;
var presentation=opponent.Components.Get<CitizenOpponentPresentation>();
if(presentation is not null && presentation.IsReady && System.MathF.Abs(presentation.AnimationSpeed-opponent.NavigationVelocity.WithZ(0).Length)<20)animatedFrames++;
}
if(PressureMode && !pressureApplied && elapsed>1.5f)
{
pressureAmmo=opponent.Magazine.Ammo;
pressureApplied=opponent.Components.Get<PaintballCombatant>().RegisterHit(0,player.WorldPosition);
}
if(FlankMode && opponent.UsingCover && !flanked)
{
var away=(opponent.WorldPosition-player.WorldPosition).WithZ(0).Normal;
player.WorldPosition=opponent.WorldPosition+away*100;
flanked=true;
}
if(FlankMode && elapsed>10)
{
Check("flank begins after cover arrival",flanked,"player moves onto exposed side");
Check("observed flank invalidates cover",opponent.InvalidatedCoverPlans>0,$"invalidated={opponent.InvalidatedCoverPlans}");
opponent.GameObject.Destroy();
state.HitLimit=originalHitLimit;state.ResetPaintHits();
player.UseInputControls=player.UseLookControls=true;
Scene.GetAllComponents<TrainingRange>().First().SetTargetsVisible(true);
Log.Info("AI_COVER COMPLETE");Destroy();return;
}
secondArrived |= second?.UsingCover==true;
if(opponent.UsingCover)
{
arrived=true;
reachedSpool |= (opponent.WorldPosition-new Vector3(-720,400,0)).WithZ(0).Length<70;
var citizen=opponent.Components.Get<CitizenOpponentPresentation>();
var lowered=CitizenCharacters ? citizen is not null && citizen.IsReady && citizen.HeadHeight>0 && citizen.HeadHeight<55 : opponent.Components.Get<SkinnedModelRenderer>().Sequence.Name=="crouch";
crouched |= lowered && opponent.Components.Get<BoxCollider>().Scale.z==43
&& System.MathF.Abs(opponent.Components.Get<PaintballCombatant>().EyePosition.z-opponent.WorldPosition.z-35)<.01f;
var shield=Scene.Trace.Ray(player.WorldPosition+Vector3.Up*55,opponent.WorldPosition+Vector3.Up*30)
.IgnoreGameObjectHierarchy(player.GameObject).IgnoreGameObjectHierarchy(opponent.GameObject).Run();
sheltered |= shield.Hit && shield.GameObject.Components.Get<CoverSurface>() is not null;
coveredShots+=opponent.ShotsFired-previousShots;
}
if(arrived && !opponent.UsingCover && opponent.ShotsFired>previousShots)reengaged=true;
previousShots=opponent.ShotsFired;
reloaded |= opponent.UsingCover && opponent.Magazine.Reloads>0;
if(elapsed<14)return;
if(CitizenCharacters)
Check("locomotion speed follows navigation",movingFrames>30 && animatedFrames>movingFrames*.8f,$"matched={animatedFrames}/{movingFrames}; native strafe/backpedal allowed");
else
Check("locomotion follows travel",movingFrames>30 && alignedFrames>(movingFrames*.8f),$"aligned={alignedFrames}/{movingFrames}");
Check("shots follow marker aim",opponent.ShotsFired>0 && opponent.MinimumShotAlignment>=.966f,$"shots={opponent.ShotsFired}, minimumDot={opponent.MinimumShotAlignment}");
Check("selects nearby cover",opponent.CoverSelections>0,$"selections={opponent.CoverSelections}, destination={opponent.CoverDestination}");
Check("reaches selected cover",arrived,$"position={opponent.WorldPosition}");
Check("crouches with smaller hit collider and lowered sight point",crouched,"observed while sheltered");
Check("reloads in cover",reloaded,$"reloads={opponent.Magazine.Reloads}, ammo={opponent.Magazine.Ammo}, reserve={opponent.Magazine.Reserve}");
Check("leaves reload cover",arrived && !opponent.UsingCover,"re-engagement state");
Check("actual crouched position shielded",sheltered,"ray from observed threat blocked by marked cover");
Check("does not fire while sheltered",arrived && coveredShots==0,$"shots={coveredShots}");
Check("fires after re-engaging",reengaged,$"total shots={opponent.ShotsFired}");
Check("does not cross obstruction",opponent.ObstructedMoves==0,$"crossings={opponent.ObstructedMoves}, distance={opponent.DistanceTravelled}");
if(SpoolMode)Check("reaches placed spool shelter",reachedSpool,"within70in of west spool while UsingCover");
if(PressureMode)Check("paint pressure triggers cover above low-ammo threshold",pressureApplied && pressureAmmo>6 && arrived,$"ammo at hit={pressureAmmo}");
if(TwoOpponents)
{
Check("teammates choose separate cover destinations",opponent.CoverSelections>0 && second.CoverSelections>0
&& (opponent.CoverDestination-second.CoverDestination).WithZ(0).Length>=65,$"first={opponent.CoverDestination}, second={second.CoverDestination}");
Check("second opponent reaches cover",secondArrived,$"selections={second.CoverSelections}");
Check("second opponent avoids obstruction crossings",second.ObstructedMoves==0,$"crossings={second.ObstructedMoves}");
second.GameObject.Destroy();
}
opponent.GameObject.Destroy();
player.Components.Get<PaintballCombatant>().HitLimit=originalHitLimit;
player.Components.Get<PaintballCombatant>().ResetPaintHits();
player.UseInputControls=player.UseLookControls=true;
Scene.GetAllComponents<TrainingRange>().First().SetTargetsVisible(true);
Log.Info("AI_COVER COMPLETE");Destroy();
}
}