From 88f5cbc92292f5cb9195990a7e3761e3f661dfe8 Mon Sep 17 00:00:00 2001 From: Fabio Pedretti Date: Sat, 17 Oct 2026 23:26:35 +0200 Subject: [PATCH] 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. --- .../public/simulation/ai/petra/defenseArmy.js | 111 ++++++++++++------ 1 file changed, 78 insertions(+), 33 deletions(-) diff --git a/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js b/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js index f25e058834..3438e5f3b4 100644 --- a/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js +++ b/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js @@ -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);