Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 27731) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 27732) @@ -1,955 +1,955 @@ PETRA.DefenseManager = function(Config) { // Array of "army" Objects. this.armies = []; this.Config = Config; this.targetList = []; this.armyMergeSize = this.Config.Defense.armyMergeSize; // Stats on how many enemies are currently attacking our allies // this.attackingArmies[enemy][ally] = number of enemy armies inside allied territory // this.attackingUnits[enemy][ally] = number of enemy units not in armies inside allied territory // this.attackedAllies[ally] = number of enemies attacking the ally this.attackingArmies = {}; this.attackingUnits = {}; this.attackedAllies = {}; }; PETRA.DefenseManager.prototype.update = function(gameState, events) { Engine.ProfileStart("Defense Manager"); this.territoryMap = gameState.ai.HQ.territoryMap; this.checkEvents(gameState, events); // Check if our potential targets are still valid. for (let i = 0; i < this.targetList.length; ++i) { let target = gameState.getEntityById(this.targetList[i]); if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) this.targetList.splice(i--, 1); } // Count the number of enemies attacking our allies in the previous turn. // We'll be more cooperative if several enemies are attacking him simultaneously. this.attackedAllies = {}; let attackingArmies = clone(this.attackingArmies); for (let enemy in this.attackingUnits) { if (!this.attackingUnits[enemy]) continue; for (let ally in this.attackingUnits[enemy]) { if (this.attackingUnits[enemy][ally] < 8) continue; if (attackingArmies[enemy] === undefined) attackingArmies[enemy] = {}; if (attackingArmies[enemy][ally] === undefined) attackingArmies[enemy][ally] = 0; attackingArmies[enemy][ally] += 1; } } for (let enemy in attackingArmies) { for (let ally in attackingArmies[enemy]) { if (this.attackedAllies[ally] === undefined) this.attackedAllies[ally] = 0; this.attackedAllies[ally] += 1; } } this.checkEnemyArmies(gameState); this.checkEnemyUnits(gameState); this.assignDefenders(gameState); Engine.ProfileStop(); }; PETRA.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default") { if (type == "default") { for (let army of this.armies) if (army.getType() == type && army.addFoe(gameState, entityID)) return; } this.armies.push(new PETRA.DefenseArmy(gameState, [entityID], type)); }; PETRA.DefenseManager.prototype.getArmy = function(partOfArmy) { return this.armies.find(army => army.ID == partOfArmy); }; PETRA.DefenseManager.prototype.isDangerous = function(gameState, entity) { if (!entity.position()) return false; let territoryOwner = this.territoryMap.getOwner(entity.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) return false; // Check if the entity is trying to build a new base near our buildings, // and if yes, add this base in our target list. if (entity.unitAIState() && entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING") { let targetId = entity.unitAIOrderData()[0].target; if (this.targetList.indexOf(targetId) != -1) return true; let target = gameState.getEntityById(targetId); if (target) { let isTargetEnemy = gameState.isPlayerEnemy(target.owner()); if (isTargetEnemy && territoryOwner == PlayerID) { if (target.hasClass("Structure")) this.targetList.push(targetId); return true; } else if (isTargetEnemy && target.hasClass("CivCentre")) { let myBuildings = gameState.getOwnStructures(); for (let building of myBuildings.values()) { if (building.foundationProgress() == 0) continue; if (API3.SquareVectorDistance(building.position(), entity.position()) > 30000) continue; this.targetList.push(targetId); return true; } } } } if (entity.attackTypes() === undefined || entity.hasClass("Support")) return false; let dist2Min = 6000; // TODO the 30 is to take roughly into account the structure size in following checks. Can be improved. if (entity.attackTypes().indexOf("Ranged") != -1) dist2Min = (entity.attackRange("Ranged").max + 30) * (entity.attackRange("Ranged").max + 30); for (let targetId of this.targetList) { let target = gameState.getEntityById(targetId); // The enemy base is either destroyed or built. if (!target || !target.position()) continue; if (API3.SquareVectorDistance(target.position(), entity.position()) < dist2Min) return true; } let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0) continue; let cooperation = this.GetCooperationLevel(cc.owner()); if (cooperation < 0.3 || cooperation < 0.6 && !!cc.foundationProgress()) continue; if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min) return true; } for (let building of gameState.getOwnStructures().values()) { if (building.foundationProgress() == 0 || API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min) continue; if (!this.territoryMap.isBlinking(building.position()) || gameState.ai.HQ.isDefendable(building)) return true; } if (gameState.isPlayerMutualAlly(territoryOwner)) { // If ally attacked by more than 2 enemies, help him not only for cc but also for structures. if (territoryOwner != PlayerID && this.attackedAllies[territoryOwner] && this.attackedAllies[territoryOwner] > 1 && this.GetCooperationLevel(territoryOwner) > 0.7) { for (let building of gameState.getAllyStructures(territoryOwner).values()) { if (building.foundationProgress() == 0 || API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min) continue; if (!this.territoryMap.isBlinking(building.position())) return true; } } // Update the number of enemies attacking this ally. let enemy = entity.owner(); if (this.attackingUnits[enemy] === undefined) this.attackingUnits[enemy] = {}; if (this.attackingUnits[enemy][territoryOwner] === undefined) this.attackingUnits[enemy][territoryOwner] = 0; this.attackingUnits[enemy][territoryOwner] += 1; } return false; }; PETRA.DefenseManager.prototype.checkEnemyUnits = function(gameState) { const nbPlayers = gameState.sharedScript.playersData.length; let i = gameState.ai.playedTurn % nbPlayers; this.attackingUnits[i] = undefined; if (i == PlayerID) { if (!this.armies.length) { // Check if we can recover capture points from any of our notdecaying structures. for (let ent of gameState.getOwnStructures().values()) { if (ent.decaying()) continue; let capture = ent.capturePoints(); if (capture === undefined) continue; let lost = 0; for (let j = 0; j < capture.length; ++j) if (gameState.isPlayerEnemy(j)) lost += capture[j]; if (lost < Math.ceil(0.25 * capture[i])) continue; this.makeIntoArmy(gameState, ent.id(), "capturing"); break; } } return; } else if (!gameState.isPlayerEnemy(i)) return; for (let ent of gameState.getEnemyUnits(i).values()) { if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) continue; // Keep animals attacking us or our allies. if (ent.hasClass("Animal")) { if (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT") continue; let orders = ent.unitAIOrderData(); if (!orders || !orders.length || !orders[0].target) continue; let target = gameState.getEntityById(orders[0].target); if (!target || !gameState.isPlayerAlly(target.owner())) continue; } // TODO what to do for ships ? if (ent.hasClasses(["Ship", "Trader"])) continue; // Check if unit is dangerous "a priori". if (this.isDangerous(gameState, ent)) this.makeIntoArmy(gameState, ent.id()); } if (i != 0 || this.armies.length > 1 || !gameState.ai.HQ.hasActiveBase()) return; // Look for possible gaia buildings inside our territory (may happen when enemy resign or after structure decay) // and attack it only if useful (and capturable) or dangereous. for (let ent of gameState.getEnemyStructures(i).values()) { if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) continue; if (!ent.capturePoints() && !ent.hasDefensiveFire()) continue; let owner = this.territoryMap.getOwner(ent.position()); if (owner == PlayerID) this.makeIntoArmy(gameState, ent.id(), "capturing"); } }; PETRA.DefenseManager.prototype.checkEnemyArmies = function(gameState) { for (let i = 0; i < this.armies.length; ++i) { let army = this.armies[i]; // This returns a list of IDs: the units that broke away from the army for being too far. let breakaways = army.update(gameState); // Assume dangerosity. for (let breaker of breakaways) this.makeIntoArmy(gameState, breaker); if (army.getState() == 0) { if (army.getType() == "default") this.switchToAttack(gameState, army); army.clear(gameState); this.armies.splice(i--, 1); } } // Check if we can't merge it with another. for (let i = 0; i < this.armies.length - 1; ++i) { let army = this.armies[i]; if (army.getType() != "default") continue; for (let j = i+1; j < this.armies.length; ++j) { let otherArmy = this.armies[j]; if (otherArmy.getType() != "default" || API3.SquareVectorDistance(army.foePosition, otherArmy.foePosition) > this.armyMergeSize) continue; // No need to clear here. army.merge(gameState, otherArmy); this.armies.splice(j--, 1); } } if (gameState.ai.playedTurn % 5 != 0) return; // Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base). this.attackingArmies = {}; for (let i = 0; i < this.armies.length; ++i) { let army = this.armies[i]; army.recalculatePosition(gameState); let owner = this.territoryMap.getOwner(army.foePosition); if (!gameState.isPlayerEnemy(owner)) { if (gameState.isPlayerMutualAlly(owner)) { // Update the number of enemies attacking this ally. for (let id of army.foeEntities) { let ent = gameState.getEntityById(id); if (!ent) continue; let enemy = ent.owner(); if (this.attackingArmies[enemy] === undefined) this.attackingArmies[enemy] = {}; if (this.attackingArmies[enemy][owner] === undefined) this.attackingArmies[enemy][owner] = 0; this.attackingArmies[enemy][owner] += 1; break; } } continue; } // Enemy army back in its territory. else if (owner != 0) { army.clear(gameState); this.armies.splice(i--, 1); continue; } // Army in neutral territory. // TODO check smaller distance with all our buildings instead of only ccs with big distance. let stillDangerous = false; let bases = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let base of bases.values()) { if (!gameState.isEntityAlly(base)) continue; let cooperation = this.GetCooperationLevel(base.owner()); if (cooperation < 0.3 && !gameState.isEntityOwn(base)) continue; if (API3.SquareVectorDistance(base.position(), army.foePosition) > 40000) continue; if(this.Config.debug > 1) API3.warn("army in neutral territory, but still near one of our CC"); stillDangerous = true; break; } if (stillDangerous) continue; // Need to also check docks because of oversea bases. for (let dock of gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).values()) { if (API3.SquareVectorDistance(dock.position(), army.foePosition) > 10000) continue; stillDangerous = true; break; } if (stillDangerous) continue; if (army.getType() == "default") this.switchToAttack(gameState, army); army.clear(gameState); this.armies.splice(i--, 1); } }; PETRA.DefenseManager.prototype.assignDefenders = function(gameState) { if (!this.armies.length) return; let armiesNeeding = []; // Let's add defenders. for (let army of this.armies) { let needsDef = army.needsDefenders(gameState); if (needsDef === false) continue; let armyAccess; for (let entId of army.foeEntities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.position()) continue; armyAccess = PETRA.getLandAccess(gameState, ent); break; } if (!armyAccess) API3.warn(" Petra error: attacking army " + army.ID + " without access"); army.recalculatePosition(gameState); armiesNeeding.push({ "army": army, "access": armyAccess, "need": needsDef }); } if (!armiesNeeding.length) return; // Let's get our potential units. let potentialDefenders = []; gameState.getOwnUnits().forEach(function(ent) { if (!ent.position()) return; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return; if (ent.hasClass("Support") || ent.attackTypes() === undefined) return; if (ent.hasClasses(["StoneThrower", "Support", "FishingBoat"])) return; if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return; if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id())) return; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1) { let subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole && (subrole === PETRA.Worker.SUBROLE_COMPLETING || subrole === PETRA.Worker.SUBROLE_WALKING || subrole === PETRA.Worker.SUBROLE_ATTACKING)) return; } potentialDefenders.push(ent.id()); }); for (let ipass = 0; ipass < 2; ++ipass) { // First pass only assign defenders with the right access. // Second pass assign all defenders. // TODO could sort them by distance. let backup = 0; for (let i = 0; i < potentialDefenders.length; ++i) { let ent = gameState.getEntityById(potentialDefenders[i]); if (!ent || !ent.position()) continue; let aMin; let distMin; let access = ipass == 0 ? PETRA.getLandAccess(gameState, ent) : undefined; for (let a = 0; a < armiesNeeding.length; ++a) { if (access && armiesNeeding[a].access != access) continue; // Do not assign defender if it cannot attack at least part of the attacking army. if (!armiesNeeding[a].army.foeEntities.some(eEnt => { let eEntID = gameState.getEntityById(eEnt); return ent.canAttackTarget(eEntID, PETRA.allowCapture(gameState, ent, eEntID)); })) continue; let dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a].army.foePosition); if (aMin !== undefined && dist > distMin) continue; aMin = a; distMin = dist; } // If outside our territory (helping an ally or attacking a cc foundation) // or if in another access, keep some troops in backup. if (backup < 12 && (aMin == undefined || distMin > 40000 && this.territoryMap.getOwner(armiesNeeding[aMin].army.foePosition) != PlayerID)) { ++backup; potentialDefenders[i] = undefined; continue; } else if (aMin === undefined) continue; armiesNeeding[aMin].need -= PETRA.getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance); armiesNeeding[aMin].army.addOwn(gameState, potentialDefenders[i]); armiesNeeding[aMin].army.assignUnit(gameState, potentialDefenders[i]); potentialDefenders[i] = undefined; if (armiesNeeding[aMin].need <= 0) armiesNeeding.splice(aMin, 1); if (!armiesNeeding.length) return; } } // If shortage of defenders, produce infantry garrisoned in nearest civil center. let armiesPos = []; for (let a = 0; a < armiesNeeding.length; ++a) armiesPos.push(armiesNeeding[a].army.foePosition); gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); }; PETRA.DefenseManager.prototype.abortArmy = function(gameState, army) { army.clear(gameState); for (let i = 0; i < this.armies.length; ++i) { if (this.armies[i].ID != army.ID) continue; this.armies.splice(i, 1); break; } }; /** * If our defense structures are attacked, garrison soldiers inside when possible * and if a support unit is attacked and has less than 55% health, garrison it inside the nearest healing structure * and if a ranged siege unit (not used for defense) is attacked, garrison it in the nearest fortress. * If our hero is attacked with regicide victory condition, the victoryManager will handle it. */ PETRA.DefenseManager.prototype.checkEvents = function(gameState, events) { // Must be called every turn for all armies. for (let army of this.armies) army.checkEvents(gameState, events); // Capture events. for (let evt of events.OwnershipChanged) { if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0) { let ent = gameState.getEntityById(evt.entity); // One of our cc has been captured. if (ent && ent.hasClass("CivCentre")) gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 }); } } let allAttacked = {}; for (let evt of events.Attacked) allAttacked[evt.target] = evt.attacker; for (let evt of events.Attacked) { let target = gameState.getEntityById(evt.target); if (!target || !target.position()) continue; let attacker = gameState.getEntityById(evt.attacker); if (attacker && gameState.isEntityOwn(attacker) && gameState.isEntityEnemy(target) && !attacker.hasClass("Ship") && (!target.hasClass("Structure") || target.attackRange("Ranged"))) { // If enemies are in range of one of our defensive structures, garrison it for arrow multiplier // (enemy non-defensive structure are not considered to stay in sync with garrisonManager). if (attacker.position() && attacker.isGarrisonHolder() && attacker.getArrowMultiplier() && (target.owner() != 0 || !target.hasClass("Unit") || target.unitAIState() && target.unitAIState().split(".")[1] == "COMBAT")) this.garrisonUnitsInside(gameState, attacker, { "attacker": target }); } if (!gameState.isEntityOwn(target)) continue; // If attacked by one of our allies (he must trying to recover capture points), do not react. if (attacker && gameState.isEntityAlly(attacker)) continue; if (attacker && attacker.position() && target.hasClass("FishingBoat")) { let unitAIState = target.unitAIState(); let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; if (target.isIdle() || unitAIStateOrder == "GATHER") { let pos = attacker.position(); let range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25; if (range * range > API3.SquareVectorDistance(pos, target.position())) target.moveToRange(pos[0], pos[1], range, range + 5); } continue; } // TODO integrate other ships later, need to be sure it is accessible. if (target.hasClass("Ship")) continue; // If a building on a blinking tile is attacked, check if it can be defended. // Same thing for a building in an isolated base (not connected to a base with anchor). if (target.hasClass("Structure")) { let base = gameState.ai.HQ.getBaseByID(target.getMetadata(PlayerID, "base")); if (this.territoryMap.isBlinking(target.position()) && !gameState.ai.HQ.isDefendable(target) || !base || gameState.ai.HQ.baseManagers().every(b => !b.anchor || b.accessIndex != base.accessIndex)) { let capture = target.capturePoints(); if (!capture) continue; let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); if (captureRatio > 0.50 && captureRatio < 0.70) target.destroy(); continue; } } // If inside a started attack plan, let the plan deal with this unit. let plan = target.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = gameState.ai.HQ.attackManager.getPlan(plan); if (attack && attack.state != PETRA.AttackPlan.STATE_UNEXECUTED) continue; } // Signal this attacker to our defense manager, except if we are in enemy territory. // TODO treat ship attack. if (attacker && attacker.position() && attacker.getMetadata(PlayerID, "PartOfArmy") === undefined && !attacker.hasClasses(["Structure", "Ship"])) { let territoryOwner = this.territoryMap.getOwner(attacker.position()); if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner)) this.makeIntoArmy(gameState, attacker.id()); } if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined) { let army = this.getArmy(target.getMetadata(PlayerID, "PartOfArmy")); if (army.getType() == "capturing") { let abort = false; // If one of the units trying to capture a structure is attacked, // abort the army so that the unit can defend itself if (army.ownEntities.indexOf(target.id()) != -1) abort = true; else if (army.foeEntities[0] == target.id() && target.owner() == PlayerID) { // else we may be trying to regain some capture point from one of our structure. abort = true; let capture = target.capturePoints(); for (let j = 0; j < capture.length; ++j) { if (!gameState.isPlayerEnemy(j) || capture[j] == 0) continue; abort = false; break; } } if (abort) this.abortArmy(gameState, army); } continue; } // Try to garrison any attacked support unit if low health. if (target.hasClass("Support") && target.healthLevel() < this.Config.garrisonHealthLevel.medium && !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) { this.garrisonAttackedUnit(gameState, target); continue; } // Try to garrison any attacked stone thrower. if (target.hasClass("StoneThrower") && !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) { this.garrisonSiegeUnit(gameState, target); continue; } if (!attacker || !attacker.position()) continue; if (target.isGarrisonHolder() && target.getArrowMultiplier()) this.garrisonUnitsInside(gameState, target, { "attacker": attacker }); if (target.hasClass("Unit") && attacker.hasClass("Unit")) { // Consider whether we should retaliate or continue our task. if (target.hasClass("Support") || target.attackTypes() === undefined) continue; let orderData = target.unitAIOrderData(); let currentTarget = orderData && orderData.length && orderData[0].target ? gameState.getEntityById(orderData[0].target) : undefined; if (currentTarget) { let unitAIState = target.unitAIState(); let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; - if (unitAIStateOrder == "COMBAT" && (currentTarget == attacker.id() || + if (unitAIStateOrder === "COMBAT" && (currentTarget.id() === attacker.id() || !currentTarget.hasClasses(["Structure", "Support"]))) continue; - if (unitAIStateOrder == "REPAIR" && currentTarget.hasDefensiveFire()) + if (unitAIStateOrder === "REPAIR" && currentTarget.hasDefensiveFire()) continue; - if (unitAIStateOrder == "COMBAT" && !PETRA.isSiegeUnit(currentTarget) && + if (unitAIStateOrder === "COMBAT" && !PETRA.isSiegeUnit(currentTarget) && gameState.ai.HQ.capturableTargets.has(orderData[0].target)) { // Take the nearest unit also attacking this structure to help us. let capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target); let minDist; let minEnt; let pos = attacker.position(); capturableTarget.ents.delete(target.id()); for (let entId of capturableTarget.ents) { if (allAttacked[entId]) continue; let ent = gameState.getEntityById(entId); if (!ent || !ent.position() || !ent.canAttackTarget(attacker, PETRA.allowCapture(gameState, ent, attacker))) continue; // Check that the unit is still attacking the structure (since the last played turn). let state = ent.unitAIState(); if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") continue; let entOrderData = ent.unitAIOrderData(); if (!entOrderData || !entOrderData.length || !entOrderData[0].target || entOrderData[0].target != orderData[0].target) continue; let dist = API3.SquareVectorDistance(pos, ent.position()); if (minEnt && dist > minDist) continue; minDist = dist; minEnt = ent; } if (minEnt) { capturableTarget.ents.delete(minEnt.id()); minEnt.attack(attacker.id(), PETRA.allowCapture(gameState, minEnt, attacker)); } } } let allowCapture = PETRA.allowCapture(gameState, target, attacker); if (target.canAttackTarget(attacker, allowCapture)) target.attack(attacker.id(), allowCapture); } } }; PETRA.DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data) { if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) return false; let minGarrison = data.min || target.garrisonMax(); if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) return false; if (data.attacker) { let attackTypes = target.attackTypes(); if (!attackTypes || attackTypes.indexOf("Ranged") == -1) return false; let dist = API3.SquareVectorDistance(data.attacker.position(), target.position()); let range = target.attackRange("Ranged").max; if (dist >= range*range) return false; } let access = PETRA.getLandAccess(gameState, target); let garrisonManager = gameState.ai.HQ.garrisonManager; let garrisonArrowClasses = target.getGarrisonArrowClasses(); const typeGarrison = data.type || PETRA.GarrisonManager.TYPE_PROTECTION; let allowMelee = gameState.ai.HQ.garrisonManager.allowMelee(target); if (allowMelee === undefined) { // Should be kept in sync with garrisonManager to avoid garrisoning-ungarrisoning some units. if (data.attacker) allowMelee = data.attacker.hasClass("Structure") ? data.attacker.attackRange("Ranged") : !PETRA.isSiegeUnit(data.attacker); else allowMelee = true; } let units = gameState.getOwnUnits().filter(ent => { if (!ent.position()) return false; if (!ent.hasClasses(garrisonArrowClasses)) return false; if (typeGarrison !== PETRA.GarrisonManager.TYPE_DECAY && !allowMelee && ent.attackTypes().indexOf("Melee") != -1) return false; if (ent.getMetadata(PlayerID, "transport") !== undefined) return false; let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; if (!army && (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)) return false; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let subrole = ent.getMetadata(PlayerID, "subrole"); // When structure decaying (usually because we've just captured it in enemy territory), also allow units from an attack plan. if (typeGarrison !== PETRA.GarrisonManager.TYPE_DECAY && subrole && (subrole === PETRA.Worker.SUBROLE_COMPLETING || subrole === PETRA.Worker.SUBROLE_WALKING || subrole === PETRA.Worker.SUBROLE_ATTACKING)) return false; } if (PETRA.getLandAccess(gameState, ent) != access) return false; return true; }).filterNearest(target.position()); let ret = false; for (let ent of units.values()) { if (garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) break; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); if (attackPlan) attackPlan.removeUnit(ent, true); } let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; if (army) army.removeOwn(gameState, ent.id()); garrisonManager.garrison(gameState, ent, target, typeGarrison); ret = true; } return ret; }; /** Garrison a attacked siege ranged unit inside the nearest fortress. */ PETRA.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit) { let distmin = Math.min(); let nearest; let unitAccess = PETRA.getLandAccess(gameState, unit); let garrisonManager = gameState.ai.HQ.garrisonManager; for (let ent of gameState.getAllyStructures().values()) { if (!ent.isGarrisonHolder()) continue; if (!unit.hasClasses(ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax()) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) continue; if (PETRA.getLandAccess(gameState, ent) != unitAccess) continue; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) continue; distmin = dist; nearest = ent; } if (nearest) garrisonManager.garrison(gameState, unit, nearest, PETRA.GarrisonManager.TYPE_PROTECTION); return nearest !== undefined; }; /** * Garrison a hurt unit inside a player-owned or allied structure. * If emergency is true, the unit will be garrisoned in the closest possible structure. * Otherwise, it will garrison in the closest healing structure. */ PETRA.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false) { let distmin = Math.min(); let nearest; let unitAccess = PETRA.getLandAccess(gameState, unit); let garrisonManager = gameState.ai.HQ.garrisonManager; for (let ent of gameState.getAllyStructures().values()) { if (!ent.isGarrisonHolder()) continue; if (!emergency && !ent.buffHeal()) continue; if (!unit.hasClasses(ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax() && (!emergency || !ent.garrisoned().length)) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) continue; if (PETRA.getLandAccess(gameState, ent) != unitAccess) continue; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) continue; distmin = dist; nearest = ent; } if (!nearest) return false; if (!emergency) { garrisonManager.garrison(gameState, unit, nearest, PETRA.GarrisonManager.TYPE_PROTECTION); return true; } if (garrisonManager.numberOfGarrisonedSlots(nearest) >= nearest.garrisonMax()) // make room for this ent nearest.unload(nearest.garrisoned()[0]); garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? PETRA.GarrisonManager.TYPE_PROTECTION : PETRA.GarrisonManager.TYPE_EMERGENCY); return true; }; /** * Be more inclined to help an ally attacked by several enemies. */ PETRA.DefenseManager.prototype.GetCooperationLevel = function(ally) { let cooperation = this.Config.personality.cooperative; if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1) cooperation += 0.2 * (this.attackedAllies[ally] - 1); return cooperation; }; /** * Switch a defense army into an attack if needed. */ PETRA.DefenseManager.prototype.switchToAttack = function(gameState, army) { if (!army) return; for (let targetId of this.targetList) { let target = gameState.getEntityById(targetId); if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) continue; let targetAccess = PETRA.getLandAccess(gameState, target); let targetPos = target.position(); for (let entId of army.ownEntities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.position() || PETRA.getLandAccess(gameState, ent) != targetAccess) continue; if (API3.SquareVectorDistance(targetPos, ent.position()) > 14400) continue; gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true }); return; } } }; PETRA.DefenseManager.prototype.Serialize = function() { let properties = { "targetList": this.targetList, "armyMergeSize": this.armyMergeSize, "attackingUnits": this.attackingUnits, "attackingArmies": this.attackingArmies, "attackedAllies": this.attackedAllies }; let armies = []; for (let army of this.armies) armies.push(army.Serialize()); return { "properties": properties, "armies": armies }; }; PETRA.DefenseManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.armies = []; for (let dataArmy of data.armies) { let army = new PETRA.DefenseArmy(gameState, []); army.Deserialize(dataArmy); this.armies.push(army); } };