ArenaOpponent.Repositioning.cs
using Sandbox;
using System;
namespace PaintballBaddies;
public sealed partial class ArenaOpponent
{
public int IncomingPaintEvents { get; private set; }
public int CoverRelocations { get; private set; }
public int PressureRelocations { get; private set; }
public bool RelocatingCover { get; private set; }
public bool RepositionPending { get; private set; }
public string LastRelocationReason { get; private set; }
public CoverSurface LastVacatedCover { get; private set; }
public float IncomingPressure { get; private set; }
public float CoverStayLimit { get; private set; }=10;
private float sinceIncomingPaint=100,pressureThreshold=4.5f,repositionDelay,repositionRetry;
private float coverRestDuration=.85f,coverCycleDuration=2.6f,coverTravelLimit=4.5f;
private readonly List<CoverMemory> recentlyUsedCover=new();
private sealed class CoverMemory { public CoverSurface Surface; public float Remaining; }
private void AdvanceCoverAwareness(float delta)
{
sinceIncomingPaint+=delta;
IncomingPressure=MathF.Max(0,IncomingPressure-delta*.55f);
repositionDelay=MathF.Max(0,repositionDelay-delta);
repositionRetry=MathF.Max(0,repositionRetry-delta);
foreach(var memory in recentlyUsedCover)memory.Remaining-=delta;
recentlyUsedCover.RemoveAll(x=>x.Remaining<=0 || !x.Surface.IsValid());
}
// Only actual nearby projectile segments/impacts report this, once per ball.
// The origin is captured at launch, not read from a hidden shooter's position.
internal void ObserveIncomingPaint(GameObject shooter,Vector3 launchOrigin)
{
var attacker=PaintballCombatant.Find(shooter);
if(!Enabled || !CombatEnabled || !combatant.IsValid() || combatant.Eliminated
|| !attacker.IsValid() || attacker==combatant || attacker.Team==combatant.Team)return;
IncomingPaintEvents++;
IncomingPressure=MathF.Min(10,IncomingPressure+1);
sinceIncomingPaint=0;
if(target==attacker || (!SeesPlayer && !HasCoverPlan && targetCommitment<=0))
{
ChangeTarget(attacker);LastKnownPlayerPosition=launchOrigin.WithZ(WorldPosition.z);
hasSeenPlayer=true;lostSight=0;
}
}
internal bool RecentlyUsed(CoverSurface surface)=>recentlyUsedCover.Any(x=>x.Surface==surface);
private void RememberCurrentCover()
{
if(!ReservedCover.IsValid())return;
recentlyUsedCover.RemoveAll(x=>x.Surface==ReservedCover);
recentlyUsedCover.Add(new CoverMemory{Surface=ReservedCover,Remaining=Game.Random.Float(10,16)});
if(recentlyUsedCover.Count>3)recentlyUsedCover.RemoveAt(0);
LastVacatedCover=ReservedCover;
}
private void VaryCoverCycle()
{
coverRestDuration=Game.Random.Float(.55f,.9f);
coverCycleDuration=coverRestDuration+Game.Random.Float(1.25f,1.9f);
}
private void BeginCoverStay()
{
RelocatingCover=false;RepositionPending=false;
CoverStayLimit=Game.Random.Float(7,11);
pressureThreshold=Game.Random.Float(3.5f,5.5f);
repositionRetry=0;VaryCoverCycle();
}
private bool UpdateRepositionDecision()
{
bool underFire=IncomingPressure>=pressureThreshold && sinceIncomingPaint<1.2f;
bool blockedLane=blockedLaneTime>1;
if(!RepositionPending && heldCoverTime>1 && (underFire || blockedLane || heldCoverTime>CoverStayLimit))
{
RepositionPending=true;repositionDelay=Game.Random.Float(.25f,.65f);
LastRelocationReason=underFire ? "incoming paint" : blockedLane ? "blocked firing lane" : "change firing angle";
}
if(!RepositionPending || repositionDelay>0 || repositionRetry>0)return false;
repositionRetry=Game.Random.Float(.75f,1.2f);
// Keep the current shelter until an unoccupied, reachable alternative
// exists. A failed search must not send her back into the firing lane.
if(!OpponentCoverPlanner.TryFind(GameObject,target.GameObject,LastKnownPlayerPosition,out var point,
requirePeek:LastRelocationReason!="incoming paint",relocating:true))return false;
var next=OpponentCoverPlanner.ShelteringSurface(GameObject,target.GameObject,LastKnownPlayerPosition,point);
if(!next.IsValid() || next==ReservedCover)return false;
RememberCurrentCover();
CoverDestination=point;ReservedCover=next;CoverSelections++;CoverRelocations++;
if(LastRelocationReason=="incoming paint")PressureRelocations++;
seekingCover=true;holdingCover=UsingCover=CoverPeeking=false;
RelocatingCover=true;RepositionPending=false;
coverTime=heldCoverTime=coverRest=blockedLaneTime=0;
coverTravelLimit=8;IncomingPressure*=.2f;
evadeAfterHit=MathF.Max(evadeAfterHit,1.2f);flankRemaining=0;
agent.MoveTo(point);
return true;
}
}