ArenaOpponent.Overdrive.cs
using Sandbox;
using System;
namespace PaintballBaddies;
public sealed partial class ArenaOpponent
{
private float overdriveThink,overdriveFire,overdriveWeaponTime,overdriveGrenadeThink;
private int overdriveWeaponRound=-1;
private int overdriveSpecialRound=-1,overdriveSpecialWave=-1;
private Vector3 overdriveAim;
private void UpdateOverdriveObjective()
{
var session=MultiplayerSession.Find(Scene);
if(!CombatEnabled || !session.IsValid() || !session.InRound || session.OverdriveReviewPaused || session.OverdrivePhase=="READY"){agent.Stop();return;}
UsingCover=CoverPeeking=holdingCover=seekingCover=false;flankRemaining=0;
var travel=WorldPosition-previous;DistanceTravelled+=travel.Length;MovementSpeed=travel.WithZ(0).Length/MathF.Max(.001f,Time.Delta);previous=WorldPosition;
overdriveThink-=Time.Delta;overdriveFire-=Time.Delta;overdriveWeaponTime-=Time.Delta;overdriveGrenadeThink-=Time.Delta;
if(overdriveSpecialRound!=session.RoundId || overdriveSpecialWave!=session.OverdriveWave)
{
overdriveSpecialRound=session.RoundId;overdriveSpecialWave=session.OverdriveWave;
combatant.PrepareAISpecial(session.OverdriveWave,CharacterIndex);
overdriveGrenadeThink=2+CharacterIndex*.45f;
}
if(overdriveWeaponRound!=session.RoundId || overdriveWeaponTime<=0)
{
overdriveWeaponRound=session.RoundId;overdriveWeaponTime=10;
Components.Get<PaintballCombatant>()?.EquipWeapon(Game.Random.Float(0,1)<.2f ? 1 : 0);
}
var candidates=session.OverdriveEnemies.Select(e=>e.AimPoint)
.Concat(session.RiotTargets.Select(t=>t.WorldPosition)).OrderBy(p=>(p-WorldPosition).Length).ToArray();
var sight=candidates.FirstOrDefault(p=>!Scene.Trace.Ray(WorldPosition+Vector3.Up*58,p)
.IgnoreGameObjectHierarchy(GameObject).WithoutTags("paintball_actor","paintball_debris","overdrive_enemy","pop_target").Run().Hit);
bool visible=candidates.Length>0 && sight!=Vector3.Zero;
overdriveAim=visible ? sight : candidates.FirstOrDefault();
if(overdriveAim==Vector3.Zero){agent.Stop();return;}
LastKnownPlayerPosition=overdriveAim-Vector3.Up*40;SeesPlayer=visible;observedAimPoint=overdriveAim;
var facing=(overdriveAim-WorldPosition).WithZ(0).Normal;
WorldRotation=Rotation.Slerp(WorldRotation,Rotation.LookAt(facing),1-MathF.Exp(-9*Time.Delta));
float distance=(overdriveAim-WorldPosition).WithZ(0).Length;
if(visible && distance<440 && distance>180)agent.Stop();
else if(overdriveThink<=0)
{
overdriveThink=.55f;
var destination=Scene.NavMesh.GetClosestPoint(overdriveAim.WithZ(WorldPosition.z)-facing*240+Rotation.FromYaw(CharacterIndex*60).Forward*80,240);
if(destination.HasValue){agent.MaxSpeed=205;agent.MoveTo(destination.Value);}
}
if(!visible || overdriveFire>0 || Vector3.Dot(WorldRotation.Forward,facing)<.9f || !marker.IsValid())return;
var muzzle=marker.WorldTransform.PointToWorld(OverdriveLoadout.MuzzleFor(GameObject));
if(Scene.Trace.Ray(WorldPosition+Vector3.Up*58,muzzle).IgnoreGameObjectHierarchy(GameObject).WithoutTags("paintball_actor","paintball_debris").Run().Hit)return;
if(TryOverdriveGrenade(session,distance,overdriveAim))return;
// Teammates help without deleting every target before a human can react.
overdriveFire=2.2f+CharacterIndex*.12f;
OverdriveShotgun.Fire(Scene,combatant,muzzle,overdriveAim+Rotation.FromYaw(shots*71+CharacterIndex*23).Forward*(shots%3==0 ? 28 : 8),++shots);
}
private bool TryOverdriveGrenade(MultiplayerSession session,float distance,Vector3 aim)
{
// One roll per suitable attack opportunity, never per frame. Teammates
// consume their own queue through the same authoritative throw.
if(overdriveGrenadeThink>0 || combatant.GrenadeCharges<=0 || distance<=190 || distance>=720)return false;
bool useful=!combatant.FreezeGrenadeReady || session.OverdriveEnemies.Any(e=>e.FrozenRemaining<=0 && (e.AimPoint-aim).Length<SausageGrenade.BlastRadius);
if(!useful)return false;
overdriveGrenadeThink=4;
if(Game.Random.Float(0,1)>=.2f || !combatant.TryThrowGrenade(aim))return false;
overdriveGrenadeThink=7;overdriveFire=2.4f;return true;
}
}