Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
230 lines
9.9 KiB
C
230 lines
9.9 KiB
C
// Local knowledge of mines laid by this checkpoint service. No mine immunity,
|
|
// no hostile/player detection, no changes to native pressure/explosion logic.
|
|
class ERR_MineAvoidanceRules
|
|
{
|
|
static bool Clear(vector start, vector end, vector mine, float radius)
|
|
{
|
|
if (!ERR_ForceRules.FiniteVector(start) || !ERR_ForceRules.FiniteVector(end) || !ERR_ForceRules.FiniteVector(mine)
|
|
|| !ERR_ForceRules.Finite(radius) || radius < 0) return false;
|
|
vector delta = end - start; delta[1] = 0;
|
|
float squared = delta.LengthSq();
|
|
if (!ERR_ForceRules.Finite(squared)) return false;
|
|
float fraction;
|
|
if (squared > 0.001) fraction = Math.Clamp(vector.Dot(mine - start, delta) / squared, 0, 1);
|
|
return vector.DistanceXZ(mine, start + delta * fraction) > radius;
|
|
}
|
|
// A short chord round the danger circle, chosen toward the original route.
|
|
// Used repeatedly until that route is clear; never order a straight crossing.
|
|
static vector Detour(vector start, vector goal, vector mine, int side)
|
|
{
|
|
vector away = start - mine; away[1] = 0;
|
|
if (away.LengthSq() < 0.01) return start;
|
|
away.Normalize();
|
|
if (vector.DistanceXZ(start, mine) < 10) return mine + away * 16;
|
|
vector tangent = Vector(away[2], 0, -away[0]);
|
|
if (side < 0) tangent = -tangent;
|
|
return mine + (away * 0.707107 + tangent * 0.707107) * 16;
|
|
}
|
|
}
|
|
class ERR_KnownMine
|
|
{
|
|
IEntity Item;
|
|
vector Position;
|
|
bool Live()
|
|
{
|
|
if (!ERR_Actors.Alive(Item) || Item.GetParent() || vector.Distance(Item.GetOrigin(), Position) > 1) return false;
|
|
SCR_PressureTriggerComponent trigger = SCR_PressureTriggerComponent.Cast(Item.FindComponent(SCR_PressureTriggerComponent));
|
|
return trigger && trigger.IsActivated();
|
|
}
|
|
}
|
|
class ERR_MineDetour
|
|
{
|
|
IEntity Actor;
|
|
SCR_AIUtilityComponent Utility;
|
|
ref SCR_AIBehaviorBase Action;
|
|
vector Goal;
|
|
vector Step;
|
|
float Expires;
|
|
float NextStep;
|
|
SCR_AIGroup Group;
|
|
AIWaypoint Waypoint;
|
|
bool ResumeMove;
|
|
bool FinalLeg;
|
|
void Clear()
|
|
{
|
|
SCR_ChimeraAIAgent agent;
|
|
if (Actor) agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(Actor));
|
|
if (agent && agent.m_UtilityComponent == Utility && Action)
|
|
{
|
|
array<ref AIActionBase> actions = {}; Utility.GetActions(actions);
|
|
if (actions.Contains(Action)) Action.Complete();
|
|
}
|
|
Action = null;
|
|
}
|
|
}
|
|
class ERR_KnownMines
|
|
{
|
|
protected ERR_GameMode m_Mode;
|
|
protected ref array<ref ERR_KnownMine> m_Mines = {};
|
|
protected ref array<ref ERR_MineDetour> m_Detours = {};
|
|
protected ref array<IEntity> m_Near = {};
|
|
protected bool m_Running, m_Stopped;
|
|
void ERR_KnownMines(ERR_GameMode mode) { m_Mode = mode; }
|
|
void Add(IEntity item, vector position)
|
|
{
|
|
if (m_Stopped || !item || m_Mines.Count() >= 48) return;
|
|
foreach (ERR_KnownMine old : m_Mines) if (old.Item == item) return;
|
|
ERR_KnownMine mine = new ERR_KnownMine(); mine.Item = item; mine.Position = position;
|
|
if (!mine.Live()) return;
|
|
m_Mines.Insert(mine);
|
|
if (!m_Running) { m_Running = true; GetGame().GetCallqueue().CallLater(Tick, 250, true); }
|
|
}
|
|
protected bool FriendlyActor(IEntity actor)
|
|
{
|
|
if (!ChimeraCharacter.Cast(actor) || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) return false;
|
|
Faction faction = SCR_FactionManager.SGetFaction(actor);
|
|
return faction && m_Mode.EnemyFaction() && !m_Mode.EnemyFaction().IsFactionEnemy(faction);
|
|
}
|
|
protected bool Collect(IEntity actor)
|
|
{
|
|
if (m_Near.Count() >= 128) return false;
|
|
if (FriendlyActor(actor) && !m_Near.Contains(actor)) m_Near.Insert(actor);
|
|
return true;
|
|
}
|
|
protected ERR_KnownMine Blocking(vector start, vector goal, float radius)
|
|
{
|
|
ERR_KnownMine nearest;
|
|
float best = float.MAX;
|
|
foreach (ERR_KnownMine mine : m_Mines)
|
|
{
|
|
if (ERR_MineAvoidanceRules.Clear(start, goal, mine.Position, radius)) continue;
|
|
float distance = vector.DistanceXZ(start, mine.Position);
|
|
if (distance < best) { best = distance; nearest = mine; }
|
|
}
|
|
return nearest;
|
|
}
|
|
protected bool SafeStep(vector start, vector end, ERR_KnownMine escaping, IEntity actor)
|
|
{
|
|
foreach (ERR_KnownMine mine : m_Mines)
|
|
{
|
|
// Inside the safety circle only a strictly outward move is admissible.
|
|
if (mine == escaping && vector.DistanceXZ(start, mine.Position) < 10)
|
|
{
|
|
vector travel = end - start, away = start - mine.Position;
|
|
if (vector.DotXZ(travel, away) <= 0) return false;
|
|
}
|
|
else if (!ERR_MineAvoidanceRules.Clear(start, end, mine.Position, 8)) return false;
|
|
}
|
|
if (!ERR_HQRoadIntercept.Standable(end)) return false;
|
|
// Do not hand native navigation an obstructed straight chord whose actual
|
|
// detour could bend back through the mine. Wait when this corridor is blocked.
|
|
TraceBox path = new TraceBox(); path.Start = start + "0 1 0"; path.End = end + "0 1 0";
|
|
path.Mins = "-0.35 -0.8 -0.35"; path.Maxs = "0.35 0.8 0.35";
|
|
path.Exclude = actor; path.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
|
return GetGame().GetWorld().TraceMove(path, null) == 1;
|
|
}
|
|
protected void Steer(ERR_MineDetour detour, ERR_KnownMine mine)
|
|
{
|
|
vector start = detour.Actor.GetOrigin();
|
|
// A cover point or movement prediction inside the danger circle is not a
|
|
// destination we can safely reach. Hold outside instead of circling it.
|
|
if (!ERR_MineAvoidanceRules.Clear(detour.Goal, detour.Goal, mine.Position, 10)
|
|
&& vector.DistanceXZ(start, mine.Position) >= 12)
|
|
{
|
|
detour.Clear(); detour.FinalLeg = false; detour.Step = start;
|
|
detour.NextStep = GetGame().GetWorld().GetWorldTime() + 5000;
|
|
detour.Action = new SCR_AIWaitBehavior(detour.Utility, null); detour.Action.SetPriority(1100);
|
|
detour.Utility.AddAction(detour.Action);
|
|
return;
|
|
}
|
|
vector first = ERR_Actors.Ground(ERR_MineAvoidanceRules.Detour(start, detour.Goal, mine.Position, 1));
|
|
vector second = ERR_Actors.Ground(ERR_MineAvoidanceRules.Detour(start, detour.Goal, mine.Position, -1));
|
|
if (vector.DistanceXZ(second, detour.Goal) < vector.DistanceXZ(first, detour.Goal))
|
|
{ vector swap = first; first = second; second = swap; }
|
|
bool safe = SafeStep(start, first, mine, detour.Actor);
|
|
if (!safe && SafeStep(start, second, mine, detour.Actor)) { first = second; safe = true; }
|
|
detour.Clear();
|
|
detour.FinalLeg = false;
|
|
detour.NextStep = GetGame().GetWorld().GetWorldTime() + 1000;
|
|
if (safe)
|
|
{
|
|
detour.Step = first;
|
|
detour.Action = new SCR_AIMoveIndividuallyBehavior(detour.Utility, null, first, 1100, 0, null, 1);
|
|
}
|
|
else
|
|
{
|
|
// Never deliberately send a guard across a mine when terrain offers no step.
|
|
detour.Step = start;
|
|
detour.Action = new SCR_AIWaitBehavior(detour.Utility, null); detour.Action.SetPriority(1100);
|
|
}
|
|
detour.Utility.AddAction(detour.Action);
|
|
}
|
|
protected void Tick()
|
|
{
|
|
if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) { Stop(); return; }
|
|
for (int i = m_Mines.Count() - 1; i >= 0; i--) if (!m_Mines[i].Live()) m_Mines.Remove(i);
|
|
float now = GetGame().GetWorld().GetWorldTime();
|
|
for (int i = m_Detours.Count() - 1; i >= 0; i--)
|
|
{
|
|
ERR_MineDetour detour = m_Detours[i];
|
|
SCR_ChimeraAIAgent agent;
|
|
if (detour.Actor) agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(detour.Actor));
|
|
if (!FriendlyActor(detour.Actor) || !agent || agent.m_UtilityComponent != detour.Utility || now >= detour.Expires || m_Mines.IsEmpty() || !detour.Group || ERR_Actors.Group(detour.Actor) != detour.Group || detour.Group.GetCurrentWaypoint() != detour.Waypoint)
|
|
{ detour.Clear(); m_Detours.Remove(i); continue; }
|
|
vector start = detour.Actor.GetOrigin();
|
|
ERR_KnownMine obstruction = Blocking(start, detour.Goal, 10);
|
|
if (!obstruction)
|
|
{
|
|
// Native individual moves can be cancelled when the avoidance action wins.
|
|
// Finish the captured move explicitly; never lose an engineer/HQ foot order
|
|
// merely because its route needed a short detour. A new group order cancels.
|
|
if (!detour.ResumeMove || vector.DistanceXZ(start, detour.Goal) < 3)
|
|
{ detour.Clear(); m_Detours.Remove(i); continue; }
|
|
if (!detour.FinalLeg)
|
|
{
|
|
detour.Clear(); detour.FinalLeg = true; detour.Step = detour.Goal;
|
|
detour.Action = new SCR_AIMoveIndividuallyBehavior(detour.Utility, null, detour.Goal, 1100, 0, null, 1);
|
|
detour.Utility.AddAction(detour.Action);
|
|
}
|
|
continue;
|
|
}
|
|
if (now >= detour.NextStep && (detour.FinalLeg || vector.DistanceXZ(start, detour.Step) < 4 || Blocking(start, start, 7))) Steer(detour, obstruction);
|
|
}
|
|
if (m_Mines.IsEmpty()) { GetGame().GetCallqueue().Remove(Tick); m_Running = false; return; }
|
|
m_Near.Clear();
|
|
foreach (ERR_KnownMine mine : m_Mines)
|
|
GetGame().GetWorld().QueryEntitiesBySphere(mine.Position, 22, Collect, queryFlags: EQueryEntitiesFlags.DYNAMIC);
|
|
foreach (IEntity actor : m_Near)
|
|
{
|
|
bool busy;
|
|
foreach (ERR_MineDetour existing : m_Detours) if (existing.Actor == actor) { busy = true; break; }
|
|
if (busy || m_Detours.Count() >= 64) continue;
|
|
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));
|
|
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
|
vector start = actor.GetOrigin(), goal = start;
|
|
SCR_AIMoveBehaviorBase movement = SCR_AIMoveBehaviorBase.Cast(agent.m_UtilityComponent.GetCurrentAction());
|
|
if (movement) goal = movement.m_vPosition.m_Value;
|
|
else
|
|
{
|
|
Physics physics = actor.GetPhysics();
|
|
if (physics) goal = start + physics.GetVelocity() * 4;
|
|
}
|
|
ERR_KnownMine obstruction = Blocking(start, goal, 10);
|
|
if (!obstruction) continue;
|
|
ERR_MineDetour detour = new ERR_MineDetour();
|
|
detour.Actor = actor; detour.Utility = agent.m_UtilityComponent; detour.Goal = goal; detour.Expires = now + 45000;
|
|
detour.Group = ERR_Actors.Group(actor);
|
|
if (!detour.Group) continue;
|
|
detour.Waypoint = detour.Group.GetCurrentWaypoint(); detour.ResumeMove = movement != null;
|
|
m_Detours.Insert(detour); Steer(detour, obstruction);
|
|
}
|
|
}
|
|
void Stop()
|
|
{
|
|
m_Stopped = true;
|
|
if (GetGame()) GetGame().GetCallqueue().Remove(Tick);
|
|
foreach (ERR_MineDetour detour : m_Detours) detour.Clear();
|
|
m_Detours.Clear(); m_Mines.Clear(); m_Near.Clear();
|
|
}
|
|
}
|