ArenaOpponent.PopRiot.cs
using Sandbox;
using System;
namespace PaintballBaddies;

public sealed partial class ArenaOpponent
{
    private PopRiotTarget riotTarget;
    private float riotThink,riotFire,riotDodge;
    private Vector3 riotDodgeDestination;
    public string PopRiotTask {get;private set;}="";
    private void UpdatePopRiotObjective()
    {
        var session=MultiplayerSession.Find(Scene);
        if(!session.IsValid() || !session.InRound || !CombatEnabled)
        {agent.Stop();SeesPlayer=false;return;}
        UsingCover=CoverPeeking=holdingCover=seekingCover=false;flankRemaining=0;
        riotThink-=Time.Delta;riotFire-=Time.Delta;riotDodge-=Time.Delta;
        bool dodging=DodgeRiotThreat(session);
        if(riotThink<=0 || !riotTarget.IsValid() || riotTarget.Popped || !riotTarget.CanBeDamaged)
        {
            riotThink=.35f;
            var previousTarget=riotTarget;
            riotTarget=session.RiotTargets.Where(t=>t.CanBeDamaged && !t.Entering
                && (t.Kind==PopKind.Glob || !Scene.GetAllComponents<ArenaOpponent>().Any(a=>a!=this && a.riotTarget==t))).OrderBy(t=>
                {var ray=PaintballFlight.Trace(Scene,WorldPosition+Vector3.Up*55,t.AimPoint(WorldPosition),GameObject);return !ray.Hit || PopRiotTarget.Find(ray.GameObject)==t ? 0 : 1;}).ThenBy(t=>(t.WorldPosition-WorldPosition).Length*(t.Kind==PopKind.Glob ? .35f : t.Kind==PopKind.Chain ? .65f : 1)
                + (t.Seed+CharacterIndex*7)%5*42).FirstOrDefault();
            if(riotTarget.IsValid() && riotTarget!=previousTarget)riotFire=MathF.Max(riotFire,riotTarget.Kind==PopKind.Glob ? .15f : .9f);
        }
        var delta=WorldPosition-previous;DistanceTravelled+=delta.Length;
        MovementSpeed=delta.WithZ(0).Length/MathF.Max(Time.Delta,.001f);previous=WorldPosition;
        if(!riotTarget.IsValid())
        {SeesPlayer=false;if(!dodging)agent.Stop();PopRiotTask=dodging ? "Dodging incoming paint" : "Watching the roof for the next wave";return;}
        var eye=WorldPosition+Vector3.Up*55;
        var aim=riotTarget.AimPoint(eye);
        var sight=PaintballFlight.Trace(Scene,eye,aim,GameObject);
        bool clear=!sight.Hit || PopRiotTarget.Find(sight.GameObject)==riotTarget;
        SeesPlayer=clear;observedAimPoint=aim;LastKnownPlayerPosition=aim-Vector3.Up*40;
        var horizontal=(aim-WorldPosition).WithZ(0);
        if(horizontal.Length>1)WorldRotation=Rotation.Lerp(WorldRotation,Rotation.LookAt(horizontal),1-MathF.Exp(-12*Time.Delta));
        if(dodging){PopRiotTask="Dodging incoming paint";}
        else if(!clear || horizontal.Length>620)
        {
            // Reuse native navigation, with distinct approach angles for the six seats.
            var approach=riotTarget.Anchor+Rotation.FromYaw(CharacterIndex*60).Forward*180;
            var point=Scene.NavMesh.GetClosestPoint(approach,180);
            if(point.HasValue)agent.MoveTo(point.Value);
            PopRiotTask="Moving to a clear firing lane";
        }
        else {agent.Stop();PopRiotTask="Bursting paint targets";}
        if(!clear || riotFire>0 || !marker.IsValid() || !combatant.IsValid())return;
        UpdateCoveredMarkerPose(false);
        var muzzle=marker.WorldTransform.PointToWorld(PaintballMarker.MuzzleOffset);
        var lane=PaintballFlight.Trace(Scene,muzzle,aim,GameObject);
        if(lane.Hit && PopRiotTarget.Find(lane.GameObject)!=riotTarget){riotThink=0;return;}
        var muzzleClear=PaintballFlight.Trace(Scene,eye,muzzle,GameObject);
        if(muzzleClear.Hit)return;
        if(Vector3.Dot(WorldRotation.Forward,horizontal.Normal)<.90f)return;
        riotFire=(riotTarget.Kind==PopKind.Glob ? .5f : 2.3f)+CharacterIndex*.1f;shots++;
        // AI helps without instantly clearing every target before a human can react.
        var error=Rotation.FromYaw(shots*71+CharacterIndex*23).Forward*(shots%4==0 ? 25 : 6);
        var lead=riotTarget.FlightVelocity*((aim-muzzle).Length/3400f);
        NetworkEffects.SpawnBall(GameObject,combatant.Team,muzzle,PaintballFlight.LaunchVelocity(muzzle,aim+lead+error));
        NetworkEffects.Sound(CharacterMarkerSound.For(GameObject),muzzle,Guid.Empty);
    }
    private bool DodgeRiotThreat(MultiplayerSession session)
    {
        if(riotDodge>0){agent.MoveTo(riotDodgeDestination);return true;}
        var eye=WorldPosition+Vector3.Up*48;
        var danger=session.RiotTargets.FirstOrDefault(t=>t.Charging && t.AttackTarget==combatant && t.ChargeRemaining<1.05f);
        if(!danger.IsValid())danger=session.RiotTargets.FirstOrDefault(t=>t.Kind==PopKind.Glob
            && (t.WorldPosition-eye).Length<420 && Vector3.Dot((eye-t.WorldPosition).Normal,t.GlobVelocity.Normal)>.93f);
        if(!danger.IsValid())return false;
        var away=(eye-danger.WorldPosition).WithZ(0).Normal;
        var side=Vector3.Cross(Vector3.Up,away);
        foreach(float sign in new[]{CharacterIndex%2==0 ? 1f : -1f,CharacterIndex%2==0 ? -1f : 1f})
        {
            var desired=WorldPosition+side*sign*145;
            var point=Scene.NavMesh.GetClosestPoint(desired,65);
            if(!point.HasValue || (point.Value-WorldPosition).Length<80)continue;
            var sweep=Scene.Trace.Sphere(18,WorldPosition+Vector3.Up*34,point.Value+Vector3.Up*34)
                .IgnoreGameObjectHierarchy(GameObject).WithoutTags("paintball_debris","pop_target").Run();
            if(sweep.Hit)continue;
            riotDodgeDestination=point.Value;riotDodge=1.05f;agent.MoveTo(point.Value);return true;
        }
        return false;
    }
}