Compute the data about the foes once per army and turn in DefenseArmy.assignUnit

assignUnit looped over all the foes of the army for every unit assigned
and, for each (unit, foe) pair, fetched the foe entity and its position,
tested its class, read its UnitAI order data (up to four times) and, when
the foe was close to saturation, tested the Hero and siege classes again.
None of this depends on the unit being assigned.

- Add the module function getFoeInfo(), which gathers once for all the
  foes of the army the entity, its position, the unit it is attacking (if
  any) and, lazily, whether it is a hero or a siege unit and its access
  index. The army keeps the result in foeInfo, which is reset when a foe
  is added, removed or renamed, and at the beginning of update(), as
  positions and orders change from one turn to the next.
- assignUnit reads that table instead of querying the entities. The foes
  are still visited in the same order and with the same checks, so the
  chosen foe is unchanged. assignedAgainst is still read live, as it
  changes while units are assigned.
- Read ent.position() only once.
This commit is contained in:
Fabio Pedretti authored and Phosit committed 2026-10-21 11:42:19 +02:00
1 parent 61534d63a1
commit 88f5cbc922
1 file changed
+78 -33
@@ -4,6 +4,38 @@ import { allowCapture, getLandAccess, getMaxStrength, isSiegeUnit, returnResourc
import { TransportPlan } from "simulation/ai/petra/transportPlan.js";
import { Worker } from "simulation/ai/petra/worker.js";
/**
* Data about the foes needed by DefenseArmy.assignUnit. It depends neither on the unit to assign
* nor on the units already assigned, so it can be computed once and reused for all of them.
* The order of foeEntities is kept as it matters for the choice made in assignUnit.
*/
function getFoeInfo(gameState, foeEntities)
{
const info = [];
for (const id of foeEntities)
{
const eEnt = gameState.getEntityById(id);
if (!eEnt)
continue;
const position = eEnt.position();
if (!position) // probably can't happen.
continue;
// The id of the unit this foe is attacking, if any.
let attackTarget;
if (eEnt.hasClass("Unit"))
{
const orderData = eEnt.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
attackTarget = orderData[0].target;
}
// "heavy" and "access" are filled lazily.
info.push({ "id": id, "ent": eEnt, "position": position, "attackTarget": attackTarget,
"heavy": undefined, "access": undefined });
}
return info;
}
/**
* Armies used by the defense manager.
* An army is a collection of own entities and enemy entities.
@@ -34,6 +66,9 @@ export class DefenseArmy
this.assignedAgainst = {};
// who we assigned against, for quick removal.
this.assignedTo = {};
// Cache of the data about the foes used by assignUnit, see getFoeInfo.
// It is reset when the foes change and at the beginning of each update.
this.foeInfo = undefined;
this.foeEntities = [];
this.foeStrength = 0;
@@ -69,6 +104,7 @@ export class DefenseArmy
return false;
this.foeEntities.push(enemyId);
this.foeInfo = undefined;
this.assignedAgainst[enemyId] = [];
this.positionLastUpdate = 0;
this.evaluateStrength(ent);
@@ -88,6 +124,7 @@ export class DefenseArmy
return false;
this.foeEntities.splice(idx, 1);
this.foeInfo = undefined;
this.assignedAgainst[enemyId] = undefined;
for (const to in this.assignedTo)
@@ -304,72 +341,76 @@ export class DefenseArmy
// we'll also override any previous assignment
const ent = gameState.getEntityById(entID);
if (!ent || !ent.position())
if (!ent)
return false;
const pos = ent.position();
if (!pos)
return false;
// try to return its resources, and if any, the attack order will be queued
const queued = returnResources(gameState, ent);
let idMin;
let foeMin;
let distMin;
let idMinAll;
let foeMinAll;
let distMinAll;
for (const id of this.foeEntities)
if (this.foeInfo === undefined)
this.foeInfo = getFoeInfo(gameState, this.foeEntities);
for (const foe of this.foeInfo)
{
const eEnt = gameState.getEntityById(id);
if (!eEnt || !eEnt.position()) // probably can't happen.
continue;
const eEnt = foe.ent;
if (!ent.canAttackTarget(eEnt, allowCapture(gameState, ent, eEnt)))
continue;
if (eEnt.hasClass("Unit") && eEnt.unitAIOrderData() && eEnt.unitAIOrderData().length &&
eEnt.unitAIOrderData()[0].target && eEnt.unitAIOrderData()[0].target == entID)
if (foe.attackTarget === entID)
{ // being attacked >>> target the unit
idMin = id;
foeMin = foe;
break;
}
// already enough units against it
if (this.assignedAgainst[id].length > 8 ||
this.assignedAgainst[id].length > 5 && !eEnt.hasClass("Hero") && !isSiegeUnit(eEnt))
const nAssigned = this.assignedAgainst[foe.id].length;
if (nAssigned > 8)
continue;
const dist = SquareVectorDistance(ent.position(), eEnt.position());
if (idMinAll === undefined || dist < distMinAll)
if (nAssigned > 5)
{
idMinAll = id;
if (foe.heavy === undefined)
foe.heavy = !!(eEnt.hasClass("Hero") || isSiegeUnit(eEnt));
if (!foe.heavy)
continue;
}
const dist = SquareVectorDistance(pos, foe.position);
if (foeMinAll === undefined || dist < distMinAll)
{
foeMinAll = foe;
distMinAll = dist;
}
if (this.assignedAgainst[id].length > 2)
if (nAssigned > 2)
continue;
if (idMin === undefined || dist < distMin)
if (foeMin === undefined || dist < distMin)
{
idMin = id;
foeMin = foe;
distMin = dist;
}
}
let idFoe;
if (idMin !== undefined)
idFoe = idMin;
else if (idMinAll !== undefined)
idFoe = idMinAll;
else
const chosen = foeMin !== undefined ? foeMin : foeMinAll;
if (chosen === undefined)
return false;
const ownIndex = getLandAccess(gameState, ent);
const foeEnt = gameState.getEntityById(idFoe);
const foePosition = foeEnt.position();
const foeIndex = gameState.ai.accessibility.getAccessValue(foePosition);
if (chosen.access === undefined)
chosen.access = gameState.ai.accessibility.getAccessValue(chosen.position);
const foeIndex = chosen.access;
if (ownIndex == foeIndex || ent.hasClass("Ship"))
{
this.assignedTo[entID] = idFoe;
this.assignedAgainst[idFoe].push(entID);
ent.attack(idFoe, allowCapture(gameState, ent, foeEnt), queued);
this.assignedTo[entID] = chosen.id;
this.assignedAgainst[chosen.id].push(entID);
ent.attack(chosen.id, allowCapture(gameState, ent, chosen.ent), queued);
}
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition);
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, chosen.position);
return true;
}
@@ -527,6 +568,7 @@ export class DefenseArmy
continue;
const idx = this.foeEntities.indexOf(evt.entity);
this.foeEntities[idx] = evt.newentity;
this.foeInfo = undefined;
this.assignedAgainst[evt.newentity] = this.assignedAgainst[evt.entity];
this.assignedAgainst[evt.entity] = undefined;
for (const to in this.assignedTo)
@@ -573,6 +615,9 @@ export class DefenseArmy
update(gameState)
{
// The foes may have moved or changed their orders since the previous turn.
this.foeInfo = undefined;
for (const entId of this.ownEntities)
{
const ent = gameState.getEntityById(entId);