Files
EscapistsReintroduced/addons/ai/Scripts/Game/ERR/ERR_HQPursuit.c
T

165 lines
6.6 KiB
C

enum ERR_HQPursuitRole
{
SEARCH,
INTERCEPT,
CONTAIN
}
// Pure decisions over delivered belief snapshots and HQ's own physical forces.
// No player/entity reads, spawning, timers, waypoints or independent assignments.
class ERR_HQPursuitRules
{
static bool Finite(float value)
{
return value == value && value > -1e20 && value < 1e20;
}
static bool PositionValid(vector value)
{
return Finite(value[0]) && Finite(value[1]) && Finite(value[2]);
}
static int Limit(int configured) { return Math.Clamp(configured, 0, 3); }
static bool Actionable(float confidence, float ageSeconds, float uncertainty)
{
return Finite(confidence) && Finite(ageSeconds) && Finite(uncertainty)
&& confidence >= 0.15 && confidence <= 1 && ageSeconds >= 0
&& uncertainty >= 0 && uncertainty <= 2000;
}
static bool Heading(vector velocity, bool reported)
{
if (!reported || !PositionValid(velocity)) return false;
velocity[1] = 0;
float speed = velocity.Length();
return speed >= 0.5 && speed <= 70;
}
static ERR_HQPursuitRole Role(float confidence, float ageSeconds, float uncertainty,
bool hasHeading, int slot)
{
// A stale/broad/weak track authorizes a search, never a precision interception.
if (!Actionable(confidence, ageSeconds, uncertainty) || !hasHeading || slot < 0 || slot > 2)
return ERR_HQPursuitRole.SEARCH;
if (confidence < 0.65 || ageSeconds > 45 || uncertainty > 150) return ERR_HQPursuitRole.SEARCH;
if (slot == 0) return ERR_HQPursuitRole.INTERCEPT;
if (slot == 2 && confidence >= 0.8 && uncertainty <= 100) return ERR_HQPursuitRole.CONTAIN;
return ERR_HQPursuitRole.SEARCH;
}
static float LifetimeSeconds(ERR_HQPursuitRole role)
{
if (role == ERR_HQPursuitRole.INTERCEPT) return 60;
if (role == ERR_HQPursuitRole.CONTAIN) return 90;
return 120;
}
static float TaskLifetimeSeconds(ERR_HQPursuitRole role, float distanceMetres)
{
if (!Finite(distanceMetres) || distanceMetres < 0) return 0;
if (role != ERR_HQPursuitRole.SEARCH) return LifetimeSeconds(role);
// Existing infantry must have time to travel and search. This is an absolute
// order lifetime, not permission to refresh an old task indefinitely.
return Math.Clamp(distanceMetres / 3 + 45, 120, 900);
}
static bool CanCommit(bool physical, bool eligible, bool busy, bool homeGuard,
int homeAvailable, int assigned, int configured)
{
if (!physical || !eligible || busy || assigned < 0 || assigned >= Limit(configured)) return false;
// Count only uncommitted physical home groups; pending radio orders spend one
// immediately. Mobile patrols may leave; at least one home group must remain.
return !homeGuard || homeAvailable > 1;
}
static int Bearing(vector forcePosition, vector beliefPosition)
{
if (!PositionValid(forcePosition) || !PositionValid(beliefPosition)) return -1;
vector offset = forcePosition - beliefPosition;
if (Math.AbsFloat(offset[0]) > Math.AbsFloat(offset[2]))
{
if (offset[0] >= 0) return 1;
return 3;
}
if (offset[2] >= 0) return 0;
return 2;
}
static bool BearingAllowed(int bearing, int first, int second)
{
if (bearing < 0 || bearing > 3 || first < -1 || first > 3 || second < -1 || second > 3)
return false;
if (first == -1 && second != -1) return false;
// At most two occupied cardinal approach sectors; never close a full ring.
return first == -1 || second == -1 || second == first || bearing == first || bearing == second;
}
static vector Direction(vector velocity, bool hasHeading)
{
if (!Heading(velocity, hasHeading)) return "0 0 1";
velocity[1] = 0;
return velocity.Normalized();
}
static vector Objective(vector position, vector velocity, bool hasHeading, float confidence,
float ageSeconds, float uncertainty, ERR_HQPursuitRole role, int slot)
{
if (!PositionValid(position)) return "0 0 0";
bool directionKnown = Heading(velocity, hasHeading);
// Callers pass the memory's already predicted position. Do not extrapolate
// report age a second time; the small future lead is independently bounded.
ERR_HQPursuitRole permitted = Role(confidence, ageSeconds, uncertainty, directionKnown, slot);
if (role != permitted) role = ERR_HQPursuitRole.SEARCH;
vector forward = Direction(velocity, directionKnown);
vector right = Vector(forward[2], 0, -forward[0]);
float radius = 40;
if (Finite(uncertainty)) radius = Math.Clamp(uncertainty, 40, 250);
vector offset;
if (role == ERR_HQPursuitRole.INTERCEPT)
{
velocity[1] = 0;
float horizon = Math.Clamp(15 - ageSeconds * 0.2, 3, 15);
float lead = Math.Clamp(velocity.Length() * horizon, 60, 200);
offset = forward * lead;
}
else if (role == ERR_HQPursuitRole.CONTAIN)
offset = forward * 80 + right * Math.Clamp(radius, 80, 150);
else
{
// Distinct search sectors within the bounded uncertainty area. HQ/memory
// owns sweep history; slot is a supplied sector, not an independent clock.
int sector = slot % 4;
if (sector < 0) sector += 4;
if (sector == 0) offset = forward * radius;
else if (sector == 1) offset = right * radius;
else if (sector == 2) offset = right * -radius;
else offset = forward * -radius;
}
if (offset.Length() > 300) offset = offset.Normalized() * 300;
return position + offset;
}
// Lower scores win. A negative score rejects malformed geometry. Travel cost
// dominates; direction penalties avoid ordering trailing units to overtake the
// contact or sending a far-side guard across it to contain the other flank.
static float Score(ERR_HQPursuitRole role, vector beliefPosition, vector velocity,
vector forcePosition, vector objective)
{
if (!PositionValid(beliefPosition) || !PositionValid(forcePosition) || !PositionValid(objective))
return -1;
vector forward = Direction(velocity, true);
vector offset = forcePosition - beliefPosition;
offset[1] = 0;
float cost = vector.DistanceXZ(forcePosition, objective);
if (role != ERR_HQPursuitRole.SEARCH && cost > 3 * (LifetimeSeconds(role) - 10)) return -1;
if (role == ERR_HQPursuitRole.INTERCEPT)
{
float ahead = vector.Dot(offset, forward);
if (ahead < 0) cost += Math.Min(-ahead, 1000);
}
else if (role == ERR_HQPursuitRole.CONTAIN)
{
vector right = Vector(forward[2], 0, -forward[0]);
float side = vector.Dot(offset, right);
float intendedSide = vector.Dot(objective - beliefPosition, right);
if (side * intendedSide < 0) cost += Math.Min(Math.AbsFloat(side), 1000);
}
return cost;
}
static bool Better(float score, int id, float bestScore, int bestId)
{
if (!Finite(score) || score < 0 || id <= 0) return false;
if (bestId <= 0 || bestScore < 0) return true;
if (score != bestScore) return score < bestScore;
return id < bestId;
}
}