LCOV - code coverage report
Current view: top level - simulation/ai/petra - defenseManager.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 582 0.0 %
Date: 2023-04-02 12:52:40 Functions: 0 23 0.0 %

          Line data    Source code
       1           0 : PETRA.DefenseManager = function(Config)
       2             : {
       3             :         // Array of "army" Objects.
       4           0 :         this.armies = [];
       5           0 :         this.Config = Config;
       6           0 :         this.targetList = [];
       7           0 :         this.armyMergeSize = this.Config.Defense.armyMergeSize;
       8             :         // Stats on how many enemies are currently attacking our allies
       9             :         // this.attackingArmies[enemy][ally] = number of enemy armies inside allied territory
      10             :         // this.attackingUnits[enemy][ally] = number of enemy units not in armies inside allied territory
      11             :         // this.attackedAllies[ally] = number of enemies attacking the ally
      12           0 :         this.attackingArmies = {};
      13           0 :         this.attackingUnits = {};
      14           0 :         this.attackedAllies = {};
      15             : };
      16             : 
      17           0 : PETRA.DefenseManager.prototype.update = function(gameState, events)
      18             : {
      19           0 :         Engine.ProfileStart("Defense Manager");
      20             : 
      21           0 :         this.territoryMap = gameState.ai.HQ.territoryMap;
      22             : 
      23           0 :         this.checkEvents(gameState, events);
      24             : 
      25             :         // Check if our potential targets are still valid.
      26           0 :         for (let i = 0; i < this.targetList.length; ++i)
      27             :         {
      28           0 :                 let target = gameState.getEntityById(this.targetList[i]);
      29           0 :                 if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner()))
      30           0 :                         this.targetList.splice(i--, 1);
      31             :         }
      32             : 
      33             :         // Count the number of enemies attacking our allies in the previous turn.
      34             :         // We'll be more cooperative if several enemies are attacking him simultaneously.
      35           0 :         this.attackedAllies = {};
      36           0 :         let attackingArmies = clone(this.attackingArmies);
      37           0 :         for (let enemy in this.attackingUnits)
      38             :         {
      39           0 :                 if (!this.attackingUnits[enemy])
      40           0 :                         continue;
      41           0 :                 for (let ally in this.attackingUnits[enemy])
      42             :                 {
      43           0 :                         if (this.attackingUnits[enemy][ally] < 8)
      44           0 :                                 continue;
      45           0 :                         if (attackingArmies[enemy] === undefined)
      46           0 :                                 attackingArmies[enemy] = {};
      47           0 :                         if (attackingArmies[enemy][ally] === undefined)
      48           0 :                                 attackingArmies[enemy][ally] = 0;
      49           0 :                         attackingArmies[enemy][ally] += 1;
      50             :                 }
      51             :         }
      52           0 :         for (let enemy in attackingArmies)
      53             :         {
      54           0 :                 for (let ally in attackingArmies[enemy])
      55             :                 {
      56           0 :                         if (this.attackedAllies[ally] === undefined)
      57           0 :                                 this.attackedAllies[ally] = 0;
      58           0 :                         this.attackedAllies[ally] += 1;
      59             :                 }
      60             :         }
      61           0 :         this.checkEnemyArmies(gameState);
      62           0 :         this.checkEnemyUnits(gameState);
      63           0 :         this.assignDefenders(gameState);
      64             : 
      65           0 :         Engine.ProfileStop();
      66             : };
      67             : 
      68           0 : PETRA.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default")
      69             : {
      70           0 :         if (type == "default")
      71             :         {
      72           0 :                 for (let army of this.armies)
      73           0 :                         if (army.getType() == type && army.addFoe(gameState, entityID))
      74           0 :                                 return;
      75             :         }
      76             : 
      77           0 :         this.armies.push(new PETRA.DefenseArmy(gameState, [entityID], type));
      78             : };
      79             : 
      80           0 : PETRA.DefenseManager.prototype.getArmy = function(partOfArmy)
      81             : {
      82           0 :         return this.armies.find(army => army.ID == partOfArmy);
      83             : };
      84             : 
      85           0 : PETRA.DefenseManager.prototype.isDangerous = function(gameState, entity)
      86             : {
      87           0 :         if (!entity.position())
      88           0 :                 return false;
      89             : 
      90           0 :         let territoryOwner = this.territoryMap.getOwner(entity.position());
      91           0 :         if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner))
      92           0 :                 return false;
      93             :         // Check if the entity is trying to build a new base near our buildings,
      94             :         // and if yes, add this base in our target list.
      95           0 :         if (entity.unitAIState() && entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING")
      96             :         {
      97           0 :                 let targetId = entity.unitAIOrderData()[0].target;
      98           0 :                 if (this.targetList.indexOf(targetId) != -1)
      99           0 :                         return true;
     100           0 :                 let target = gameState.getEntityById(targetId);
     101           0 :                 if (target)
     102             :                 {
     103           0 :                         let isTargetEnemy = gameState.isPlayerEnemy(target.owner());
     104           0 :                         if (isTargetEnemy && territoryOwner == PlayerID)
     105             :                         {
     106           0 :                                 if (target.hasClass("Structure"))
     107           0 :                                         this.targetList.push(targetId);
     108           0 :                                 return true;
     109             :                         }
     110           0 :                         else if (isTargetEnemy && target.hasClass("CivCentre"))
     111             :                         {
     112           0 :                                 let myBuildings = gameState.getOwnStructures();
     113           0 :                                 for (let building of myBuildings.values())
     114             :                                 {
     115           0 :                                         if (building.foundationProgress() == 0)
     116           0 :                                                 continue;
     117           0 :                                         if (API3.SquareVectorDistance(building.position(), entity.position()) > 30000)
     118           0 :                                                 continue;
     119           0 :                                         this.targetList.push(targetId);
     120           0 :                                         return true;
     121             :                                 }
     122             :                         }
     123             :                 }
     124             :         }
     125             : 
     126           0 :         if (entity.attackTypes() === undefined || entity.hasClass("Support"))
     127           0 :                 return false;
     128           0 :         let dist2Min = 6000;
     129             :         // TODO the 30 is to take roughly into account the structure size in following checks. Can be improved.
     130           0 :         if (entity.attackTypes().indexOf("Ranged") != -1)
     131           0 :                 dist2Min = (entity.attackRange("Ranged").max + 30) * (entity.attackRange("Ranged").max + 30);
     132             : 
     133           0 :         for (let targetId of this.targetList)
     134             :         {
     135           0 :                 let target = gameState.getEntityById(targetId);
     136             :                 // The enemy base is either destroyed or built.
     137           0 :                 if (!target || !target.position())
     138           0 :                         continue;
     139           0 :                 if (API3.SquareVectorDistance(target.position(), entity.position()) < dist2Min)
     140           0 :                         return true;
     141             :         }
     142             : 
     143           0 :         let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
     144           0 :         for (let cc of ccEnts.values())
     145             :         {
     146           0 :                 if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0)
     147           0 :                         continue;
     148           0 :                 let cooperation = this.GetCooperationLevel(cc.owner());
     149           0 :                 if (cooperation < 0.3 || cooperation < 0.6 && !!cc.foundationProgress())
     150           0 :                         continue;
     151           0 :                 if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min)
     152           0 :                         return true;
     153             :         }
     154             : 
     155           0 :         for (let building of gameState.getOwnStructures().values())
     156             :         {
     157           0 :                 if (building.foundationProgress() == 0 ||
     158             :                     API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min)
     159           0 :                         continue;
     160           0 :                 if (!this.territoryMap.isBlinking(building.position()) || gameState.ai.HQ.isDefendable(building))
     161           0 :                         return true;
     162             :         }
     163             : 
     164           0 :         if (gameState.isPlayerMutualAlly(territoryOwner))
     165             :         {
     166             :                 // If ally attacked by more than 2 enemies, help him not only for cc but also for structures.
     167           0 :                 if (territoryOwner != PlayerID && this.attackedAllies[territoryOwner] &&
     168             :                                                   this.attackedAllies[territoryOwner] > 1 &&
     169             :                                                   this.GetCooperationLevel(territoryOwner) > 0.7)
     170             :                 {
     171           0 :                         for (let building of gameState.getAllyStructures(territoryOwner).values())
     172             :                         {
     173           0 :                                 if (building.foundationProgress() == 0 ||
     174             :                                     API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min)
     175           0 :                                         continue;
     176           0 :                                 if (!this.territoryMap.isBlinking(building.position()))
     177           0 :                                         return true;
     178             :                         }
     179             :                 }
     180             : 
     181             :                 // Update the number of enemies attacking this ally.
     182           0 :                 let enemy = entity.owner();
     183           0 :                 if (this.attackingUnits[enemy] === undefined)
     184           0 :                         this.attackingUnits[enemy] = {};
     185           0 :                 if (this.attackingUnits[enemy][territoryOwner] === undefined)
     186           0 :                         this.attackingUnits[enemy][territoryOwner] = 0;
     187           0 :                 this.attackingUnits[enemy][territoryOwner] += 1;
     188             :         }
     189             : 
     190           0 :         return false;
     191             : };
     192             : 
     193           0 : PETRA.DefenseManager.prototype.checkEnemyUnits = function(gameState)
     194             : {
     195           0 :         const nbPlayers = gameState.sharedScript.playersData.length;
     196           0 :         let i = gameState.ai.playedTurn % nbPlayers;
     197           0 :         this.attackingUnits[i] = undefined;
     198             : 
     199           0 :         if (i == PlayerID)
     200             :         {
     201           0 :                 if (!this.armies.length)
     202             :                 {
     203             :                         // Check if we can recover capture points from any of our notdecaying structures.
     204           0 :                         for (let ent of gameState.getOwnStructures().values())
     205             :                         {
     206           0 :                                 if (ent.decaying())
     207           0 :                                         continue;
     208           0 :                                 let capture = ent.capturePoints();
     209           0 :                                 if (capture === undefined)
     210           0 :                                         continue;
     211           0 :                                 let lost = 0;
     212           0 :                                 for (let j = 0; j < capture.length; ++j)
     213           0 :                                         if (gameState.isPlayerEnemy(j))
     214           0 :                                                 lost += capture[j];
     215           0 :                                 if (lost < Math.ceil(0.25 * capture[i]))
     216           0 :                                         continue;
     217           0 :                                 this.makeIntoArmy(gameState, ent.id(), "capturing");
     218           0 :                                 break;
     219             :                         }
     220             :                 }
     221           0 :                 return;
     222             :         }
     223           0 :         else if (!gameState.isPlayerEnemy(i))
     224           0 :                 return;
     225             : 
     226           0 :         for (let ent of gameState.getEnemyUnits(i).values())
     227             :         {
     228           0 :                 if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined)
     229           0 :                         continue;
     230             : 
     231             :                 // Keep animals attacking us or our allies.
     232           0 :                 if (ent.hasClass("Animal"))
     233             :                 {
     234           0 :                         if (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT")
     235           0 :                                 continue;
     236           0 :                         let orders = ent.unitAIOrderData();
     237           0 :                         if (!orders || !orders.length || !orders[0].target)
     238           0 :                                 continue;
     239           0 :                         let target = gameState.getEntityById(orders[0].target);
     240           0 :                         if (!target || !gameState.isPlayerAlly(target.owner()))
     241           0 :                                 continue;
     242             :                 }
     243             : 
     244             :                 // TODO what to do for ships ?
     245           0 :                 if (ent.hasClasses(["Ship", "Trader"]))
     246           0 :                         continue;
     247             : 
     248             :                 // Check if unit is dangerous "a priori".
     249           0 :                 if (this.isDangerous(gameState, ent))
     250           0 :                         this.makeIntoArmy(gameState, ent.id());
     251             :         }
     252             : 
     253           0 :         if (i != 0 || this.armies.length > 1 || !gameState.ai.HQ.hasActiveBase())
     254           0 :                 return;
     255             :         // Look for possible gaia buildings inside our territory (may happen when enemy resign or after structure decay)
     256             :         // and attack it only if useful (and capturable) or dangereous.
     257           0 :         for (let ent of gameState.getEnemyStructures(i).values())
     258             :         {
     259           0 :                 if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined)
     260           0 :                         continue;
     261           0 :                 if (!ent.capturePoints() && !ent.hasDefensiveFire())
     262           0 :                         continue;
     263           0 :                 let owner = this.territoryMap.getOwner(ent.position());
     264           0 :                 if (owner == PlayerID)
     265           0 :                         this.makeIntoArmy(gameState, ent.id(), "capturing");
     266             :         }
     267             : };
     268             : 
     269           0 : PETRA.DefenseManager.prototype.checkEnemyArmies = function(gameState)
     270             : {
     271           0 :         for (let i = 0; i < this.armies.length; ++i)
     272             :         {
     273           0 :                 let army = this.armies[i];
     274             :                 // This returns a list of IDs: the units that broke away from the army for being too far.
     275           0 :                 let breakaways = army.update(gameState);
     276             :                 // Assume dangerosity.
     277           0 :                 for (let breaker of breakaways)
     278           0 :                         this.makeIntoArmy(gameState, breaker);
     279             : 
     280           0 :                 if (army.getState() == 0)
     281             :                 {
     282           0 :                         if (army.getType() == "default")
     283           0 :                                 this.switchToAttack(gameState, army);
     284           0 :                         army.clear(gameState);
     285           0 :                         this.armies.splice(i--, 1);
     286             :                 }
     287             :         }
     288             :         // Check if we can't merge it with another.
     289           0 :         for (let i = 0; i < this.armies.length - 1; ++i)
     290             :         {
     291           0 :                 let army = this.armies[i];
     292           0 :                 if (army.getType() != "default")
     293           0 :                         continue;
     294           0 :                 for (let j = i+1; j < this.armies.length; ++j)
     295             :                 {
     296           0 :                         let otherArmy = this.armies[j];
     297           0 :                         if (otherArmy.getType() != "default" ||
     298             :                                 API3.SquareVectorDistance(army.foePosition, otherArmy.foePosition) > this.armyMergeSize)
     299           0 :                                 continue;
     300             :                         // No need to clear here.
     301           0 :                         army.merge(gameState, otherArmy);
     302           0 :                         this.armies.splice(j--, 1);
     303             :                 }
     304             :         }
     305             : 
     306           0 :         if (gameState.ai.playedTurn % 5 != 0)
     307           0 :                 return;
     308             :         // Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base).
     309           0 :         this.attackingArmies = {};
     310           0 :         for (let i = 0; i < this.armies.length; ++i)
     311             :         {
     312           0 :                 let army = this.armies[i];
     313           0 :                 army.recalculatePosition(gameState);
     314           0 :                 let owner = this.territoryMap.getOwner(army.foePosition);
     315           0 :                 if (!gameState.isPlayerEnemy(owner))
     316             :                 {
     317           0 :                         if (gameState.isPlayerMutualAlly(owner))
     318             :                         {
     319             :                                 // Update the number of enemies attacking this ally.
     320           0 :                                 for (let id of army.foeEntities)
     321             :                                 {
     322           0 :                                         let ent = gameState.getEntityById(id);
     323           0 :                                         if (!ent)
     324           0 :                                                 continue;
     325           0 :                                         let enemy = ent.owner();
     326           0 :                                         if (this.attackingArmies[enemy] === undefined)
     327           0 :                                                 this.attackingArmies[enemy] = {};
     328           0 :                                         if (this.attackingArmies[enemy][owner] === undefined)
     329           0 :                                                 this.attackingArmies[enemy][owner] = 0;
     330           0 :                                         this.attackingArmies[enemy][owner] += 1;
     331           0 :                                         break;
     332             :                                 }
     333             :                         }
     334           0 :                         continue;
     335             :                 }
     336             :                 // Enemy army back in its territory.
     337           0 :                 else if (owner != 0)
     338             :                 {
     339           0 :                         army.clear(gameState);
     340           0 :                         this.armies.splice(i--, 1);
     341           0 :                         continue;
     342             :                 }
     343             : 
     344             :                 // Army in neutral territory.
     345             :                 // TODO check smaller distance with all our buildings instead of only ccs with big distance.
     346           0 :                 let stillDangerous = false;
     347           0 :                 let bases = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
     348           0 :                 for (let base of bases.values())
     349             :                 {
     350           0 :                         if (!gameState.isEntityAlly(base))
     351           0 :                                 continue;
     352           0 :                         let cooperation = this.GetCooperationLevel(base.owner());
     353           0 :                         if (cooperation < 0.3 && !gameState.isEntityOwn(base))
     354           0 :                                 continue;
     355           0 :                         if (API3.SquareVectorDistance(base.position(), army.foePosition) > 40000)
     356           0 :                                 continue;
     357           0 :                         if(this.Config.debug > 1)
     358           0 :                                 API3.warn("army in neutral territory, but still near one of our CC");
     359           0 :                         stillDangerous = true;
     360           0 :                         break;
     361             :                 }
     362           0 :                 if (stillDangerous)
     363           0 :                         continue;
     364             :                 // Need to also check docks because of oversea bases.
     365           0 :                 for (let dock of gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).values())
     366             :                 {
     367           0 :                         if (API3.SquareVectorDistance(dock.position(), army.foePosition) > 10000)
     368           0 :                                 continue;
     369           0 :                         stillDangerous = true;
     370           0 :                         break;
     371             :                 }
     372           0 :                 if (stillDangerous)
     373           0 :                         continue;
     374             : 
     375           0 :                 if (army.getType() == "default")
     376           0 :                         this.switchToAttack(gameState, army);
     377           0 :                 army.clear(gameState);
     378           0 :                 this.armies.splice(i--, 1);
     379             :         }
     380             : };
     381             : 
     382           0 : PETRA.DefenseManager.prototype.assignDefenders = function(gameState)
     383             : {
     384           0 :         if (!this.armies.length)
     385           0 :                 return;
     386             : 
     387           0 :         let armiesNeeding = [];
     388             :         // Let's add defenders.
     389           0 :         for (let army of this.armies)
     390             :         {
     391           0 :                 let needsDef = army.needsDefenders(gameState);
     392           0 :                 if (needsDef === false)
     393           0 :                         continue;
     394             : 
     395             :                 let armyAccess;
     396           0 :                 for (let entId of army.foeEntities)
     397             :                 {
     398           0 :                         let ent = gameState.getEntityById(entId);
     399           0 :                         if (!ent || !ent.position())
     400           0 :                                 continue;
     401           0 :                         armyAccess = PETRA.getLandAccess(gameState, ent);
     402           0 :                         break;
     403             :                 }
     404           0 :                 if (!armyAccess)
     405           0 :                         API3.warn(" Petra error: attacking army " + army.ID + " without access");
     406           0 :                 army.recalculatePosition(gameState);
     407           0 :                 armiesNeeding.push({ "army": army, "access": armyAccess, "need": needsDef });
     408             :         }
     409             : 
     410           0 :         if (!armiesNeeding.length)
     411           0 :                 return;
     412             : 
     413             :         // Let's get our potential units.
     414           0 :         let potentialDefenders = [];
     415           0 :         gameState.getOwnUnits().forEach(function(ent) {
     416           0 :                 if (!ent.position())
     417           0 :                         return;
     418           0 :                 if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
     419           0 :                         return;
     420           0 :                 if (ent.hasClass("Support") || ent.attackTypes() === undefined)
     421           0 :                         return;
     422           0 :                 if (ent.hasClasses(["StoneThrower", "Support", "FishingBoat"]))
     423           0 :                         return;
     424           0 :                 if (ent.getMetadata(PlayerID, "transport") !== undefined ||
     425             :                     ent.getMetadata(PlayerID, "transporter") !== undefined)
     426           0 :                         return;
     427           0 :                 if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()))
     428           0 :                         return;
     429           0 :                 if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1)
     430             :                 {
     431           0 :                         let subrole = ent.getMetadata(PlayerID, "subrole");
     432           0 :                         if (subrole &&
     433             :                                 (subrole === PETRA.Worker.SUBROLE_COMPLETING || subrole === PETRA.Worker.SUBROLE_WALKING || subrole === PETRA.Worker.SUBROLE_ATTACKING))
     434           0 :                                 return;
     435             :                 }
     436           0 :                 potentialDefenders.push(ent.id());
     437             :         });
     438             : 
     439           0 :         for (let ipass = 0; ipass < 2; ++ipass)
     440             :         {
     441             :                 // First pass only assign defenders with the right access.
     442             :                 // Second pass assign all defenders.
     443             :                 // TODO could sort them by distance.
     444           0 :                 let backup = 0;
     445           0 :                 for (let i = 0; i < potentialDefenders.length; ++i)
     446             :                 {
     447           0 :                         let ent = gameState.getEntityById(potentialDefenders[i]);
     448           0 :                         if (!ent || !ent.position())
     449           0 :                                 continue;
     450             :                         let aMin;
     451             :                         let distMin;
     452           0 :                         let access = ipass == 0 ? PETRA.getLandAccess(gameState, ent) : undefined;
     453           0 :                         for (let a = 0; a < armiesNeeding.length; ++a)
     454             :                         {
     455           0 :                                 if (access && armiesNeeding[a].access != access)
     456           0 :                                         continue;
     457             : 
     458             :                                 // Do not assign defender if it cannot attack at least part of the attacking army.
     459           0 :                                 if (!armiesNeeding[a].army.foeEntities.some(eEnt => {
     460           0 :                                         let eEntID = gameState.getEntityById(eEnt);
     461           0 :                                         return ent.canAttackTarget(eEntID, PETRA.allowCapture(gameState, ent, eEntID));
     462             :                                         }))
     463           0 :                                         continue;
     464             : 
     465           0 :                                 let dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a].army.foePosition);
     466           0 :                                 if (aMin !== undefined && dist > distMin)
     467           0 :                                         continue;
     468           0 :                                 aMin = a;
     469           0 :                                 distMin = dist;
     470             :                         }
     471             : 
     472             :                         // If outside our territory (helping an ally or attacking a cc foundation)
     473             :                         // or if in another access, keep some troops in backup.
     474           0 :                         if (backup < 12 && (aMin == undefined || distMin > 40000 &&
     475             :                                 this.territoryMap.getOwner(armiesNeeding[aMin].army.foePosition) != PlayerID))
     476             :                         {
     477           0 :                                 ++backup;
     478           0 :                                 potentialDefenders[i] = undefined;
     479           0 :                                 continue;
     480             :                         }
     481           0 :                         else if (aMin === undefined)
     482           0 :                                 continue;
     483             : 
     484           0 :                         armiesNeeding[aMin].need -= PETRA.getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance);
     485           0 :                         armiesNeeding[aMin].army.addOwn(gameState, potentialDefenders[i]);
     486           0 :                         armiesNeeding[aMin].army.assignUnit(gameState, potentialDefenders[i]);
     487           0 :                         potentialDefenders[i] = undefined;
     488             : 
     489           0 :                         if (armiesNeeding[aMin].need <= 0)
     490           0 :                                 armiesNeeding.splice(aMin, 1);
     491           0 :                         if (!armiesNeeding.length)
     492           0 :                                 return;
     493             :                 }
     494             :         }
     495             : 
     496             :         // If shortage of defenders, produce infantry garrisoned in nearest civil center.
     497           0 :         let armiesPos = [];
     498           0 :         for (let a = 0; a < armiesNeeding.length; ++a)
     499           0 :                 armiesPos.push(armiesNeeding[a].army.foePosition);
     500           0 :         gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos);
     501             : };
     502             : 
     503           0 : PETRA.DefenseManager.prototype.abortArmy = function(gameState, army)
     504             : {
     505           0 :         army.clear(gameState);
     506           0 :         for (let i = 0; i < this.armies.length; ++i)
     507             :         {
     508           0 :                 if (this.armies[i].ID != army.ID)
     509           0 :                         continue;
     510           0 :                 this.armies.splice(i, 1);
     511           0 :                 break;
     512             :         }
     513             : };
     514             : 
     515             : /**
     516             :  * If our defense structures are attacked, garrison soldiers inside when possible
     517             :  * and if a support unit is attacked and has less than 55% health, garrison it inside the nearest healing structure
     518             :  * and if a ranged siege unit (not used for defense) is attacked, garrison it in the nearest fortress.
     519             :  * If our hero is attacked with regicide victory condition, the victoryManager will handle it.
     520             :  */
     521           0 : PETRA.DefenseManager.prototype.checkEvents = function(gameState, events)
     522             : {
     523             :         // Must be called every turn for all armies.
     524           0 :         for (let army of this.armies)
     525           0 :                 army.checkEvents(gameState, events);
     526             : 
     527             :         // Capture events.
     528           0 :         for (let evt of events.OwnershipChanged)
     529             :         {
     530           0 :                 if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0)
     531             :                 {
     532           0 :                         let ent = gameState.getEntityById(evt.entity);
     533             :                         // One of our cc has been captured.
     534           0 :                         if (ent && ent.hasClass("CivCentre"))
     535           0 :                                 gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 });
     536             :                 }
     537             :         }
     538             : 
     539           0 :         let allAttacked = {};
     540           0 :         for (let evt of events.Attacked)
     541           0 :                 allAttacked[evt.target] = evt.attacker;
     542             : 
     543           0 :         for (let evt of events.Attacked)
     544             :         {
     545           0 :                 let target = gameState.getEntityById(evt.target);
     546           0 :                 if (!target || !target.position())
     547           0 :                         continue;
     548             : 
     549           0 :                 let attacker = gameState.getEntityById(evt.attacker);
     550           0 :                 if (attacker && gameState.isEntityOwn(attacker) && gameState.isEntityEnemy(target) && !attacker.hasClass("Ship") &&
     551             :                    (!target.hasClass("Structure") || target.attackRange("Ranged")))
     552             :                 {
     553             :                         // If enemies are in range of one of our defensive structures, garrison it for arrow multiplier
     554             :                         // (enemy non-defensive structure are not considered to stay in sync with garrisonManager).
     555           0 :                         if (attacker.position() && attacker.isGarrisonHolder() && attacker.getArrowMultiplier() &&
     556             :                             (target.owner() != 0 || !target.hasClass("Unit") ||
     557             :                              target.unitAIState() && target.unitAIState().split(".")[1] == "COMBAT"))
     558           0 :                                 this.garrisonUnitsInside(gameState, attacker, { "attacker": target });
     559             :                 }
     560             : 
     561           0 :                 if (!gameState.isEntityOwn(target))
     562           0 :                         continue;
     563             : 
     564             :                 // If attacked by one of our allies (he must trying to recover capture points), do not react.
     565           0 :                 if (attacker && gameState.isEntityAlly(attacker))
     566           0 :                         continue;
     567             : 
     568           0 :                 if (attacker && attacker.position() && target.hasClass("FishingBoat"))
     569             :                 {
     570           0 :                         let unitAIState = target.unitAIState();
     571           0 :                         let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : "";
     572           0 :                         if (target.isIdle() || unitAIStateOrder == "GATHER")
     573             :                         {
     574           0 :                                 let pos = attacker.position();
     575           0 :                                 let range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25;
     576           0 :                                 if (range * range > API3.SquareVectorDistance(pos, target.position()))
     577           0 :                                         target.moveToRange(pos[0], pos[1], range, range + 5);
     578             :                         }
     579           0 :                         continue;
     580             :                 }
     581             :                 
     582             :                 // TODO integrate other ships later, need to be sure it is accessible.
     583           0 :                 if (target.hasClass("Ship"))
     584           0 :                         continue;
     585             : 
     586             :                 // If a building on a blinking tile is attacked, check if it can be defended.
     587             :                 // Same thing for a building in an isolated base (not connected to a base with anchor).
     588           0 :                 if (target.hasClass("Structure"))
     589             :                 {
     590           0 :                         let base = gameState.ai.HQ.getBaseByID(target.getMetadata(PlayerID, "base"));
     591           0 :                         if (this.territoryMap.isBlinking(target.position()) && !gameState.ai.HQ.isDefendable(target) ||
     592           0 :                             !base || gameState.ai.HQ.baseManagers().every(b => !b.anchor || b.accessIndex != base.accessIndex))
     593             :                         {
     594           0 :                                 let capture = target.capturePoints();
     595           0 :                                 if (!capture)
     596           0 :                                         continue;
     597           0 :                                 let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b);
     598           0 :                                 if (captureRatio > 0.50 && captureRatio < 0.70)
     599           0 :                                         target.destroy();
     600           0 :                                 continue;
     601             :                         }
     602             :                 }
     603             : 
     604             : 
     605             :                 // If inside a started attack plan, let the plan deal with this unit.
     606           0 :                 let plan = target.getMetadata(PlayerID, "plan");
     607           0 :                 if (plan !== undefined && plan >= 0)
     608             :                 {
     609           0 :                         let attack = gameState.ai.HQ.attackManager.getPlan(plan);
     610           0 :                         if (attack && attack.state != PETRA.AttackPlan.STATE_UNEXECUTED)
     611           0 :                                 continue;
     612             :                 }
     613             : 
     614             :                 // Signal this attacker to our defense manager, except if we are in enemy territory.
     615             :                 // TODO treat ship attack.
     616           0 :                 if (attacker && attacker.position() && attacker.getMetadata(PlayerID, "PartOfArmy") === undefined &&
     617             :                         !attacker.hasClasses(["Structure", "Ship"]))
     618             :                 {
     619           0 :                         let territoryOwner = this.territoryMap.getOwner(attacker.position());
     620           0 :                         if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner))
     621           0 :                                 this.makeIntoArmy(gameState, attacker.id());
     622             :                 }
     623             : 
     624           0 :                 if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined)
     625             :                 {
     626           0 :                         let army = this.getArmy(target.getMetadata(PlayerID, "PartOfArmy"));
     627           0 :                         if (army.getType() == "capturing")
     628             :                         {
     629           0 :                                 let abort = false;
     630             :                                 // If one of the units trying to capture a structure is attacked,
     631             :                                 // abort the army so that the unit can defend itself
     632           0 :                                 if (army.ownEntities.indexOf(target.id()) != -1)
     633           0 :                                         abort = true;
     634           0 :                                 else if (army.foeEntities[0] == target.id() && target.owner() == PlayerID)
     635             :                                 {
     636             :                                         // else we may be trying to regain some capture point from one of our structure.
     637           0 :                                         abort = true;
     638           0 :                                         let capture = target.capturePoints();
     639           0 :                                         for (let j = 0; j < capture.length; ++j)
     640             :                                         {
     641           0 :                                                 if (!gameState.isPlayerEnemy(j) || capture[j] == 0)
     642           0 :                                                         continue;
     643           0 :                                                 abort = false;
     644           0 :                                                 break;
     645             :                                         }
     646             :                                 }
     647           0 :                                 if (abort)
     648           0 :                                         this.abortArmy(gameState, army);
     649             :                         }
     650           0 :                         continue;
     651             :                 }
     652             : 
     653             :                 // Try to garrison any attacked support unit if low health.
     654           0 :                 if (target.hasClass("Support") && target.healthLevel() < this.Config.garrisonHealthLevel.medium &&
     655             :                         !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3)
     656             :                 {
     657           0 :                         this.garrisonAttackedUnit(gameState, target);
     658           0 :                         continue;
     659             :                 }
     660             : 
     661             :                 // Try to garrison any attacked stone thrower.
     662           0 :                 if (target.hasClass("StoneThrower") &&
     663             :                         !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3)
     664             :                 {
     665           0 :                         this.garrisonSiegeUnit(gameState, target);
     666           0 :                         continue;
     667             :                 }
     668             : 
     669           0 :                 if (!attacker || !attacker.position())
     670           0 :                         continue;
     671             : 
     672           0 :                 if (target.isGarrisonHolder() && target.getArrowMultiplier())
     673           0 :                         this.garrisonUnitsInside(gameState, target, { "attacker": attacker });
     674             : 
     675           0 :                 if (target.hasClass("Unit") && attacker.hasClass("Unit"))
     676             :                 {
     677             :                         // Consider whether we should retaliate or continue our task.
     678           0 :                         if (target.hasClass("Support") || target.attackTypes() === undefined)
     679           0 :                                 continue;
     680           0 :                         let orderData = target.unitAIOrderData();
     681           0 :                         let currentTarget = orderData && orderData.length && orderData[0].target ?
     682             :                                 gameState.getEntityById(orderData[0].target) : undefined;
     683           0 :                         if (currentTarget)
     684             :                         {
     685           0 :                                 let unitAIState = target.unitAIState();
     686           0 :                                 let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : "";
     687           0 :                                 if (unitAIStateOrder == "COMBAT" && (currentTarget == attacker.id() ||
     688             :                                         !currentTarget.hasClasses(["Structure", "Support"])))
     689           0 :                                         continue;
     690           0 :                                 if (unitAIStateOrder == "REPAIR" && currentTarget.hasDefensiveFire())
     691           0 :                                         continue;
     692           0 :                                 if (unitAIStateOrder == "COMBAT" && !PETRA.isSiegeUnit(currentTarget) &&
     693             :                                     gameState.ai.HQ.capturableTargets.has(orderData[0].target))
     694             :                                 {
     695             :                                         // Take the nearest unit also attacking this structure to help us.
     696           0 :                                         let capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target);
     697             :                                         let minDist;
     698             :                                         let minEnt;
     699           0 :                                         let pos = attacker.position();
     700           0 :                                         capturableTarget.ents.delete(target.id());
     701           0 :                                         for (let entId of capturableTarget.ents)
     702             :                                         {
     703           0 :                                                 if (allAttacked[entId])
     704           0 :                                                         continue;
     705           0 :                                                 let ent = gameState.getEntityById(entId);
     706           0 :                                                 if (!ent || !ent.position() || !ent.canAttackTarget(attacker, PETRA.allowCapture(gameState, ent, attacker)))
     707           0 :                                                         continue;
     708             :                                                 // Check that the unit is still attacking the structure (since the last played turn).
     709           0 :                                                 let state = ent.unitAIState();
     710           0 :                                                 if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT")
     711           0 :                                                         continue;
     712           0 :                                                 let entOrderData = ent.unitAIOrderData();
     713           0 :                                                 if (!entOrderData || !entOrderData.length || !entOrderData[0].target ||
     714             :                                                      entOrderData[0].target != orderData[0].target)
     715           0 :                                                         continue;
     716           0 :                                                 let dist = API3.SquareVectorDistance(pos, ent.position());
     717           0 :                                                 if (minEnt && dist > minDist)
     718           0 :                                                         continue;
     719           0 :                                                 minDist = dist;
     720           0 :                                                 minEnt = ent;
     721             :                                         }
     722           0 :                                         if (minEnt)
     723             :                                         {
     724           0 :                                                 capturableTarget.ents.delete(minEnt.id());
     725           0 :                                                 minEnt.attack(attacker.id(), PETRA.allowCapture(gameState, minEnt, attacker));
     726             :                                         }
     727             :                                 }
     728             :                         }
     729           0 :                         let allowCapture = PETRA.allowCapture(gameState, target, attacker);
     730           0 :                         if (target.canAttackTarget(attacker, allowCapture))
     731           0 :                                 target.attack(attacker.id(), allowCapture);
     732             :                 }
     733             :         }
     734             : };
     735             : 
     736           0 : PETRA.DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data)
     737             : {
     738           0 :         if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints())
     739           0 :                 return false;
     740           0 :         let minGarrison = data.min || target.garrisonMax();
     741           0 :         if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison)
     742           0 :                 return false;
     743           0 :         if (data.attacker)
     744             :         {
     745           0 :                 let attackTypes = target.attackTypes();
     746           0 :                 if (!attackTypes || attackTypes.indexOf("Ranged") == -1)
     747           0 :                         return false;
     748           0 :                 let dist = API3.SquareVectorDistance(data.attacker.position(), target.position());
     749           0 :                 let range = target.attackRange("Ranged").max;
     750           0 :                 if (dist >= range*range)
     751           0 :                         return false;
     752             :         }
     753           0 :         let access = PETRA.getLandAccess(gameState, target);
     754           0 :         let garrisonManager = gameState.ai.HQ.garrisonManager;
     755           0 :         let garrisonArrowClasses = target.getGarrisonArrowClasses();
     756           0 :         const typeGarrison = data.type || PETRA.GarrisonManager.TYPE_PROTECTION;
     757           0 :         let allowMelee = gameState.ai.HQ.garrisonManager.allowMelee(target);
     758           0 :         if (allowMelee === undefined)
     759             :         {
     760             :                 // Should be kept in sync with garrisonManager to avoid garrisoning-ungarrisoning some units.
     761           0 :                 if (data.attacker)
     762           0 :                         allowMelee = data.attacker.hasClass("Structure") ? data.attacker.attackRange("Ranged") : !PETRA.isSiegeUnit(data.attacker);
     763             :                 else
     764           0 :                         allowMelee = true;
     765             :         }
     766           0 :         let units = gameState.getOwnUnits().filter(ent => {
     767           0 :                 if (!ent.position())
     768           0 :                         return false;
     769           0 :                 if (!ent.hasClasses(garrisonArrowClasses))
     770           0 :                         return false;
     771           0 :                 if (typeGarrison !== PETRA.GarrisonManager.TYPE_DECAY && !allowMelee && ent.attackTypes().indexOf("Melee") != -1)
     772           0 :                         return false;
     773           0 :                 if (ent.getMetadata(PlayerID, "transport") !== undefined)
     774           0 :                         return false;
     775           0 :                 let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined;
     776           0 :                 if (!army && (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3))
     777           0 :                         return false;
     778           0 :                 if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0)
     779             :                 {
     780           0 :                         let subrole = ent.getMetadata(PlayerID, "subrole");
     781             :                         // When structure decaying (usually because we've just captured it in enemy territory), also allow units from an attack plan.
     782           0 :                         if (typeGarrison !== PETRA.GarrisonManager.TYPE_DECAY && subrole &&
     783             :                                 (subrole === PETRA.Worker.SUBROLE_COMPLETING || subrole === PETRA.Worker.SUBROLE_WALKING || subrole === PETRA.Worker.SUBROLE_ATTACKING))
     784           0 :                                 return false;
     785             :                 }
     786           0 :                 if (PETRA.getLandAccess(gameState, ent) != access)
     787           0 :                         return false;
     788           0 :                 return true;
     789             :         }).filterNearest(target.position());
     790             : 
     791           0 :         let ret = false;
     792           0 :         for (let ent of units.values())
     793             :         {
     794           0 :                 if (garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison)
     795           0 :                         break;
     796           0 :                 if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0)
     797             :                 {
     798           0 :                         let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan"));
     799           0 :                         if (attackPlan)
     800           0 :                                 attackPlan.removeUnit(ent, true);
     801             :                 }
     802           0 :                 let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined;
     803           0 :                 if (army)
     804           0 :                         army.removeOwn(gameState, ent.id());
     805           0 :                 garrisonManager.garrison(gameState, ent, target, typeGarrison);
     806           0 :                 ret = true;
     807             :         }
     808           0 :         return ret;
     809             : };
     810             : 
     811             : /** Garrison a attacked siege ranged unit inside the nearest fortress. */
     812           0 : PETRA.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit)
     813             : {
     814           0 :         let distmin = Math.min();
     815             :         let nearest;
     816           0 :         let unitAccess = PETRA.getLandAccess(gameState, unit);
     817           0 :         let garrisonManager = gameState.ai.HQ.garrisonManager;
     818           0 :         for (let ent of gameState.getAllyStructures().values())
     819             :         {
     820           0 :                 if (!ent.isGarrisonHolder())
     821           0 :                         continue;
     822           0 :                 if (!unit.hasClasses(ent.garrisonableClasses()))
     823           0 :                         continue;
     824           0 :                 if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax())
     825           0 :                         continue;
     826           0 :                 if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints())
     827           0 :                         continue;
     828           0 :                 if (PETRA.getLandAccess(gameState, ent) != unitAccess)
     829           0 :                         continue;
     830           0 :                 let dist = API3.SquareVectorDistance(ent.position(), unit.position());
     831           0 :                 if (dist > distmin)
     832           0 :                         continue;
     833           0 :                 distmin = dist;
     834           0 :                 nearest = ent;
     835             :         }
     836           0 :         if (nearest)
     837           0 :                 garrisonManager.garrison(gameState, unit, nearest, PETRA.GarrisonManager.TYPE_PROTECTION);
     838           0 :         return nearest !== undefined;
     839             : };
     840             : 
     841             : /**
     842             :  * Garrison a hurt unit inside a player-owned or allied structure.
     843             :  * If emergency is true, the unit will be garrisoned in the closest possible structure.
     844             :  * Otherwise, it will garrison in the closest healing structure.
     845             :  */
     846           0 : PETRA.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false)
     847             : {
     848           0 :         let distmin = Math.min();
     849             :         let nearest;
     850           0 :         let unitAccess = PETRA.getLandAccess(gameState, unit);
     851           0 :         let garrisonManager = gameState.ai.HQ.garrisonManager;
     852           0 :         for (let ent of gameState.getAllyStructures().values())
     853             :         {
     854           0 :                 if (!ent.isGarrisonHolder())
     855           0 :                         continue;
     856           0 :                 if (!emergency && !ent.buffHeal())
     857           0 :                         continue;
     858           0 :                 if (!unit.hasClasses(ent.garrisonableClasses()))
     859           0 :                         continue;
     860           0 :                 if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax() &&
     861             :                     (!emergency || !ent.garrisoned().length))
     862           0 :                         continue;
     863           0 :                 if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints())
     864           0 :                         continue;
     865           0 :                 if (PETRA.getLandAccess(gameState, ent) != unitAccess)
     866           0 :                         continue;
     867           0 :                 let dist = API3.SquareVectorDistance(ent.position(), unit.position());
     868           0 :                 if (dist > distmin)
     869           0 :                         continue;
     870           0 :                 distmin = dist;
     871           0 :                 nearest = ent;
     872             :         }
     873           0 :         if (!nearest)
     874           0 :                 return false;
     875             : 
     876           0 :         if (!emergency)
     877             :         {
     878           0 :                 garrisonManager.garrison(gameState, unit, nearest, PETRA.GarrisonManager.TYPE_PROTECTION);
     879           0 :                 return true;
     880             :         }
     881           0 :         if (garrisonManager.numberOfGarrisonedSlots(nearest) >= nearest.garrisonMax()) // make room for this ent
     882           0 :                 nearest.unload(nearest.garrisoned()[0]);
     883             : 
     884           0 :         garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? PETRA.GarrisonManager.TYPE_PROTECTION : PETRA.GarrisonManager.TYPE_EMERGENCY);
     885           0 :         return true;
     886             : };
     887             : 
     888             : /**
     889             :  * Be more inclined to help an ally attacked by several enemies.
     890             :  */
     891           0 : PETRA.DefenseManager.prototype.GetCooperationLevel = function(ally)
     892             : {
     893           0 :         let cooperation = this.Config.personality.cooperative;
     894           0 :         if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1)
     895           0 :                 cooperation += 0.2 * (this.attackedAllies[ally] - 1);
     896           0 :         return cooperation;
     897             : };
     898             : 
     899             : /**
     900             :  * Switch a defense army into an attack if needed.
     901             :  */
     902           0 : PETRA.DefenseManager.prototype.switchToAttack = function(gameState, army)
     903             : {
     904           0 :         if (!army)
     905           0 :                 return;
     906           0 :         for (let targetId of this.targetList)
     907             :         {
     908           0 :                 let target = gameState.getEntityById(targetId);
     909           0 :                 if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner()))
     910           0 :                         continue;
     911           0 :                 let targetAccess = PETRA.getLandAccess(gameState, target);
     912           0 :                 let targetPos = target.position();
     913           0 :                 for (let entId of army.ownEntities)
     914             :                 {
     915           0 :                         let ent = gameState.getEntityById(entId);
     916           0 :                         if (!ent || !ent.position() || PETRA.getLandAccess(gameState, ent) != targetAccess)
     917           0 :                                 continue;
     918           0 :                         if (API3.SquareVectorDistance(targetPos, ent.position()) > 14400)
     919           0 :                                 continue;
     920           0 :                         gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true });
     921           0 :                         return;
     922             :                 }
     923             :         }
     924             : };
     925             : 
     926           0 : PETRA.DefenseManager.prototype.Serialize = function()
     927             : {
     928           0 :         let properties = {
     929             :                 "targetList": this.targetList,
     930             :                 "armyMergeSize": this.armyMergeSize,
     931             :                 "attackingUnits": this.attackingUnits,
     932             :                 "attackingArmies": this.attackingArmies,
     933             :                 "attackedAllies": this.attackedAllies
     934             :         };
     935             : 
     936           0 :         let armies = [];
     937           0 :         for (let army of this.armies)
     938           0 :                 armies.push(army.Serialize());
     939             : 
     940           0 :         return { "properties": properties, "armies": armies };
     941             : };
     942             : 
     943           0 : PETRA.DefenseManager.prototype.Deserialize = function(gameState, data)
     944             : {
     945           0 :         for (let key in data.properties)
     946           0 :                 this[key] = data.properties[key];
     947             : 
     948           0 :         this.armies = [];
     949           0 :         for (let dataArmy of data.armies)
     950             :         {
     951           0 :                 let army = new PETRA.DefenseArmy(gameState, []);
     952           0 :                 army.Deserialize(dataArmy);
     953           0 :                 this.armies.push(army);
     954             :         }
     955             : };

Generated by: LCOV version 1.14