165 lines
6.6 KiB
C
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;
|
|
}
|
|
}
|