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;
}
}