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; } }