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

          Line data    Source code
       1             : /**
       2             :  * This is an attack plan:
       3             :  * It deals with everything in an attack, from picking a target to picking a path to it
       4             :  * To making sure units are built, and pushing elements to the queue manager otherwise
       5             :  * It also handles the actual attack, though much work is needed on that.
       6             :  */
       7           0 : PETRA.AttackPlan = function(gameState, Config, uniqueID, type = PETRA.AttackPlan.TYPE_DEFAULT, data)
       8             : {
       9           0 :         this.Config = Config;
      10           0 :         this.name = uniqueID;
      11           0 :         this.type = type;
      12           0 :         this.state = PETRA.AttackPlan.STATE_UNEXECUTED;
      13           0 :         this.forced = false;  // true when this attacked has been forced to help an ally
      14             : 
      15           0 :         if (data && data.target)
      16             :         {
      17           0 :                 this.target = data.target;
      18           0 :                 this.targetPos = this.target.position();
      19           0 :                 this.targetPlayer = this.target.owner();
      20             :         }
      21             :         else
      22             :         {
      23           0 :                 this.target = undefined;
      24           0 :                 this.targetPos = undefined;
      25           0 :                 this.targetPlayer = undefined;
      26             :         }
      27             : 
      28           0 :         this.uniqueTargetId = data && data.uniqueTargetId || undefined;
      29             : 
      30             :         // get a starting rallyPoint ... will be improved later
      31             :         let rallyPoint;
      32             :         let rallyAccess;
      33           0 :         let allAccesses = {};
      34           0 :         for (const base of gameState.ai.HQ.baseManagers())
      35             :         {
      36           0 :                 if (!base.anchor || !base.anchor.position())
      37           0 :                         continue;
      38           0 :                 let access = PETRA.getLandAccess(gameState, base.anchor);
      39           0 :                 if (!rallyPoint)
      40             :                 {
      41           0 :                         rallyPoint = base.anchor.position();
      42           0 :                         rallyAccess = access;
      43             :                 }
      44           0 :                 if (!allAccesses[access])
      45           0 :                         allAccesses[access] = base.anchor.position();
      46             :         }
      47           0 :         if (!rallyPoint)        // no base ?  take the position of any of our entities
      48             :         {
      49           0 :                 for (let ent of gameState.getOwnEntities().values())
      50             :                 {
      51           0 :                         if (!ent.position())
      52           0 :                                 continue;
      53           0 :                         let access = PETRA.getLandAccess(gameState, ent);
      54           0 :                         rallyPoint = ent.position();
      55           0 :                         rallyAccess = access;
      56           0 :                         allAccesses[access] = rallyPoint;
      57           0 :                         break;
      58             :                 }
      59           0 :                 if (!rallyPoint)
      60             :                 {
      61           0 :                         this.failed = true;
      62           0 :                         return false;
      63             :                 }
      64             :         }
      65           0 :         this.rallyPoint = rallyPoint;
      66           0 :         this.overseas = 0;
      67           0 :         if (gameState.ai.HQ.navalMap)
      68             :         {
      69           0 :                 for (let structure of gameState.getEnemyStructures().values())
      70             :                 {
      71           0 :                         if (this.target && structure.id() != this.target.id())
      72           0 :                                 continue;
      73           0 :                         if (!structure.position())
      74           0 :                                 continue;
      75           0 :                         let access = PETRA.getLandAccess(gameState, structure);
      76           0 :                         if (access in allAccesses)
      77             :                         {
      78           0 :                                 this.overseas = 0;
      79           0 :                                 this.rallyPoint = allAccesses[access];
      80           0 :                                 break;
      81             :                         }
      82           0 :                         else if (!this.overseas)
      83             :                         {
      84           0 :                                 let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access);
      85           0 :                                 if (!sea)
      86             :                                 {
      87           0 :                                         if (this.target)
      88             :                                         {
      89           0 :                                                 API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target " +
      90             :                                                           this.target.templateName() + " indices " + rallyAccess + " " + access);
      91           0 :                                                 this.failed = true;
      92           0 :                                                 return false;
      93             :                                         }
      94           0 :                                         continue;
      95             :                                 }
      96           0 :                                 this.overseas = sea;
      97           0 :                                 gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1);
      98             :                         }
      99             :                 }
     100             :         }
     101           0 :         this.paused = false;
     102           0 :         this.maxCompletingTime = 0;
     103             : 
     104             :         // priority of the queues we'll create.
     105           0 :         let priority = 70;
     106             : 
     107             :         // unitStat priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize".
     108             :         // if not, this is a "bonus". The higher the priority, the faster this unit will get built.
     109             :         // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm)
     110             :         // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built
     111             :         // only once every other category is at least 50% of its target size.
     112             :         // note: siege build order is currently added by the military manager if a fortress is there.
     113           0 :         this.unitStat = {};
     114             : 
     115             :         // neededShips is the minimal number of ships which should be available for transport
     116           0 :         if (type === PETRA.AttackPlan.TYPE_RUSH)
     117             :         {
     118           0 :                 priority = 250;
     119           0 :                 this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"],
     120             :                         "interests": [["strength", 1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"]] };
     121           0 :                 this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"],
     122             :                         "interests": [["strength", 1]] };
     123           0 :                 if (data && data.targetSize)
     124           0 :                         this.unitStat.Infantry.targetSize = data.targetSize;
     125           0 :                 this.neededShips = 1;
     126             :         }
     127           0 :         else if (type === PETRA.AttackPlan.TYPE_RAID)
     128             :         {
     129           0 :                 priority = 150;
     130           0 :                 this.unitStat.FastMoving = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"],
     131             :                         "interests": [ ["strength", 1] ] };
     132           0 :                 this.neededShips = 1;
     133             :         }
     134           0 :         else if (type === PETRA.AttackPlan.TYPE_HUGE_ATTACK)
     135             :         {
     136           0 :                 priority = 90;
     137             :                 // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units.
     138           0 :                 this.unitStat.RangedInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry+Ranged+CitizenSoldier"],
     139             :                         "interests": [["strength", 3]] };
     140           0 :                 this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry+Melee+CitizenSoldier"],
     141             :                         "interests": [["strength", 3]] };
     142           0 :                 this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry+Ranged+Champion"],
     143             :                         "interests": [["strength", 3]] };
     144           0 :                 this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry+Melee+Champion"],
     145             :                         "interests": [["strength", 3]] };
     146           0 :                 this.unitStat.RangedFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Ranged+CitizenSoldier"],
     147             :                         "interests": [["strength", 2]] };
     148           0 :                 this.unitStat.MeleeFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Melee+CitizenSoldier"],
     149             :                         "interests": [["strength", 2]] };
     150           0 :                 this.unitStat.ChampRangedFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Ranged+Champion"],
     151             :                         "interests": [["strength", 3]] };
     152           0 :                 this.unitStat.ChampMeleeFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Melee+Champion"],
     153             :                         "interests": [["strength", 2]] };
     154           0 :                 this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"],
     155             :                         "interests": [["strength", 2]] };
     156           0 :                 this.neededShips = 5;
     157             :         }
     158             :         else
     159             :         {
     160           0 :                 priority = 70;
     161           0 :                 this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry+Ranged"],
     162             :                         "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] };
     163           0 :                 this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry+Melee"],
     164             :                         "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] };
     165           0 :                 this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"],
     166             :                         "interests": [["strength", 1]] };
     167           0 :                 this.neededShips = 3;
     168             :         }
     169             : 
     170             :         // Put some randomness on the attack size
     171           0 :         let variation = randFloat(0.8, 1.2);
     172             :         // and lower priority and smaller sizes for easier difficulty levels
     173           0 :         if (this.Config.difficulty < PETRA.DIFFICULTY_EASY)
     174             :         {
     175           0 :                 priority *= 0.4;
     176           0 :                 variation *= 0.2;
     177             :         }
     178           0 :         else if (this.Config.difficulty < PETRA.DIFFICULTY_MEDIUM)
     179             :         {
     180           0 :                 priority *= 0.8;
     181           0 :                 variation *= 0.6;
     182             :         }
     183             : 
     184           0 :         if (this.Config.difficulty < PETRA.DIFFICULTY_EASY)
     185             :         {
     186           0 :                 for (const cat in this.unitStat)
     187             :                 {
     188           0 :                         this.unitStat[cat].targetSize = Math.ceil(variation * this.unitStat[cat].targetSize);
     189           0 :                         this.unitStat[cat].minSize = Math.min(this.unitStat[cat].targetSize, Math.min(this.unitStat[cat].minSize, 2));
     190           0 :                         this.unitStat[cat].batchSize = this.unitStat[cat].minSize;
     191             :                 }
     192             :         }
     193             :         else
     194             :         {
     195           0 :                 for (const cat in this.unitStat)
     196             :                 {
     197           0 :                         this.unitStat[cat].targetSize = Math.ceil(variation * this.unitStat[cat].targetSize);
     198           0 :                         this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize);
     199             :                 }
     200             :         }
     201             : 
     202             :         // change the sizes according to max population
     203           0 :         this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips);
     204           0 :         for (let cat in this.unitStat)
     205             :         {
     206           0 :                 this.unitStat[cat].targetSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].targetSize);
     207           0 :                 this.unitStat[cat].minSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].minSize);
     208             :         }
     209             : 
     210             :         // TODO: there should probably be one queue per type of training building
     211           0 :         gameState.ai.queueManager.addQueue("plan_" + this.name, priority);
     212           0 :         gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1);
     213           0 :         gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority);
     214             : 
     215             :         // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ]
     216           0 :         this.buildOrders = [];
     217           0 :         this.canBuildUnits = gameState.ai.HQ.canBuildUnits;
     218           0 :         this.siegeState = PETRA.AttackPlan.SIEGE_NOT_TESTED;
     219             : 
     220             :         // some variables used during the attack
     221           0 :         this.position5TurnsAgo = [0, 0];
     222           0 :         this.lastPosition = [0, 0];
     223           0 :         this.position = [0, 0];
     224           0 :         this.isBlocked = false;      // true when this attack faces walls
     225             : 
     226           0 :         return true;
     227             : };
     228             : 
     229           0 : PETRA.AttackPlan.PREPARATION_FAILED = 0;
     230           0 : PETRA.AttackPlan.PREPARATION_KEEP_GOING = 1;
     231           0 : PETRA.AttackPlan.PREPARATION_START = 2;
     232             : 
     233           0 : PETRA.AttackPlan.SIEGE_NOT_TESTED = 0;
     234           0 : PETRA.AttackPlan.SIEGE_NO_TRAINER = 1;
     235             : 
     236             : /**
     237             :  * Siege added in build orders
     238             :  */
     239           0 : PETRA.AttackPlan.SIEGE_ADDED = 2;
     240             : 
     241           0 : PETRA.AttackPlan.STATE_UNEXECUTED = "unexecuted";
     242           0 : PETRA.AttackPlan.STATE_COMPLETING = "completing";
     243           0 : PETRA.AttackPlan.STATE_ARRIVED = "arrived";
     244             : 
     245           0 : PETRA.AttackPlan.TYPE_DEFAULT = "Attack";
     246           0 : PETRA.AttackPlan.TYPE_HUGE_ATTACK = "HugeAttack";
     247           0 : PETRA.AttackPlan.TYPE_RAID = "Raid";
     248           0 : PETRA.AttackPlan.TYPE_RUSH = "Rush";
     249             : 
     250           0 : PETRA.AttackPlan.prototype.init = function(gameState)
     251             : {
     252           0 :         this.queue = gameState.ai.queues["plan_" + this.name];
     253           0 :         this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"];
     254           0 :         this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"];
     255             : 
     256           0 :         this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name));
     257           0 :         this.unitCollection.registerUpdates();
     258             : 
     259           0 :         this.unit = {};
     260             : 
     261             :         // defining the entity collections. Will look for units I own, that are part of this plan.
     262             :         // Also defining the buildOrders.
     263           0 :         for (let cat in this.unitStat)
     264             :         {
     265           0 :                 let Unit = this.unitStat[cat];
     266           0 :                 this.unit[cat] = this.unitCollection.filter(API3.Filters.byClasses(Unit.classes));
     267           0 :                 this.unit[cat].registerUpdates();
     268           0 :                 if (this.canBuildUnits)
     269           0 :                         this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]);
     270             :         }
     271             : };
     272             : 
     273           0 : PETRA.AttackPlan.prototype.getName = function()
     274             : {
     275           0 :         return this.name;
     276             : };
     277             : 
     278           0 : PETRA.AttackPlan.prototype.getType = function()
     279             : {
     280           0 :         return this.type;
     281             : };
     282             : 
     283           0 : PETRA.AttackPlan.prototype.isStarted = function()
     284             : {
     285           0 :         return this.state !== PETRA.AttackPlan.STATE_UNEXECUTED && this.state !== PETRA.AttackPlan.STATE_COMPLETING;
     286             : };
     287             : 
     288           0 : PETRA.AttackPlan.prototype.isPaused = function()
     289             : {
     290           0 :         return this.paused;
     291             : };
     292             : 
     293           0 : PETRA.AttackPlan.prototype.setPaused = function(boolValue)
     294             : {
     295           0 :         this.paused = boolValue;
     296             : };
     297             : 
     298             : /**
     299             :  * Returns true if the attack can be executed at the current time
     300             :  * Basically it checks we have enough units.
     301             :  */
     302           0 : PETRA.AttackPlan.prototype.canStart = function()
     303             : {
     304           0 :         if (!this.canBuildUnits)
     305           0 :                 return true;
     306             : 
     307           0 :         for (let unitCat in this.unitStat)
     308           0 :                 if (this.unit[unitCat].length < this.unitStat[unitCat].minSize)
     309           0 :                         return false;
     310             : 
     311           0 :         return true;
     312             : };
     313             : 
     314           0 : PETRA.AttackPlan.prototype.mustStart = function()
     315             : {
     316           0 :         if (this.isPaused())
     317           0 :                 return false;
     318             : 
     319           0 :         if (!this.canBuildUnits)
     320           0 :                 return this.unitCollection.hasEntities();
     321             : 
     322           0 :         let MaxReachedEverywhere = true;
     323           0 :         let MinReachedEverywhere = true;
     324           0 :         for (let unitCat in this.unitStat)
     325             :         {
     326           0 :                 let Unit = this.unitStat[unitCat];
     327           0 :                 if (this.unit[unitCat].length < Unit.targetSize)
     328           0 :                         MaxReachedEverywhere = false;
     329           0 :                 if (this.unit[unitCat].length < Unit.minSize)
     330             :                 {
     331           0 :                         MinReachedEverywhere = false;
     332           0 :                         break;
     333             :                 }
     334             :         }
     335             : 
     336           0 :         if (MaxReachedEverywhere)
     337           0 :                 return true;
     338           0 :         if (MinReachedEverywhere)
     339           0 :                 return this.type === PETRA.AttackPlan.TYPE_RAID && this.target && this.target.foundationProgress() &&
     340             :                                                              this.target.foundationProgress() > 50;
     341           0 :         return false;
     342             : };
     343             : 
     344           0 : PETRA.AttackPlan.prototype.forceStart = function()
     345             : {
     346           0 :         for (let unitCat in this.unitStat)
     347             :         {
     348           0 :                 let Unit = this.unitStat[unitCat];
     349           0 :                 Unit.targetSize = 0;
     350           0 :                 Unit.minSize = 0;
     351             :         }
     352           0 :         this.forced = true;
     353             : };
     354             : 
     355           0 : PETRA.AttackPlan.prototype.emptyQueues = function()
     356             : {
     357           0 :         this.queue.empty();
     358           0 :         this.queueChamp.empty();
     359           0 :         this.queueSiege.empty();
     360             : };
     361             : 
     362           0 : PETRA.AttackPlan.prototype.removeQueues = function(gameState)
     363             : {
     364           0 :         gameState.ai.queueManager.removeQueue("plan_" + this.name);
     365           0 :         gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ");
     366           0 :         gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege");
     367             : };
     368             : 
     369             : /** Adds a build order. If resetQueue is true, this will reset the queue. */
     370           0 : PETRA.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue)
     371             : {
     372           0 :         if (!this.isStarted())
     373             :         {
     374             :                 // no minsize as we don't want the plan to fail at the last minute though.
     375           0 :                 this.unitStat[name] = unitStats;
     376           0 :                 let Unit = this.unitStat[name];
     377           0 :                 this.unit[name] = this.unitCollection.filter(API3.Filters.byClasses(Unit.classes));
     378           0 :                 this.unit[name].registerUpdates();
     379           0 :                 this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]);
     380           0 :                 if (resetQueue)
     381           0 :                         this.emptyQueues();
     382             :         }
     383             : };
     384             : 
     385           0 : PETRA.AttackPlan.prototype.addSiegeUnits = function(gameState)
     386             : {
     387           0 :         if (this.siegeState === PETRA.AttackPlan.SIEGE_ADDED || this.state !== PETRA.AttackPlan.STATE_UNEXECUTED)
     388           0 :                 return false;
     389             : 
     390           0 :         let civ = gameState.getPlayerCiv();
     391           0 :         const classes = [["Siege+Melee"], ["Siege+Ranged"], ["Elephant+Melee"]];
     392           0 :         let hasTrainer = [false, false, false];
     393           0 :         for (let ent of gameState.getOwnTrainingFacilities().values())
     394             :         {
     395           0 :                 let trainables = ent.trainableEntities(civ);
     396           0 :                 if (!trainables)
     397           0 :                         continue;
     398           0 :                 for (let trainable of trainables)
     399             :                 {
     400           0 :                         if (gameState.isTemplateDisabled(trainable))
     401           0 :                                 continue;
     402           0 :                         let template = gameState.getTemplate(trainable);
     403           0 :                         if (!template || !template.available(gameState))
     404           0 :                                 continue;
     405           0 :                         for (let i = 0; i < classes.length; ++i)
     406           0 :                                 if (template.hasClasses(classes[i]))
     407           0 :                                         hasTrainer[i] = true;
     408             :                 }
     409             :         }
     410           0 :         if (hasTrainer.every(e => !e))
     411             :         {
     412           0 :                 this.siegeState = PETRA.AttackPlan.SIEGE_NO_TRAINER;
     413           0 :                 return false;
     414             :         }
     415           0 :         let i = this.name % classes.length;
     416           0 :         for (let k = 0; k < classes.length; ++k)
     417             :         {
     418           0 :                 if (hasTrainer[i])
     419           0 :                         break;
     420           0 :                 i = ++i % classes.length;
     421             :         }
     422             : 
     423           0 :         this.siegeState = PETRA.AttackPlan.SIEGE_ADDED;
     424             :         let targetSize;
     425           0 :         if (this.Config.difficulty < PETRA.DIFFICULTY_MEDIUM)
     426           0 :                 targetSize = this.type === PETRA.AttackPlan.TYPE_HUGE_ATTACK ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0);
     427             :         else
     428           0 :                 targetSize = this.type === PETRA.AttackPlan.TYPE_HUGE_ATTACK ? this.Config.difficulty + 1 : this.Config.difficulty - 1;
     429           0 :         targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type === PETRA.AttackPlan.TYPE_HUGE_ATTACK ? 1 : 0);
     430           0 :         if (!targetSize)
     431           0 :                 return true;
     432             :         // no minsize as we don't want the plan to fail at the last minute though.
     433           0 :         let stat = { "priority": 1, "minSize": 0, "targetSize": targetSize, "batchSize": Math.min(targetSize, 2),
     434             :                  "classes": classes[i], "interests": [ ["siegeStrength", 3] ] };
     435           0 :         this.addBuildOrder(gameState, "Siege", stat, true);
     436           0 :         return true;
     437             : };
     438             : 
     439             : /** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */
     440           0 : PETRA.AttackPlan.prototype.updatePreparation = function(gameState)
     441             : {
     442             :         // the completing step is used to return resources and regroup the units
     443             :         // so we check that we have no more forced order before starting the attack
     444           0 :         if (this.state === PETRA.AttackPlan.STATE_COMPLETING)
     445             :         {
     446             :                 // if our target was destroyed, go back to "unexecuted" state
     447           0 :                 if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id()))
     448             :                 {
     449           0 :                         this.state = PETRA.AttackPlan.STATE_UNEXECUTED;
     450           0 :                         this.target = undefined;
     451             :                 }
     452             :                 else
     453             :                 {
     454             :                         // check that all units have finished with their transport if needed
     455           0 :                         if (this.waitingForTransport())
     456           0 :                                 return PETRA.AttackPlan.PREPARATION_KEEP_GOING;
     457             :                         // bloqued units which cannot finish their order should not stop the attack
     458           0 :                         if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder())
     459           0 :                                 return PETRA.AttackPlan.PREPARATION_KEEP_GOING;
     460           0 :                         return PETRA.AttackPlan.PREPARATION_START;
     461             :                 }
     462             :         }
     463             : 
     464           0 :         if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0)
     465           0 :                 this.debugAttack();
     466             : 
     467             :         // if we need a transport, wait for some transport ships
     468           0 :         if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length)
     469           0 :                 return PETRA.AttackPlan.PREPARATION_KEEP_GOING;
     470             : 
     471           0 :         if (this.type !== PETRA.AttackPlan.TYPE_RAID || !this.forced)    // Forced Raids have special purposes (as relic capture)
     472           0 :                 this.assignUnits(gameState);
     473           0 :         if (this.type !== PETRA.AttackPlan.TYPE_RAID && gameState.ai.HQ.attackManager.getAttackInPreparation(PETRA.AttackPlan.TYPE_RAID) !== undefined)
     474           0 :                 this.reassignFastUnit(gameState);    // reassign some fast units (if any) to fasten raid preparations
     475             : 
     476             :         // Fasten the end game.
     477           0 :         if (gameState.ai.playedTurn % 5 == 0 && this.hasSiegeUnits())
     478             :         {
     479           0 :                 let totEnemies = 0;
     480           0 :                 let hasEnemies = false;
     481           0 :                 for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
     482             :                 {
     483           0 :                         if (!gameState.isPlayerEnemy(i) || gameState.ai.HQ.attackManager.defeated[i])
     484           0 :                                 continue;
     485           0 :                         hasEnemies = true;
     486           0 :                         totEnemies += gameState.getEnemyUnits(i).length;
     487             :                 }
     488           0 :                 if (hasEnemies && this.unitCollection.length > 20 + 2 * totEnemies)
     489           0 :                         this.forceStart();
     490             :         }
     491             : 
     492             :         // special case: if we've reached max pop, and we can start the plan, start it.
     493           0 :         if (gameState.getPopulationMax() - gameState.getPopulation() < 5)
     494             :         {
     495           0 :                 let lengthMin = 16;
     496           0 :                 if (gameState.getPopulationMax() < 300)
     497           0 :                         lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300);
     498           0 :                 if (this.canStart() || this.unitCollection.length > lengthMin)
     499             :                 {
     500           0 :                         this.emptyQueues();
     501             :                 }
     502             :                 else    // Abort the plan so that its units will be reassigned to other plans.
     503             :                 {
     504           0 :                         if (this.Config.debug > 1)
     505             :                         {
     506           0 :                                 let am = gameState.ai.HQ.attackManager;
     507           0 :                                 API3.warn(" attacks upcoming: raid " + am.upcomingAttacks[PETRA.AttackPlan.TYPE_RAID].length +
     508             :                                           " rush " + am.upcomingAttacks[PETRA.AttackPlan.TYPE_RUSH].length +
     509             :                                           " attack " + am.upcomingAttacks[PETRA.AttackPlan.TYPE_DEFAULT].length +
     510             :                                           " huge " + am.upcomingAttacks[PETRA.AttackPlan.TYPE_HUGE_ATTACK].length);
     511           0 :                                 API3.warn(" attacks started: raid " + am.startedAttacks[PETRA.AttackPlan.TYPE_RAID].length +
     512             :                                           " rush " + am.startedAttacks[PETRA.AttackPlan.TYPE_RUSH].length +
     513             :                                           " attack " + am.startedAttacks[PETRA.AttackPlan.TYPE_DEFAULT].length +
     514             :                                           " huge " + am.startedAttacks[PETRA.AttackPlan.TYPE_HUGE_ATTACK].length);
     515             :                         }
     516           0 :                         return PETRA.AttackPlan.PREPARATION_FAILED;
     517             :                 }
     518             :         }
     519           0 :         else if (this.mustStart())
     520             :         {
     521           0 :                 if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0)
     522             :                 {
     523             :                         // keep on while the units finish being trained, then we'll start
     524           0 :                         this.emptyQueues();
     525           0 :                         return PETRA.AttackPlan.PREPARATION_KEEP_GOING;
     526             :                 }
     527             :         }
     528             :         else
     529             :         {
     530           0 :                 if (this.canBuildUnits)
     531             :                 {
     532             :                         // We still have time left to recruit units and do stuffs.
     533           0 :                         if (this.siegeState === PETRA.AttackPlan.SIEGE_NOT_TESTED ||
     534             :                                 this.siegeState === PETRA.AttackPlan.SIEGE_NO_TRAINER && gameState.ai.playedTurn % 5 == 0)
     535           0 :                                 this.addSiegeUnits(gameState);
     536           0 :                         this.trainMoreUnits(gameState);
     537             :                         // may happen if we have no more training facilities and build orders are canceled
     538           0 :                         if (!this.buildOrders.length)
     539           0 :                                 return PETRA.AttackPlan.PREPARATION_FAILED;     // will abort the plan
     540             :                 }
     541           0 :                 return PETRA.AttackPlan.PREPARATION_KEEP_GOING;
     542             :         }
     543             : 
     544             :         // if we're here, it means we must start
     545           0 :         this.state = PETRA.AttackPlan.STATE_COMPLETING;
     546             : 
     547             :         // Raids have their predefined target
     548           0 :         if (!this.target && !this.chooseTarget(gameState))
     549           0 :                 return PETRA.AttackPlan.PREPARATION_FAILED;
     550           0 :         if (!this.overseas)
     551           0 :                 this.getPathToTarget(gameState);
     552             : 
     553           0 :         if (this.type === PETRA.AttackPlan.TYPE_RAID)
     554           0 :                 this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20;
     555             :         else
     556             :         {
     557           0 :                 if (this.type === PETRA.AttackPlan.TYPE_RUSH || this.forced)
     558           0 :                         this.maxCompletingTime = gameState.ai.elapsedTime + 40;
     559             :                 else
     560           0 :                         this.maxCompletingTime = gameState.ai.elapsedTime + 60;
     561             :                 // warn our allies so that they can help if possible
     562           0 :                 if (!this.requested)
     563           0 :                         Engine.PostCommand(PlayerID, { "type": "attack-request", "source": PlayerID, "player": this.targetPlayer });
     564             :         }
     565             : 
     566             :         // Remove those units which were in a temporary bombing attack
     567           0 :         for (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values())
     568             :         {
     569           0 :                 for (let entId of unitIds.values())
     570             :                 {
     571           0 :                         let ent = gameState.getEntityById(entId);
     572           0 :                         if (!ent || ent.getMetadata(PlayerID, "plan") != this.name)
     573           0 :                                 continue;
     574           0 :                         unitIds.delete(entId);
     575           0 :                         ent.stopMoving();
     576             :                 }
     577             :         }
     578             : 
     579           0 :         let rallyPoint = this.rallyPoint;
     580           0 :         let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint);
     581           0 :         for (let ent of this.unitCollection.values())
     582             :         {
     583             :                 // For the time being, if occupied in a transport, remove the unit from this plan   TODO improve that
     584           0 :                 if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
     585             :                 {
     586           0 :                         ent.setMetadata(PlayerID, "plan", -1);
     587           0 :                         continue;
     588             :                 }
     589           0 :                 ent.setMetadata(PlayerID, "role", PETRA.Worker.ROLE_ATTACK);
     590           0 :                 ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_COMPLETING);
     591           0 :                 let queued = false;
     592           0 :                 if (ent.resourceCarrying() && ent.resourceCarrying().length)
     593           0 :                         queued = PETRA.returnResources(gameState, ent);
     594           0 :                 let index = PETRA.getLandAccess(gameState, ent);
     595           0 :                 if (index == rallyIndex)
     596           0 :                         ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued);
     597             :                 else
     598           0 :                         gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint);
     599             :         }
     600             : 
     601             :         // reset all queued units
     602           0 :         this.removeQueues(gameState);
     603           0 :         return PETRA.AttackPlan.PREPARATION_KEEP_GOING;
     604             : };
     605             : 
     606           0 : PETRA.AttackPlan.prototype.trainMoreUnits = function(gameState)
     607             : {
     608             :         // let's sort by training advancement, ie 'current size / target size'
     609             :         // count the number of queued units too.
     610             :         // substract priority.
     611           0 :         for (let order of this.buildOrders)
     612             :         {
     613           0 :                 let special = "Plan_" + this.name + "_" + order[4];
     614           0 :                 let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special);
     615           0 :                 aQueued += this.queue.countQueuedUnitsWithMetadata("special", special);
     616           0 :                 aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special);
     617           0 :                 aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special);
     618           0 :                 order[0] = order[2].length + aQueued;
     619             :         }
     620           0 :         this.buildOrders.sort((a, b) => {
     621           0 :                 let va = a[0]/a[3].targetSize - a[3].priority;
     622           0 :                 if (a[0] >= a[3].targetSize)
     623           0 :                         va += 1000;
     624           0 :                 let vb = b[0]/b[3].targetSize - b[3].priority;
     625           0 :                 if (b[0] >= b[3].targetSize)
     626           0 :                         vb += 1000;
     627           0 :                 return va - vb;
     628             :         });
     629             : 
     630           0 :         if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0)
     631             :         {
     632           0 :                 API3.warn("====================================");
     633           0 :                 API3.warn("======== build order for plan " + this.name);
     634           0 :                 for (let order of this.buildOrders)
     635             :                 {
     636           0 :                         let specialData = "Plan_"+this.name+"_"+order[4];
     637           0 :                         let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData);
     638           0 :                         let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData);
     639           0 :                         let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData);
     640           0 :                         let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData);
     641           0 :                         API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining +
     642             :                                   " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize);
     643             :                 }
     644           0 :                 API3.warn("====================================");
     645             :         }
     646             : 
     647           0 :         let firstOrder = this.buildOrders[0];
     648           0 :         if (firstOrder[0] < firstOrder[3].targetSize)
     649             :         {
     650             :                 // find the actual queue we want
     651           0 :                 let queue = this.queue;
     652           0 :                 if (firstOrder[4] == "Siege")
     653           0 :                         queue = this.queueSiege;
     654           0 :                 else if (firstOrder[3].classes.indexOf("Hero") != -1)
     655           0 :                         queue = this.queueSiege;
     656           0 :                 else if (firstOrder[3].classes.indexOf("Champion") != -1)
     657           0 :                         queue = this.queueChamp;
     658             : 
     659           0 :                 if (queue.length() <= 5)
     660             :                 {
     661           0 :                         let template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[3].interests);
     662             :                         // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder,
     663             :                         // effectively removing the unit from the plan.
     664           0 :                         if (template === undefined)
     665             :                         {
     666           0 :                                 if (this.Config.debug > 1)
     667           0 :                                         API3.warn("attack no template found " + firstOrder[1]);
     668           0 :                                 delete this.unitStat[firstOrder[4]];    // deleting the associated unitstat.
     669           0 :                                 this.buildOrders.splice(0, 1);
     670             :                         }
     671             :                         else
     672             :                         {
     673           0 :                                 if (this.Config.debug > 2)
     674           0 :                                         API3.warn("attack template " + template + " added for plan " + this.name);
     675           0 :                                 let max = firstOrder[3].batchSize;
     676           0 :                                 let specialData = "Plan_" + this.name + "_" + firstOrder[4];
     677           0 :                                 let data = { "plan": this.name, "special": specialData, "base": 0 };
     678           0 :                                 data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? PETRA.Worker.ROLE_WORKER : PETRA.Worker.ROLE_ATTACK;
     679           0 :                                 let trainingPlan = new PETRA.TrainingPlan(gameState, template, data, max, max);
     680           0 :                                 if (trainingPlan.template)
     681           0 :                                         queue.addPlan(trainingPlan);
     682           0 :                                 else if (this.Config.debug > 1)
     683           0 :                                         API3.warn("training plan canceled because no template for " + template + "   build1 " + uneval(firstOrder[1]) +
     684             :                                                   " build3 " + uneval(firstOrder[3].interests));
     685             :                         }
     686             :                 }
     687             :         }
     688             : };
     689             : 
     690           0 : PETRA.AttackPlan.prototype.assignUnits = function(gameState)
     691             : {
     692           0 :         let plan = this.name;
     693           0 :         let added = false;
     694             :         // If we can not build units, assign all available except those affected to allied defense to the current attack.
     695           0 :         if (!this.canBuildUnits)
     696             :         {
     697           0 :                 for (let ent of gameState.getOwnUnits().values())
     698             :                 {
     699           0 :                         if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent))
     700           0 :                                 continue;
     701           0 :                         ent.setMetadata(PlayerID, "plan", plan);
     702           0 :                         this.unitCollection.updateEnt(ent);
     703           0 :                         added = true;
     704             :                 }
     705           0 :                 return added;
     706             :         }
     707             : 
     708           0 :         if (this.type === PETRA.AttackPlan.TYPE_RAID)
     709             :         {
     710             :                 // Raids are quick attacks: assign all FastMoving soldiers except some for hunting.
     711           0 :                 let num = 0;
     712           0 :                 for (let ent of gameState.getOwnUnits().values())
     713             :                 {
     714           0 :                         if (!ent.hasClass("FastMoving") || !this.isAvailableUnit(gameState, ent))
     715           0 :                                 continue;
     716           0 :                         if (num++ < 2)
     717           0 :                                 continue;
     718           0 :                         ent.setMetadata(PlayerID, "plan", plan);
     719           0 :                         this.unitCollection.updateEnt(ent);
     720           0 :                         added = true;
     721             :                 }
     722           0 :                 return added;
     723             :         }
     724             : 
     725             :         // Assign all units without specific role.
     726           0 :         for (let ent of gameState.getOwnEntitiesByRole(undefined, true).values())
     727             :         {
     728           0 :                 if (ent.hasClasses(["!Unit", "Ship", "Support"]) ||
     729             :                         !this.isAvailableUnit(gameState, ent) ||
     730             :                         ent.attackTypes() === undefined)
     731           0 :                         continue;
     732           0 :                 ent.setMetadata(PlayerID, "plan", plan);
     733           0 :                 this.unitCollection.updateEnt(ent);
     734           0 :                 added = true;
     735             :         }
     736             :         // Add units previously in a plan, but which left it because needed for defense or attack finished.
     737           0 :         for (let ent of gameState.ai.HQ.attackManager.outOfPlan.values())
     738             :         {
     739           0 :                 if (!this.isAvailableUnit(gameState, ent))
     740           0 :                         continue;
     741           0 :                 ent.setMetadata(PlayerID, "plan", plan);
     742           0 :                 this.unitCollection.updateEnt(ent);
     743           0 :                 added = true;
     744             :         }
     745             : 
     746             :         // Finally add also some workers for the higher difficulties,
     747             :         // If Rush, assign all kind of workers, keeping only a minimum number of defenders
     748             :         // Otherwise, assign only some idle workers if too much of them
     749           0 :         if (this.Config.difficulty <= PETRA.DIFFICULTY_EASY)
     750           0 :                 return added;
     751             : 
     752           0 :         let num = 0;
     753           0 :         const numbase = {};
     754           0 :         let keep = this.type !== PETRA.AttackPlan.TYPE_RUSH ?
     755             :                 6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8;
     756           0 :         keep = Math.round(this.Config.popScaling * keep);
     757           0 :         for (const ent of gameState.getOwnEntitiesByRole(PETRA.Worker.ROLE_WORKER, true).values())
     758             :         {
     759           0 :                 if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent))
     760           0 :                         continue;
     761           0 :                 const baseID = ent.getMetadata(PlayerID, "base");
     762           0 :                 if (baseID)
     763           0 :                         numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1;
     764             :                 else
     765             :                 {
     766           0 :                         API3.warn("Petra problem ent without base ");
     767           0 :                         PETRA.dumpEntity(ent);
     768           0 :                         continue;
     769             :                 }
     770           0 :                 if (num++ < keep || numbase[baseID] < 5)
     771           0 :                         continue;
     772           0 :                 if (this.type !== PETRA.AttackPlan.TYPE_RUSH && ent.getMetadata(PlayerID, "subrole") !== PETRA.Worker.SUBROLE_IDLE)
     773           0 :                         continue;
     774           0 :                 ent.setMetadata(PlayerID, "plan", plan);
     775           0 :                 this.unitCollection.updateEnt(ent);
     776           0 :                 added = true;
     777             :         }
     778           0 :         return added;
     779             : };
     780             : 
     781           0 : PETRA.AttackPlan.prototype.isAvailableUnit = function(gameState, ent)
     782             : {
     783           0 :         if (!ent.position())
     784           0 :                 return false;
     785           0 :         if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 ||
     786             :             ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
     787           0 :                 return false;
     788           0 :         if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8))
     789           0 :                 return false;
     790           0 :         return true;
     791             : };
     792             : 
     793             : /** Reassign one (at each turn) FastMoving unit to fasten raid preparation. */
     794           0 : PETRA.AttackPlan.prototype.reassignFastUnit = function(gameState)
     795             : {
     796           0 :         for (let ent of this.unitCollection.values())
     797             :         {
     798           0 :                 if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined)
     799           0 :                         continue;
     800           0 :                 if (!ent.hasClasses(["FastMoving", "CitizenSoldier"]))
     801           0 :                         continue;
     802           0 :                 const raid = gameState.ai.HQ.attackManager.getAttackInPreparation(PETRA.AttackPlan.TYPE_RAID);
     803           0 :                 ent.setMetadata(PlayerID, "plan", raid.name);
     804           0 :                 this.unitCollection.updateEnt(ent);
     805           0 :                 raid.unitCollection.updateEnt(ent);
     806           0 :                 return;
     807             :         }
     808             : };
     809             : 
     810           0 : PETRA.AttackPlan.prototype.chooseTarget = function(gameState)
     811             : {
     812           0 :         if (this.targetPlayer === undefined)
     813             :         {
     814           0 :                 this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
     815           0 :                 if (this.targetPlayer === undefined)
     816           0 :                         return false;
     817             :         }
     818             : 
     819           0 :         this.target = this.getNearestTarget(gameState, this.rallyPoint);
     820           0 :         if (!this.target)
     821             :         {
     822           0 :                 if (this.uniqueTargetId)
     823           0 :                         return false;
     824             : 
     825             :                 // may-be all our previous enemey target (if not recomputed here) have been destroyed ?
     826           0 :                 this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
     827           0 :                 if (this.targetPlayer !== undefined)
     828           0 :                         this.target = this.getNearestTarget(gameState, this.rallyPoint);
     829           0 :                 if (!this.target)
     830           0 :                         return false;
     831             :         }
     832           0 :         this.targetPos = this.target.position();
     833             :         // redefine a new rally point for this target if we have a base on the same land
     834             :         // find a new one on the pseudo-nearest base (dist weighted by the size of the island)
     835           0 :         let targetIndex = PETRA.getLandAccess(gameState, this.target);
     836           0 :         let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
     837           0 :         if (targetIndex != rallyIndex)
     838             :         {
     839           0 :                 let distminSame = Math.min();
     840             :                 let rallySame;
     841           0 :                 let distminDiff = Math.min();
     842             :                 let rallyDiff;
     843           0 :                 for (const base of gameState.ai.HQ.baseManagers())
     844             :                 {
     845           0 :                         let anchor = base.anchor;
     846           0 :                         if (!anchor || !anchor.position())
     847           0 :                                 continue;
     848           0 :                         let dist = API3.SquareVectorDistance(anchor.position(), this.targetPos);
     849           0 :                         if (base.accessIndex == targetIndex)
     850             :                         {
     851           0 :                                 if (dist >= distminSame)
     852           0 :                                         continue;
     853           0 :                                 distminSame = dist;
     854           0 :                                 rallySame = anchor.position();
     855             :                         }
     856             :                         else
     857             :                         {
     858           0 :                                 dist /= Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]);
     859           0 :                                 if (dist >= distminDiff)
     860           0 :                                         continue;
     861           0 :                                 distminDiff = dist;
     862           0 :                                 rallyDiff = anchor.position();
     863             :                         }
     864             :                 }
     865             : 
     866           0 :                 if (rallySame)
     867             :                 {
     868           0 :                         this.rallyPoint = rallySame;
     869           0 :                         this.overseas = 0;
     870             :                 }
     871           0 :                 else if (rallyDiff)
     872             :                 {
     873           0 :                         rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff);
     874           0 :                         this.rallyPoint = rallyDiff;
     875           0 :                         let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex);
     876           0 :                         if (sea)
     877             :                         {
     878           0 :                                 this.overseas = sea;
     879           0 :                                 gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips);
     880             :                         }
     881             :                         else
     882             :                         {
     883           0 :                                 API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target" +
     884             :                                           " with indices " + rallyIndex + " " + targetIndex + " from " + this.target.templateName());
     885           0 :                                 return false;
     886             :                         }
     887             :                 }
     888             :         }
     889           0 :         else if (this.overseas)
     890           0 :                 this.overseas = 0;
     891             : 
     892           0 :         return true;
     893             : };
     894             : /**
     895             :  * sameLand true means that we look for a target for which we do not need to take a transport
     896             :  */
     897           0 : PETRA.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand)
     898             : {
     899           0 :         this.isBlocked = false;
     900             :         // Temporary variables needed by isValidTarget
     901           0 :         this.gameState = gameState;
     902           0 :         this.sameLand = sameLand && sameLand > 1 ? sameLand : false;
     903             : 
     904             :         let targets;
     905           0 :         if (this.uniqueTargetId)
     906             :         {
     907           0 :                 targets = new API3.EntityCollection(gameState.sharedScript);
     908           0 :                 let ent = gameState.getEntityById(this.uniqueTargetId);
     909           0 :                 if (ent)
     910           0 :                         targets.addEnt(ent);
     911             :         }
     912             :         else
     913             :         {
     914           0 :                 if (this.type === PETRA.AttackPlan.TYPE_RAID)
     915           0 :                         targets = this.raidTargetFinder(gameState);
     916           0 :                 else if (this.type === PETRA.AttackPlan.TYPE_RUSH || this.type === PETRA.AttackPlan.TYPE_DEFAULT)
     917             :                 {
     918           0 :                         targets = this.rushTargetFinder(gameState, this.targetPlayer);
     919           0 :                         if (!targets.hasEntities() && (this.hasSiegeUnits() || this.forced))
     920           0 :                                 targets = this.defaultTargetFinder(gameState, this.targetPlayer);
     921             :                 }
     922             :                 else
     923           0 :                         targets = this.defaultTargetFinder(gameState, this.targetPlayer);
     924             :         }
     925           0 :         if (!targets.hasEntities())
     926           0 :                 return undefined;
     927             : 
     928             :         // picking the nearest target
     929             :         let target;
     930           0 :         let minDist = Math.min();
     931           0 :         for (let ent of targets.values())
     932             :         {
     933           0 :                 if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") &&
     934             :                    (!ent.hasClass("Relic") || gameState.ai.HQ.victoryManager.targetedGaiaRelics.has(ent.id())))
     935           0 :                         continue;
     936             :                 // Do not bother with some pointless targets
     937           0 :                 if (!this.isValidTarget(ent))
     938           0 :                         continue;
     939           0 :                 let dist = API3.SquareVectorDistance(ent.position(), position);
     940             :                 // In normal attacks, disfavor fields
     941           0 :                 if (this.type !== PETRA.AttackPlan.TYPE_RUSH && this.type !== PETRA.AttackPlan.TYPE_RAID && ent.hasClass("Field"))
     942           0 :                         dist += 100000;
     943           0 :                 if (dist < minDist)
     944             :                 {
     945           0 :                         minDist = dist;
     946           0 :                         target = ent;
     947             :                 }
     948             :         }
     949           0 :         if (!target)
     950           0 :                 return undefined;
     951             : 
     952             :         // Check that we can reach this target
     953           0 :         target = this.checkTargetObstruction(gameState, target, position);
     954             : 
     955           0 :         if (!target)
     956           0 :                 return undefined;
     957           0 :         if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && target.hasClass("Relic"))
     958           0 :                 gameState.ai.HQ.victoryManager.targetedGaiaRelics.set(target.id(), [this.name]);
     959             :         // Rushes can change their enemy target if nothing found with the preferred enemy
     960             :         // Obstruction also can change the enemy target
     961           0 :         this.targetPlayer = target.owner();
     962           0 :         return target;
     963             : };
     964             : 
     965             : /**
     966             :  * Default target finder aims for conquest critical targets
     967             :  * We must apply the *same* selection (isValidTarget) as done in getNearestTarget
     968             :  */
     969           0 : PETRA.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy)
     970             : {
     971           0 :         let targets = new API3.EntityCollection(gameState.sharedScript);
     972           0 :         if (gameState.getVictoryConditions().has("wonder"))
     973           0 :                 for (let ent of gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")).values())
     974           0 :                         targets.addEnt(ent);
     975           0 :         if (gameState.getVictoryConditions().has("regicide"))
     976           0 :                 for (let ent of gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")).values())
     977           0 :                         targets.addEnt(ent);
     978           0 :         if (gameState.getVictoryConditions().has("capture_the_relic"))
     979           0 :                 for (let ent of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values())
     980           0 :                         targets.addEnt(ent);
     981           0 :         targets = targets.filter(this.isValidTarget, this);
     982           0 :         if (targets.hasEntities())
     983           0 :                 return targets;
     984             : 
     985           0 :         let validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this);
     986           0 :         targets = validTargets.filter(API3.Filters.byClass("CivCentre"));
     987           0 :         if (!targets.hasEntities())
     988           0 :                 targets = validTargets.filter(API3.Filters.byClass("ConquestCritical"));
     989             :         // If there's nothing, attack anything else that's less critical
     990           0 :         if (!targets.hasEntities())
     991           0 :                 targets = validTargets.filter(API3.Filters.byClass("Town"));
     992           0 :         if (!targets.hasEntities())
     993           0 :                 targets = validTargets.filter(API3.Filters.byClass("Village"));
     994             :         // No buildings, attack anything conquest critical, units included.
     995             :         // TODO Should add naval attacks against the last remaining ships.
     996           0 :         if (!targets.hasEntities())
     997           0 :                 targets = gameState.getEntities(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")).
     998             :                                                        filter(API3.Filters.not(API3.Filters.byClass("Ship")));
     999           0 :         return targets;
    1000             : };
    1001             : 
    1002           0 : PETRA.AttackPlan.prototype.isValidTarget = function(ent)
    1003             : {
    1004           0 :         if (!ent.position())
    1005           0 :                 return false;
    1006           0 :         if (this.sameLand && PETRA.getLandAccess(this.gameState, ent) != this.sameLand)
    1007           0 :                 return false;
    1008           0 :         return !ent.decaying() || ent.getDefaultArrow() || ent.isGarrisonHolder() && ent.garrisoned().length;
    1009             : };
    1010             : 
    1011             : /** Rush target finder aims at isolated non-defended buildings */
    1012           0 : PETRA.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy)
    1013             : {
    1014           0 :         let targets = new API3.EntityCollection(gameState.sharedScript);
    1015             :         let buildings;
    1016           0 :         if (playerEnemy !== undefined)
    1017           0 :                 buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray();
    1018             :         else
    1019           0 :                 buildings = gameState.getEnemyStructures().toEntityArray();
    1020           0 :         if (!buildings.length)
    1021           0 :                 return targets;
    1022             : 
    1023           0 :         this.position = this.unitCollection.getCentrePosition();
    1024           0 :         if (!this.position)
    1025           0 :                 this.position = this.rallyPoint;
    1026             : 
    1027             :         let target;
    1028           0 :         let minDist = Math.min();
    1029           0 :         for (let building of buildings)
    1030             :         {
    1031           0 :                 if (building.owner() == 0)
    1032           0 :                         continue;
    1033           0 :                 if (building.hasDefensiveFire())
    1034           0 :                         continue;
    1035           0 :                 if (!this.isValidTarget(building))
    1036           0 :                         continue;
    1037           0 :                 let pos = building.position();
    1038           0 :                 let defended = false;
    1039           0 :                 for (let defense of buildings)
    1040             :                 {
    1041           0 :                         if (!defense.hasDefensiveFire())
    1042           0 :                                 continue;
    1043           0 :                         let dist = API3.SquareVectorDistance(pos, defense.position());
    1044           0 :                         if (dist < 6400)   // TODO check on defense range rather than this fixed 80*80
    1045             :                         {
    1046           0 :                                 defended = true;
    1047           0 :                                 break;
    1048             :                         }
    1049             :                 }
    1050           0 :                 if (defended)
    1051           0 :                         continue;
    1052           0 :                 let dist = API3.SquareVectorDistance(pos, this.position);
    1053           0 :                 if (dist > minDist)
    1054           0 :                         continue;
    1055           0 :                 minDist = dist;
    1056           0 :                 target = building;
    1057             :         }
    1058           0 :         if (target)
    1059           0 :                 targets.addEnt(target);
    1060             : 
    1061           0 :         if (!targets.hasEntities() && this.type === PETRA.AttackPlan.TYPE_RUSH && playerEnemy)
    1062           0 :                 targets = this.rushTargetFinder(gameState);
    1063             : 
    1064           0 :         return targets;
    1065             : };
    1066             : 
    1067             : /** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */
    1068           0 : PETRA.AttackPlan.prototype.raidTargetFinder = function(gameState)
    1069             : {
    1070           0 :         let targets = new API3.EntityCollection(gameState.sharedScript);
    1071           0 :         for (let targetId of gameState.ai.HQ.defenseManager.targetList)
    1072             :         {
    1073           0 :                 let target = gameState.getEntityById(targetId);
    1074           0 :                 if (target && target.position())
    1075           0 :                         targets.addEnt(target);
    1076             :         }
    1077           0 :         return targets;
    1078             : };
    1079             : 
    1080             : /**
    1081             :  * Check that we can have a path to this target
    1082             :  * otherwise we may be blocked by walls and try to react accordingly
    1083             :  * This is done only when attacker and target are on the same land
    1084             :  */
    1085           0 : PETRA.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position)
    1086             : {
    1087           0 :         if (PETRA.getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position))
    1088           0 :                 return target;
    1089             : 
    1090           0 :         let targetPos = target.position();
    1091           0 :         let startPos = { "x": position[0], "y": position[1] };
    1092           0 :         let endPos = { "x": targetPos[0], "y": targetPos[1] };
    1093             :         let blocker;
    1094           0 :         let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default"));
    1095           0 :         if (!path.length)
    1096           0 :                 return undefined;
    1097             : 
    1098           0 :         let pathPos = [path[0].x, path[0].y];
    1099           0 :         let dist = API3.VectorDistance(pathPos, targetPos);
    1100           0 :         let radius = target.obstructionRadius().max;
    1101           0 :         for (let struct of gameState.getEnemyStructures().values())
    1102             :         {
    1103           0 :                 if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field"))
    1104           0 :                         continue;
    1105             :                 // we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate
    1106           0 :                 if (dist < radius + 10 && !struct.hasClass("Gate"))
    1107           0 :                         continue;
    1108             :                 // Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m
    1109             :                 // in the target direction would bring us inside its obstruction.
    1110           0 :                 let structPos = struct.position();
    1111           0 :                 let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist;
    1112           0 :                 let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist;
    1113             : 
    1114           0 :                 if (struct.get("Obstruction/Static"))
    1115             :                 {
    1116           0 :                         if (!struct.angle())
    1117           0 :                                 continue;
    1118           0 :                         let angle = struct.angle();
    1119           0 :                         let width = +struct.get("Obstruction/Static/@width");
    1120           0 :                         let depth = +struct.get("Obstruction/Static/@depth");
    1121           0 :                         let cosa = Math.cos(angle);
    1122           0 :                         let sina = Math.sin(angle);
    1123           0 :                         let u = x * cosa - y * sina;
    1124           0 :                         let v = x * sina + y * cosa;
    1125           0 :                         if (Math.abs(u) < width/2 && Math.abs(v) < depth/2)
    1126             :                         {
    1127           0 :                                 blocker = struct;
    1128           0 :                                 break;
    1129             :                         }
    1130             :                 }
    1131           0 :                 else if (struct.get("Obstruction/Obstructions"))
    1132             :                 {
    1133           0 :                         if (!struct.angle())
    1134           0 :                                 continue;
    1135           0 :                         let angle = struct.angle();
    1136           0 :                         let width = +struct.get("Obstruction/Obstructions/Door/@width");
    1137           0 :                         let depth = +struct.get("Obstruction/Obstructions/Door/@depth");
    1138           0 :                         let doorHalfWidth = width / 2;
    1139           0 :                         width += +struct.get("Obstruction/Obstructions/Left/@width");
    1140           0 :                         depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth"));
    1141           0 :                         width += +struct.get("Obstruction/Obstructions/Right/@width");
    1142           0 :                         depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth"));
    1143           0 :                         let cosa = Math.cos(angle);
    1144           0 :                         let sina = Math.sin(angle);
    1145           0 :                         let u = x * cosa - y * sina;
    1146           0 :                         let v = x * sina + y * cosa;
    1147           0 :                         if (Math.abs(u) < width/2 && Math.abs(v) < depth/2)
    1148             :                         {
    1149           0 :                                 blocker = struct;
    1150           0 :                                 break;
    1151             :                         }
    1152             :                         // check that the path does not cross this gate (could happen if not locked)
    1153           0 :                         for (let i = 1; i < path.length; ++i)
    1154             :                         {
    1155           0 :                                 let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina;
    1156           0 :                                 let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa;
    1157           0 :                                 let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina;
    1158           0 :                                 let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa;
    1159           0 :                                 if (v1 * v2 < 0)
    1160             :                                 {
    1161           0 :                                         let u0 = (u1*v2 - u2*v1) / (v2-v1);
    1162           0 :                                         if (Math.abs(u0) > doorHalfWidth)
    1163           0 :                                                 continue;
    1164           0 :                                         blocker = struct;
    1165           0 :                                         break;
    1166             :                                 }
    1167             :                         }
    1168           0 :                         if (blocker)
    1169           0 :                                 break;
    1170             :                 }
    1171           0 :                 else if (struct.get("Obstruction/Unit"))
    1172             :                 {
    1173           0 :                         let r = +this.get("Obstruction/Unit/@radius");
    1174           0 :                         if (x*x + y*y < r*r)
    1175             :                         {
    1176           0 :                                 blocker = struct;
    1177           0 :                                 break;
    1178             :                         }
    1179             :                 }
    1180             :         }
    1181             : 
    1182           0 :         if (blocker)
    1183             :         {
    1184           0 :                 this.isBlocked = true;
    1185           0 :                 return blocker;
    1186             :         }
    1187             : 
    1188           0 :         return target;
    1189             : };
    1190             : 
    1191           0 : PETRA.AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false)
    1192             : {
    1193           0 :         let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
    1194           0 :         let endAccess = PETRA.getLandAccess(gameState, this.target);
    1195           0 :         if (startAccess != endAccess)
    1196           0 :                 return false;
    1197             : 
    1198           0 :         Engine.ProfileStart("AI Compute path");
    1199           0 :         let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] };
    1200           0 :         let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] };
    1201           0 :         let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large"));
    1202           0 :         this.path = [];
    1203           0 :         this.path.push(this.targetPos);
    1204           0 :         for (let p in path)
    1205           0 :                 this.path.push([path[p].x, path[p].y]);
    1206           0 :         this.path.push(this.rallyPoint);
    1207           0 :         this.path.reverse();
    1208             :         // Change the rally point to something useful
    1209           0 :         if (!fixedRallyPoint)
    1210           0 :                 this.setRallyPoint(gameState);
    1211           0 :         Engine.ProfileStop();
    1212             : 
    1213           0 :         return true;
    1214             : };
    1215             : 
    1216             : /** Set rally point at the border of our territory */
    1217           0 : PETRA.AttackPlan.prototype.setRallyPoint = function(gameState)
    1218             : {
    1219           0 :         for (let i = 0; i < this.path.length; ++i)
    1220             :         {
    1221           0 :                 if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID)
    1222           0 :                         continue;
    1223             : 
    1224           0 :                 if (i === 0)
    1225           0 :                         this.rallyPoint = this.path[0];
    1226           0 :                 else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20))
    1227             :                 {
    1228           0 :                         this.rallyPoint = this.path[i-2];
    1229           0 :                         this.path.splice(0, i-2);
    1230             :                 }
    1231             :                 else
    1232             :                 {
    1233           0 :                         this.rallyPoint = this.path[i-1];
    1234           0 :                         this.path.splice(0, i-1);
    1235             :                 }
    1236           0 :                 break;
    1237             :         }
    1238             : };
    1239             : 
    1240             : /**
    1241             :  * Executes the attack plan, after this is executed the update function will be run every turn
    1242             :  * If we're here, it's because we have enough units.
    1243             :  */
    1244           0 : PETRA.AttackPlan.prototype.StartAttack = function(gameState)
    1245             : {
    1246           0 :         if (this.Config.debug > 1)
    1247           0 :                 API3.warn("start attack " + this.name + " with type " + this.type);
    1248             : 
    1249             :         // if our target was destroyed during preparation, choose a new one
    1250           0 :         if ((this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) &&
    1251             :             !this.chooseTarget(gameState))
    1252           0 :                 return false;
    1253             : 
    1254             :         // erase our queue. This will stop any leftover unit from being trained.
    1255           0 :         this.removeQueues(gameState);
    1256             : 
    1257           0 :         for (let ent of this.unitCollection.values())
    1258             :         {
    1259           0 :                 ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_WALKING);
    1260           0 :                 let stance = ent.isPackable() ? "standground" : "aggressive";
    1261           0 :                 if (ent.getStance() != stance)
    1262           0 :                         ent.setStance(stance);
    1263             :         }
    1264             : 
    1265           0 :         let rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
    1266           0 :         let targetAccess = PETRA.getLandAccess(gameState, this.target);
    1267           0 :         if (rallyAccess == targetAccess)
    1268             :         {
    1269           0 :                 if (!this.path)
    1270           0 :                         this.getPathToTarget(gameState, true);
    1271           0 :                 if (!this.path || !this.path[0][0] || !this.path[0][1])
    1272           0 :                         return false;
    1273           0 :                 this.overseas = 0;
    1274           0 :                 this.state = "walking";
    1275           0 :                 this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15);
    1276             :         }
    1277             :         else
    1278             :         {
    1279           0 :                 this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, targetAccess);
    1280           0 :                 if (!this.overseas)
    1281           0 :                         return false;
    1282           0 :                 this.state = "transporting";
    1283             :                 // TODO require a global transport for the collection,
    1284             :                 // and put back its state to "walking" when the transport is finished
    1285           0 :                 for (let ent of this.unitCollection.values())
    1286           0 :                         gameState.ai.HQ.navalManager.requireTransport(gameState, ent, rallyAccess, targetAccess, this.targetPos);
    1287             :         }
    1288           0 :         return true;
    1289             : };
    1290             : 
    1291             : /** Runs every turn after the attack is executed */
    1292           0 : PETRA.AttackPlan.prototype.update = function(gameState, events)
    1293             : {
    1294           0 :         if (!this.unitCollection.hasEntities())
    1295           0 :                 return 0;
    1296             : 
    1297           0 :         Engine.ProfileStart("Update Attack");
    1298             : 
    1299           0 :         this.position = this.unitCollection.getCentrePosition();
    1300             : 
    1301             :         // we are transporting our units, let's wait
    1302             :         // TODO instead of state "arrived", made a state "walking" with a new path
    1303           0 :         if (this.state == "transporting")
    1304           0 :                 this.UpdateTransporting(gameState, events);
    1305             : 
    1306           0 :         if (!this.position)
    1307             :         {
    1308           0 :                 Engine.ProfileStop();
    1309           0 :                 return this.unitCollection.length;
    1310             :         }
    1311             : 
    1312           0 :         if (this.state == "walking" && !this.UpdateWalking(gameState, events))
    1313             :         {
    1314           0 :                 Engine.ProfileStop();
    1315           0 :                 return 0;
    1316             :         }
    1317             : 
    1318           0 :         if (this.state === PETRA.AttackPlan.STATE_ARRIVED)
    1319             :         {
    1320             :                 // let's proceed on with whatever happens now.
    1321           0 :                 this.state = "";
    1322           0 :                 this.startingAttack = true;
    1323           0 :                 this.unitCollection.forEach(ent => {
    1324           0 :                         ent.stopMoving();
    1325           0 :                         ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_ATTACKING);
    1326             :                 });
    1327           0 :                 if (this.type === PETRA.AttackPlan.TYPE_RUSH)   // try to find a better target for rush
    1328             :                 {
    1329           0 :                         let newtarget = this.getNearestTarget(gameState, this.position);
    1330           0 :                         if (newtarget)
    1331             :                         {
    1332           0 :                                 this.target = newtarget;
    1333           0 :                                 this.targetPos = this.target.position();
    1334             :                         }
    1335             :                 }
    1336             :         }
    1337             : 
    1338             :         // basic state of attacking.
    1339           0 :         if (this.state == "")
    1340             :         {
    1341             :                 // First update the target and/or its position if needed
    1342           0 :                 if (!this.UpdateTarget(gameState))
    1343             :                 {
    1344           0 :                         Engine.ProfileStop();
    1345           0 :                         return false;
    1346             :                 }
    1347             : 
    1348           0 :                 let time = gameState.ai.elapsedTime;
    1349           0 :                 let attackedByStructure = {};
    1350           0 :                 for (let evt of events.Attacked)
    1351             :                 {
    1352           0 :                         if (!this.unitCollection.hasEntId(evt.target))
    1353           0 :                                 continue;
    1354           0 :                         let attacker = gameState.getEntityById(evt.attacker);
    1355           0 :                         let ourUnit = gameState.getEntityById(evt.target);
    1356           0 :                         if (!ourUnit || !ourUnit.position() || !attacker || !attacker.position())
    1357           0 :                                 continue;
    1358           0 :                         if (!attacker.hasClass("Unit"))
    1359             :                         {
    1360           0 :                                 attackedByStructure[evt.target] = true;
    1361           0 :                                 continue;
    1362             :                         }
    1363           0 :                         if (PETRA.isSiegeUnit(ourUnit))
    1364             :                         {       // if our siege units are attacked, we'll send some units to deal with enemies.
    1365           0 :                                 let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5);
    1366           0 :                                 for (let ent of collec.values())
    1367             :                                 {
    1368           0 :                                         if (PETRA.isSiegeUnit(ent))     // needed as mauryan elephants are not filtered out
    1369           0 :                                                 continue;
    1370             : 
    1371           0 :                                         let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
    1372           0 :                                         if (!ent.canAttackTarget(attacker, allowCapture))
    1373           0 :                                                 continue;
    1374           0 :                                         ent.attack(attacker.id(), allowCapture);
    1375           0 :                                         ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    1376             :                                 }
    1377             :                                 // And if this attacker is a non-ranged siege unit and our unit also, attack it
    1378           0 :                                 if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, PETRA.allowCapture(gameState, ourUnit, attacker)))
    1379             :                                 {
    1380           0 :                                         ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker));
    1381           0 :                                         ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    1382             :                                 }
    1383             :                         }
    1384             :                         else
    1385             :                         {
    1386           0 :                                 if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged"))
    1387             :                                 {
    1388             :                                         // do not react if our melee units are attacked by ranged one and we are blocked by walls
    1389             :                                         // TODO check that the attacker is from behind the wall
    1390           0 :                                         continue;
    1391             :                                 }
    1392           0 :                                 else if (PETRA.isSiegeUnit(attacker))
    1393             :                                 {       // if our unit is attacked by a siege unit, we'll send some melee units to help it.
    1394           0 :                                         let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
    1395           0 :                                         for (let ent of collec.values())
    1396             :                                         {
    1397           0 :                                                 let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
    1398           0 :                                                 if (!ent.canAttackTarget(attacker, allowCapture))
    1399           0 :                                                         continue;
    1400           0 :                                                 ent.attack(attacker.id(), allowCapture);
    1401           0 :                                                 ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    1402             :                                         }
    1403             :                                 }
    1404             :                                 else
    1405             :                                 {
    1406             :                                         // Look first for nearby units to help us if possible
    1407           0 :                                         let collec = this.unitCollection.filterNearest(ourUnit.position(), 2);
    1408           0 :                                         for (let ent of collec.values())
    1409             :                                         {
    1410           0 :                                                 let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
    1411           0 :                                                 if (PETRA.isSiegeUnit(ent) || !ent.canAttackTarget(attacker, allowCapture))
    1412           0 :                                                         continue;
    1413           0 :                                                 let orderData = ent.unitAIOrderData();
    1414           0 :                                                 if (orderData && orderData.length && orderData[0].target)
    1415             :                                                 {
    1416           0 :                                                         if (orderData[0].target === attacker.id())
    1417           0 :                                                                 continue;
    1418           0 :                                                         let target = gameState.getEntityById(orderData[0].target);
    1419           0 :                                                         if (target && !target.hasClasses(["Structure", "Support"]))
    1420           0 :                                                                 continue;
    1421             :                                                 }
    1422           0 :                                                 ent.attack(attacker.id(), allowCapture);
    1423           0 :                                                 ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    1424             :                                         }
    1425             :                                         // Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate
    1426             :                                         // also if our unit is attacking a range unit and the attacker is a melee unit, retaliate
    1427           0 :                                         let orderData = ourUnit.unitAIOrderData();
    1428           0 :                                         if (orderData && orderData.length && orderData[0].target)
    1429             :                                         {
    1430           0 :                                                 if (orderData[0].target === attacker.id())
    1431           0 :                                                         continue;
    1432           0 :                                                 let target = gameState.getEntityById(orderData[0].target);
    1433           0 :                                                 if (target && !target.hasClasses(["Structure", "Support"]))
    1434             :                                                 {
    1435           0 :                                                         if (!target.hasClass("Ranged") || !attacker.hasClass("Melee"))
    1436           0 :                                                                 continue;
    1437             :                                                 }
    1438             :                                         }
    1439           0 :                                         let allowCapture = PETRA.allowCapture(gameState, ourUnit, attacker);
    1440           0 :                                         if (ourUnit.canAttackTarget(attacker, allowCapture))
    1441             :                                         {
    1442           0 :                                                 ourUnit.attack(attacker.id(), allowCapture);
    1443           0 :                                                 ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    1444             :                                         }
    1445             :                                 }
    1446             :                         }
    1447             :                 }
    1448             : 
    1449           0 :                 let enemyUnits = gameState.getEnemyUnits(this.targetPlayer);
    1450           0 :                 let enemyStructures = gameState.getEnemyStructures(this.targetPlayer);
    1451             : 
    1452             :                 // Count the number of times an enemy is targeted, to prevent all units to follow the same target
    1453           0 :                 let unitTargets = {};
    1454           0 :                 for (let ent of this.unitCollection.values())
    1455             :                 {
    1456           0 :                         if (ent.hasClass("Ship"))     // TODO What to do with ships
    1457           0 :                                 continue;
    1458           0 :                         let orderData = ent.unitAIOrderData();
    1459           0 :                         if (!orderData || !orderData.length || !orderData[0].target)
    1460           0 :                                 continue;
    1461           0 :                         let targetId = orderData[0].target;
    1462           0 :                         let target = gameState.getEntityById(targetId);
    1463           0 :                         if (!target || target.hasClass("Structure"))
    1464           0 :                                 continue;
    1465           0 :                         if (!(targetId in unitTargets))
    1466             :                         {
    1467           0 :                                 if (PETRA.isSiegeUnit(target) || target.hasClass("Hero"))
    1468           0 :                                         unitTargets[targetId] = -8;
    1469           0 :                                 else if (target.hasClasses(["Champion", "Ship"]))
    1470           0 :                                         unitTargets[targetId] = -5;
    1471             :                                 else
    1472           0 :                                         unitTargets[targetId] = -3;
    1473             :                         }
    1474           0 :                         ++unitTargets[targetId];
    1475             :                 }
    1476           0 :                 let veto = {};
    1477           0 :                 for (let target in unitTargets)
    1478           0 :                         if (unitTargets[target] > 0)
    1479           0 :                                 veto[target] = true;
    1480             : 
    1481             :                 let targetClassesUnit;
    1482             :                 let targetClassesSiege;
    1483           0 :                 if (this.type === PETRA.AttackPlan.TYPE_RUSH)
    1484           0 :                         targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Tower", "Fortress"], "vetoEntities": veto };
    1485             :                 else
    1486             :                 {
    1487           0 :                         if (this.target.hasClass("Fortress"))
    1488           0 :                                 targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall"], "vetoEntities": veto };
    1489           0 :                         else if (this.target.hasClasses(["Palisade", "Wall"]))
    1490           0 :                                 targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto };
    1491             :                         else
    1492           0 :                                 targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Fortress"], "vetoEntities": veto };
    1493             :                 }
    1494           0 :                 if (this.target.hasClass("Structure"))
    1495           0 :                         targetClassesSiege = { "attack": ["Structure"], "avoid": [], "vetoEntities": veto };
    1496             :                 else
    1497           0 :                         targetClassesSiege = { "attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto };
    1498             : 
    1499             :                 // do not loose time destroying buildings which do not help enemy's defense and can be easily captured later
    1500           0 :                 if (this.target.hasDefensiveFire())
    1501             :                 {
    1502           0 :                         targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge");
    1503           0 :                         targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge");
    1504             :                 }
    1505             : 
    1506           0 :                 if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length)
    1507           0 :                         this.unitCollUpdateArray = this.unitCollection.toIdArray();
    1508             : 
    1509             :                 // Let's check a few units each time we update (currently 10) except when attack starts
    1510           0 :                 let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10;
    1511           0 :                 for (let check = 0; check < lgth; check++)
    1512             :                 {
    1513           0 :                         let ent = gameState.getEntityById(this.unitCollUpdateArray[check]);
    1514           0 :                         if (!ent || !ent.position())
    1515           0 :                                 continue;
    1516             :                         // Do not reassign units which have reacted to an attack in that same turn
    1517           0 :                         if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time)
    1518           0 :                                 continue;
    1519             : 
    1520             :                         let targetId;
    1521           0 :                         let orderData = ent.unitAIOrderData();
    1522           0 :                         if (orderData && orderData.length && orderData[0].target)
    1523           0 :                                 targetId = orderData[0].target;
    1524             : 
    1525             :                         // update the order if needed
    1526           0 :                         let needsUpdate = false;
    1527           0 :                         let maybeUpdate = false;
    1528           0 :                         let siegeUnit = PETRA.isSiegeUnit(ent);
    1529           0 :                         if (ent.isIdle())
    1530           0 :                                 needsUpdate = true;
    1531           0 :                         else if (siegeUnit && targetId)
    1532             :                         {
    1533           0 :                                 let target = gameState.getEntityById(targetId);
    1534           0 :                                 if (!target || gameState.isPlayerAlly(target.owner()))
    1535           0 :                                         needsUpdate = true;
    1536           0 :                                 else if (unitTargets[targetId] && unitTargets[targetId] > 0)
    1537             :                                 {
    1538           0 :                                         needsUpdate = true;
    1539           0 :                                         --unitTargets[targetId];
    1540             :                                 }
    1541           0 :                                 else if (!target.hasClass("Structure"))
    1542           0 :                                         maybeUpdate = true;
    1543             :                         }
    1544           0 :                         else if (targetId)
    1545             :                         {
    1546           0 :                                 let target = gameState.getEntityById(targetId);
    1547           0 :                                 if (!target || gameState.isPlayerAlly(target.owner()))
    1548           0 :                                         needsUpdate = true;
    1549           0 :                                 else if (unitTargets[targetId] && unitTargets[targetId] > 0)
    1550             :                                 {
    1551           0 :                                         needsUpdate = true;
    1552           0 :                                         --unitTargets[targetId];
    1553             :                                 }
    1554           0 :                                 else if (target.hasClass("Ship") && !ent.hasClass("Ship"))
    1555           0 :                                         maybeUpdate = true;
    1556           0 :                                 else if (attackedByStructure[ent.id()] && target.hasClass("Field"))
    1557           0 :                                         maybeUpdate = true;
    1558           0 :                                 else if (!ent.hasClass("FastMoving") && !ent.hasClass("Ranged") &&
    1559             :                                          target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING")
    1560           0 :                                         maybeUpdate = true;
    1561             :                         }
    1562             : 
    1563             :                         // don't update too soon if not necessary
    1564           0 :                         if (!needsUpdate)
    1565             :                         {
    1566           0 :                                 if (!maybeUpdate)
    1567           0 :                                         continue;
    1568           0 :                                 let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5;
    1569           0 :                                 let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime");
    1570           0 :                                 if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat)
    1571           0 :                                         continue;
    1572             :                         }
    1573           0 :                         ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
    1574           0 :                         let range = 60;
    1575           0 :                         let attackTypes = ent.attackTypes();
    1576           0 :                         if (this.isBlocked)
    1577             :                         {
    1578           0 :                                 if (attackTypes && attackTypes.indexOf("Ranged") !== -1)
    1579           0 :                                         range = ent.attackRange("Ranged").max;
    1580           0 :                                 else if (attackTypes && attackTypes.indexOf("Melee") !== -1)
    1581           0 :                                         range = ent.attackRange("Melee").max;
    1582             :                                 else
    1583           0 :                                         range = 10;
    1584             :                         }
    1585           0 :                         else if (attackTypes && attackTypes.indexOf("Ranged") !== -1)
    1586           0 :                                 range = 30 + ent.attackRange("Ranged").max;
    1587           0 :                         else if (ent.hasClass("FastMoving"))
    1588           0 :                                 range += 30;
    1589           0 :                         range *= range;
    1590           0 :                         let entAccess = PETRA.getLandAccess(gameState, ent);
    1591             :                         // Checking for gates if we're a siege unit.
    1592           0 :                         if (siegeUnit)
    1593             :                         {
    1594           0 :                                 let mStruct = enemyStructures.filter(enemy => {
    1595           0 :                                         if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
    1596           0 :                                                 return false;
    1597           0 :                                         if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
    1598           0 :                                                 return false;
    1599           0 :                                         if (enemy.foundationProgress() == 0)
    1600           0 :                                                 return false;
    1601           0 :                                         if (PETRA.getLandAccess(gameState, enemy) != entAccess)
    1602           0 :                                                 return false;
    1603           0 :                                         return true;
    1604             :                                 }).toEntityArray();
    1605           0 :                                 if (mStruct.length)
    1606             :                                 {
    1607           0 :                                         mStruct.sort((structa, structb) => {
    1608           0 :                                                 let vala = structa.costSum();
    1609           0 :                                                 if (structa.hasClass("Gate") && ent.canAttackClass("Wall"))
    1610           0 :                                                         vala += 10000;
    1611           0 :                                                 else if (structa.hasDefensiveFire())
    1612           0 :                                                         vala += 1000;
    1613           0 :                                                 else if (structa.hasClass("ConquestCritical"))
    1614           0 :                                                         vala += 200;
    1615           0 :                                                 let valb = structb.costSum();
    1616           0 :                                                 if (structb.hasClass("Gate") && ent.canAttackClass("Wall"))
    1617           0 :                                                         valb += 10000;
    1618           0 :                                                 else if (structb.hasDefensiveFire())
    1619           0 :                                                         valb += 1000;
    1620           0 :                                                 else if (structb.hasClass("ConquestCritical"))
    1621           0 :                                                         valb += 200;
    1622           0 :                                                 return valb - vala;
    1623             :                                         });
    1624           0 :                                         if (mStruct[0].hasClass("Gate"))
    1625           0 :                                                 ent.attack(mStruct[0].id(), PETRA.allowCapture(gameState, ent, mStruct[0]));
    1626             :                                         else
    1627             :                                         {
    1628           0 :                                                 let rand = randIntExclusive(0, mStruct.length * 0.2);
    1629           0 :                                                 ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand]));
    1630             :                                         }
    1631             :                                 }
    1632             :                                 else
    1633             :                                 {
    1634           0 :                                         if (!ent.hasClass("Ranged"))
    1635             :                                         {
    1636           0 :                                                 let targetClasses = { "attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto };
    1637           0 :                                                 ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses);
    1638             :                                         }
    1639             :                                         else
    1640           0 :                                                 ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege);
    1641             :                                 }
    1642             :                         }
    1643             :                         else
    1644             :                         {
    1645           0 :                                 const nearby = !ent.hasClasses(["FastMoving", "Ranged"]);
    1646           0 :                                 let mUnit = enemyUnits.filter(enemy => {
    1647           0 :                                         if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
    1648           0 :                                                 return false;
    1649           0 :                                         if (enemy.hasClass("Animal"))
    1650           0 :                                                 return false;
    1651           0 :                                         if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING")
    1652           0 :                                                 return false;
    1653           0 :                                         let dist = API3.SquareVectorDistance(enemy.position(), ent.position());
    1654           0 :                                         if (dist > range)
    1655           0 :                                                 return false;
    1656           0 :                                         if (PETRA.getLandAccess(gameState, enemy) != entAccess)
    1657           0 :                                                 return false;
    1658             :                                         // if already too much units targeting this enemy, let's continue towards our main target
    1659           0 :                                         if (veto[enemy.id()] && API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500)
    1660           0 :                                                 return false;
    1661           0 :                                         enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist));
    1662           0 :                                         return true;
    1663             :                                 }, this).toEntityArray();
    1664           0 :                                 if (mUnit.length)
    1665             :                                 {
    1666           0 :                                         mUnit.sort((unitA, unitB) => {
    1667           0 :                                                 let vala = unitA.hasClass("Support") ? 50 : 0;
    1668           0 :                                                 if (ent.counters(unitA))
    1669           0 :                                                         vala += 100;
    1670           0 :                                                 let valb = unitB.hasClass("Support") ? 50 : 0;
    1671           0 :                                                 if (ent.counters(unitB))
    1672           0 :                                                         valb += 100;
    1673           0 :                                                 let distA = unitA.getMetadata(PlayerID, "distance");
    1674           0 :                                                 let distB = unitB.getMetadata(PlayerID, "distance");
    1675           0 :                                                 if (distA && distB)
    1676             :                                                 {
    1677           0 :                                                         vala -= distA;
    1678           0 :                                                         valb -= distB;
    1679             :                                                 }
    1680           0 :                                                 if (veto[unitA.id()])
    1681           0 :                                                         vala -= 20000;
    1682           0 :                                                 if (veto[unitB.id()])
    1683           0 :                                                         valb -= 20000;
    1684           0 :                                                 return valb - vala;
    1685             :                                         });
    1686           0 :                                         let rand = randIntExclusive(0, mUnit.length * 0.1);
    1687           0 :                                         ent.attack(mUnit[rand].id(), PETRA.allowCapture(gameState, ent, mUnit[rand]));
    1688             :                                 }
    1689             :                                 // This may prove dangerous as we may be blocked by something we
    1690             :                                 // cannot attack. See similar behaviour at #5741.
    1691           0 :                                 else if (this.isBlocked && ent.canAttackTarget(this.target, false))
    1692           0 :                                         ent.attack(this.target.id(), false);
    1693           0 :                                 else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500)
    1694             :                                 {
    1695           0 :                                         let targetClasses = targetClassesUnit;
    1696           0 :                                         if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING")     // we may be blocked by walls, attack everything
    1697             :                                         {
    1698           0 :                                                 if (!ent.hasClasses(["Ranged", "Ship"]))
    1699           0 :                                                         targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto };
    1700             :                                                 else
    1701           0 :                                                         targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto };
    1702             :                                         }
    1703           0 :                                         else if (!ent.hasClasses(["Ranged", "Ship"]))
    1704           0 :                                                 targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto };
    1705           0 :                                         ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses);
    1706             :                                 }
    1707             :                                 else
    1708             :                                 {
    1709           0 :                                         let mStruct = enemyStructures.filter(enemy => {
    1710           0 :                                                 if (this.isBlocked && enemy.id() != this.target.id())
    1711           0 :                                                         return false;
    1712           0 :                                                 if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
    1713           0 :                                                         return false;
    1714           0 :                                                 if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
    1715           0 :                                                         return false;
    1716           0 :                                                 if (PETRA.getLandAccess(gameState, enemy) != entAccess)
    1717           0 :                                                         return false;
    1718           0 :                                                 return true;
    1719             :                                         }, this).toEntityArray();
    1720           0 :                                         if (mStruct.length)
    1721             :                                         {
    1722           0 :                                                 mStruct.sort((structa, structb) => {
    1723           0 :                                                         let vala = structa.costSum();
    1724           0 :                                                         if (structa.hasClass("Gate") && ent.canAttackClass("Wall"))
    1725           0 :                                                                 vala += 10000;
    1726           0 :                                                         else if (structa.hasClass("ConquestCritical"))
    1727           0 :                                                                 vala += 100;
    1728           0 :                                                         let valb = structb.costSum();
    1729           0 :                                                         if (structb.hasClass("Gate") && ent.canAttackClass("Wall"))
    1730           0 :                                                                 valb += 10000;
    1731           0 :                                                         else if (structb.hasClass("ConquestCritical"))
    1732           0 :                                                                 valb += 100;
    1733           0 :                                                         return valb - vala;
    1734             :                                                 });
    1735           0 :                                                 if (mStruct[0].hasClass("Gate"))
    1736           0 :                                                         ent.attack(mStruct[0].id(), false);
    1737             :                                                 else
    1738             :                                                 {
    1739           0 :                                                         let rand = randIntExclusive(0, mStruct.length * 0.2);
    1740           0 :                                                         ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand]));
    1741             :                                                 }
    1742             :                                         }
    1743           0 :                                         else if (needsUpdate)  // really nothing   let's try to help our nearest unit
    1744             :                                         {
    1745           0 :                                                 let distmin = Math.min();
    1746             :                                                 let attacker;
    1747           0 :                                                 this.unitCollection.forEach(unit => {
    1748           0 :                                                         if (!unit.position())
    1749           0 :                                                                 return;
    1750           0 :                                                         if (unit.unitAIState().split(".")[1] != "COMBAT" || !unit.unitAIOrderData().length ||
    1751             :                                                                 !unit.unitAIOrderData()[0].target)
    1752           0 :                                                                 return;
    1753           0 :                                                         let target = gameState.getEntityById(unit.unitAIOrderData()[0].target);
    1754           0 :                                                         if (!target)
    1755           0 :                                                                 return;
    1756           0 :                                                         let dist = API3.SquareVectorDistance(unit.position(), ent.position());
    1757           0 :                                                         if (dist > distmin)
    1758           0 :                                                                 return;
    1759           0 :                                                         distmin = dist;
    1760           0 :                                                         if (!ent.canAttackTarget(target, PETRA.allowCapture(gameState, ent, target)))
    1761           0 :                                                                 return;
    1762           0 :                                                         attacker = target;
    1763             :                                                 });
    1764           0 :                                                 if (attacker)
    1765           0 :                                                         ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker));
    1766             :                                         }
    1767             :                                 }
    1768             :                         }
    1769             :                 }
    1770           0 :                 this.unitCollUpdateArray.splice(0, lgth);
    1771           0 :                 this.startingAttack = false;
    1772             : 
    1773             :                 // check if this enemy has resigned
    1774           0 :                 if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0)
    1775           0 :                         this.target = undefined;
    1776             :         }
    1777           0 :         this.lastPosition = this.position;
    1778           0 :         Engine.ProfileStop();
    1779             : 
    1780           0 :         return this.unitCollection.length;
    1781             : };
    1782             : 
    1783           0 : PETRA.AttackPlan.prototype.UpdateTransporting = function(gameState, events)
    1784             : {
    1785           0 :         let done = true;
    1786           0 :         for (let ent of this.unitCollection.values())
    1787             :         {
    1788           0 :                 if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined)
    1789           0 :                         Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 2, 0] });
    1790           0 :                 else if (this.Config.debug > 1)
    1791           0 :                         Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [1, 1, 1] });
    1792           0 :                 if (!done)
    1793           0 :                         continue;
    1794           0 :                 if (ent.getMetadata(PlayerID, "transport") !== undefined)
    1795           0 :                         done = false;
    1796             :         }
    1797             : 
    1798           0 :         if (done)
    1799             :         {
    1800           0 :                 this.state = PETRA.AttackPlan.STATE_ARRIVED;
    1801           0 :                 return;
    1802             :         }
    1803             : 
    1804             :         // if we are attacked while waiting the rest of the army, retaliate
    1805           0 :         for (let evt of events.Attacked)
    1806             :         {
    1807           0 :                 if (!this.unitCollection.hasEntId(evt.target))
    1808           0 :                         continue;
    1809           0 :                 let attacker = gameState.getEntityById(evt.attacker);
    1810           0 :                 if (!attacker || !gameState.getEntityById(evt.target))
    1811           0 :                         continue;
    1812           0 :                 for (let ent of this.unitCollection.values())
    1813             :                 {
    1814           0 :                         if (ent.getMetadata(PlayerID, "transport") !== undefined)
    1815           0 :                                 continue;
    1816             : 
    1817           0 :                         let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
    1818           0 :                         if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture))
    1819           0 :                                 continue;
    1820           0 :                         ent.attack(attacker.id(), allowCapture);
    1821             :                 }
    1822           0 :                 break;
    1823             :         }
    1824             : };
    1825             : 
    1826           0 : PETRA.AttackPlan.prototype.UpdateWalking = function(gameState, events)
    1827             : {
    1828             :         // we're marching towards the target
    1829             :         // Let's check if any of our unit has been attacked.
    1830             :         // In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building
    1831             :         // or if we reached the enemy base. Different plans may react differently.
    1832           0 :         let attackedNB = 0;
    1833           0 :         let attackedUnitNB = 0;
    1834           0 :         for (let evt of events.Attacked)
    1835             :         {
    1836           0 :                 if (!this.unitCollection.hasEntId(evt.target))
    1837           0 :                         continue;
    1838           0 :                 let attacker = gameState.getEntityById(evt.attacker);
    1839           0 :                 if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0))
    1840             :                 {
    1841           0 :                         attackedNB++;
    1842           0 :                         if (attacker.hasClass("Unit"))
    1843           0 :                                 attackedUnitNB++;
    1844             :                 }
    1845             :         }
    1846             :         // Are we arrived at destination ?
    1847           0 :         if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits()))
    1848             :         {
    1849           0 :                 if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3)
    1850             :                 {
    1851           0 :                         this.state = PETRA.AttackPlan.STATE_ARRIVED;
    1852           0 :                         return true;
    1853             :                 }
    1854             :         }
    1855             : 
    1856             :         // basically haven't moved an inch: very likely stuck)
    1857           0 :         if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0)
    1858             :         {
    1859             :                 // check for stuck siege units
    1860           0 :                 let farthest = 0;
    1861             :                 let farthestEnt;
    1862           0 :                 for (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values())
    1863             :                 {
    1864           0 :                         let dist = API3.SquareVectorDistance(ent.position(), this.position);
    1865           0 :                         if (dist < farthest)
    1866           0 :                                 continue;
    1867           0 :                         farthest = dist;
    1868           0 :                         farthestEnt = ent;
    1869             :                 }
    1870           0 :                 if (farthestEnt)
    1871           0 :                         farthestEnt.destroy();
    1872             :         }
    1873           0 :         if (gameState.ai.playedTurn % 5 === 0)
    1874           0 :                 this.position5TurnsAgo = this.position;
    1875             : 
    1876           0 :         if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0)
    1877             :         {
    1878           0 :                 if (!this.path[0][0] || !this.path[0][1])
    1879           0 :                         API3.warn("Start: Problem with path " + uneval(this.path));
    1880             :                 // We're stuck, presumably. Check if there are no walls just close to us.
    1881           0 :                 for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "Wall"])).values())
    1882             :                 {
    1883           0 :                         if (API3.SquareVectorDistance(this.position, ent.position()) > 800)
    1884           0 :                                 continue;
    1885           0 :                         let enemyClass = ent.hasClass("Wall") ? "Wall" : "Palisade";
    1886             :                         // there are walls, so check if we can attack
    1887           0 :                         if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities())
    1888             :                         {
    1889           0 :                                 if (this.Config.debug > 1)
    1890           0 :                                         API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy.");
    1891           0 :                                 this.state = PETRA.AttackPlan.STATE_ARRIVED;
    1892           0 :                                 return true;
    1893             :                         }
    1894             :                         // abort plan
    1895           0 :                         if (this.Config.debug > 1)
    1896           0 :                                 API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up.");
    1897           0 :                         return false;
    1898             :                 }
    1899             : 
    1900             :                 // this.unitCollection.move(this.path[0][0], this.path[0][1]);
    1901           0 :                 this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]);
    1902             :         }
    1903             : 
    1904             :         // check if our units are close enough from the next waypoint.
    1905           0 :         if (API3.SquareVectorDistance(this.position, this.targetPos) < 10000)
    1906             :         {
    1907           0 :                 if (this.Config.debug > 1)
    1908           0 :                         API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
    1909           0 :                 this.state = PETRA.AttackPlan.STATE_ARRIVED;
    1910           0 :                 return true;
    1911             :         }
    1912           0 :         else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600)
    1913             :         {
    1914           0 :                 this.path.shift();
    1915           0 :                 if (this.path.length)
    1916           0 :                         this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15);
    1917             :                 else
    1918             :                 {
    1919           0 :                         if (this.Config.debug > 1)
    1920           0 :                                 API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
    1921           0 :                         this.state = PETRA.AttackPlan.STATE_ARRIVED;
    1922           0 :                         return true;
    1923             :                 }
    1924             :         }
    1925             : 
    1926           0 :         return true;
    1927             : };
    1928             : 
    1929           0 : PETRA.AttackPlan.prototype.UpdateTarget = function(gameState)
    1930             : {
    1931             :         // First update the target position in case it's a unit (and check if it has garrisoned)
    1932           0 :         if (this.target && this.target.hasClass("Unit"))
    1933             :         {
    1934           0 :                 this.targetPos = this.target.position();
    1935           0 :                 if (!this.targetPos)
    1936             :                 {
    1937           0 :                         let holder = PETRA.getHolder(gameState, this.target);
    1938           0 :                         if (holder && gameState.isPlayerEnemy(holder.owner()))
    1939             :                         {
    1940           0 :                                 this.target = holder;
    1941           0 :                                 this.targetPos = holder.position();
    1942             :                         }
    1943             :                         else
    1944           0 :                                 this.target = undefined;
    1945             :                 }
    1946             :         }
    1947             :         // Then update the target if needed:
    1948           0 :         if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer))
    1949             :         {
    1950           0 :                 this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
    1951           0 :                 if (this.targetPlayer === undefined)
    1952           0 :                         return false;
    1953             : 
    1954           0 :                 if (this.target && this.target.owner() !== this.targetPlayer)
    1955           0 :                         this.target = undefined;
    1956             :         }
    1957           0 :         if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0)  // this enemy has resigned
    1958           0 :                 this.target = undefined;
    1959             : 
    1960           0 :         if (!this.target || !gameState.getEntityById(this.target.id()))
    1961             :         {
    1962           0 :                 if (this.Config.debug > 1)
    1963           0 :                         API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching.");
    1964           0 :                 let accessIndex = this.getAttackAccess(gameState);
    1965           0 :                 this.target = this.getNearestTarget(gameState, this.position, accessIndex);
    1966           0 :                 if (!this.target)
    1967             :                 {
    1968           0 :                         if (this.uniqueTargetId)
    1969           0 :                                 return false;
    1970             : 
    1971             :                         // Check if we could help any current attack
    1972           0 :                         let attackManager = gameState.ai.HQ.attackManager;
    1973           0 :                         for (let attackType in attackManager.startedAttacks)
    1974             :                         {
    1975           0 :                                 for (let attack of attackManager.startedAttacks[attackType])
    1976             :                                 {
    1977           0 :                                         if (attack.name == this.name)
    1978           0 :                                                 continue;
    1979           0 :                                         if (!attack.target || !gameState.getEntityById(attack.target.id()) ||
    1980             :                                             !gameState.isPlayerEnemy(attack.target.owner()))
    1981           0 :                                                 continue;
    1982           0 :                                         if (accessIndex != PETRA.getLandAccess(gameState, attack.target))
    1983           0 :                                                 continue;
    1984           0 :                                         if (attack.target.owner() == 0 && attack.targetPlayer != 0)     // looks like it has resigned
    1985           0 :                                                 continue;
    1986           0 :                                         if (!gameState.isPlayerEnemy(attack.targetPlayer))
    1987           0 :                                                 continue;
    1988           0 :                                         this.target = attack.target;
    1989           0 :                                         this.targetPlayer = attack.targetPlayer;
    1990           0 :                                         this.targetPos = this.target.position();
    1991           0 :                                         return true;
    1992             :                                 }
    1993             :                         }
    1994             : 
    1995             :                         // If not, let's look for another enemy
    1996           0 :                         if (!this.target)
    1997             :                         {
    1998           0 :                                 this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
    1999           0 :                                 if (this.targetPlayer !== undefined)
    2000           0 :                                         this.target = this.getNearestTarget(gameState, this.position, accessIndex);
    2001           0 :                                 if (!this.target)
    2002             :                                 {
    2003           0 :                                         if (this.Config.debug > 1)
    2004           0 :                                                 API3.warn("No new target found. Remaining units " + this.unitCollection.length);
    2005           0 :                                         return false;
    2006             :                                 }
    2007             :                         }
    2008           0 :                         if (this.Config.debug > 1)
    2009           0 :                                 API3.warn("We will help one of our other attacks");
    2010             :                 }
    2011           0 :                 this.targetPos = this.target.position();
    2012             :         }
    2013           0 :         return true;
    2014             : };
    2015             : 
    2016             : /** reset any units */
    2017           0 : PETRA.AttackPlan.prototype.Abort = function(gameState)
    2018             : {
    2019           0 :         this.unitCollection.unregister();
    2020           0 :         if (this.unitCollection.hasEntities())
    2021             :         {
    2022             :                 // If the attack was started, look for a good rallyPoint to withdraw
    2023             :                 let rallyPoint;
    2024           0 :                 if (this.isStarted())
    2025             :                 {
    2026           0 :                         let access = this.getAttackAccess(gameState);
    2027           0 :                         let dist = Math.min();
    2028           0 :                         if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access)
    2029             :                         {
    2030           0 :                                 rallyPoint = this.rallyPoint;
    2031           0 :                                 dist = API3.SquareVectorDistance(this.position, rallyPoint);
    2032             :                         }
    2033             :                         // Then check if we have a nearer base (in case this attack has captured one)
    2034           0 :                         for (const base of gameState.ai.HQ.baseManagers())
    2035             :                         {
    2036           0 :                                 if (!base.anchor || !base.anchor.position())
    2037           0 :                                         continue;
    2038           0 :                                 if (PETRA.getLandAccess(gameState, base.anchor) != access)
    2039           0 :                                         continue;
    2040           0 :                                 let newdist = API3.SquareVectorDistance(this.position, base.anchor.position());
    2041           0 :                                 if (newdist > dist)
    2042           0 :                                         continue;
    2043           0 :                                 dist = newdist;
    2044           0 :                                 rallyPoint = base.anchor.position();
    2045             :                         }
    2046             :                 }
    2047             : 
    2048           0 :                 for (let ent of this.unitCollection.values())
    2049             :                 {
    2050           0 :                         if (ent.getMetadata(PlayerID, "role") === PETRA.Worker.ROLE_ATTACK)
    2051           0 :                                 ent.stopMoving();
    2052           0 :                         if (rallyPoint)
    2053           0 :                                 ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15);
    2054           0 :                         this.removeUnit(ent);
    2055             :                 }
    2056             :         }
    2057             : 
    2058           0 :         for (let unitCat in this.unitStat)
    2059           0 :                 this.unit[unitCat].unregister();
    2060             : 
    2061           0 :         this.removeQueues(gameState);
    2062             : };
    2063             : 
    2064           0 : PETRA.AttackPlan.prototype.removeUnit = function(ent, update)
    2065             : {
    2066           0 :         if (ent.getMetadata(PlayerID, "role") === PETRA.Worker.ROLE_ATTACK)
    2067             :         {
    2068           0 :                 if (ent.hasClass("CitizenSoldier"))
    2069           0 :                         ent.setMetadata(PlayerID, "role", PETRA.Worker.ROLE_WORKER);
    2070             :                 else
    2071           0 :                         ent.setMetadata(PlayerID, "role", undefined);
    2072           0 :                 ent.setMetadata(PlayerID, "subrole", undefined);
    2073             :         }
    2074           0 :         ent.setMetadata(PlayerID, "plan", -1);
    2075           0 :         if (update)
    2076           0 :                 this.unitCollection.updateEnt(ent);
    2077             : };
    2078             : 
    2079           0 : PETRA.AttackPlan.prototype.checkEvents = function(gameState, events)
    2080             : {
    2081           0 :         for (let evt of events.EntityRenamed)
    2082             :         {
    2083           0 :                 if (!this.target || this.target.id() != evt.entity)
    2084           0 :                         continue;
    2085           0 :                 if (this.type === PETRA.AttackPlan.TYPE_RAID && !this.isStarted())
    2086           0 :                         this.target = undefined;
    2087             :                 else
    2088           0 :                         this.target = gameState.getEntityById(evt.newentity);
    2089           0 :                 if (this.target)
    2090           0 :                         this.targetPos = this.target.position();
    2091             :         }
    2092             : 
    2093           0 :         for (let evt of events.OwnershipChanged)        // capture event
    2094           0 :                 if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to))
    2095           0 :                         this.target = undefined;
    2096             : 
    2097           0 :         for (let evt of events.PlayerDefeated)
    2098             :         {
    2099           0 :                 if (this.targetPlayer !== evt.playerId)
    2100           0 :                         continue;
    2101           0 :                 this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
    2102           0 :                 this.target = undefined;
    2103             :         }
    2104             : 
    2105           0 :         if (!this.overseas || this.state !== PETRA.AttackPlan.STATE_UNEXECUTED)
    2106           0 :                 return;
    2107             :         // let's check if an enemy has built a structure at our access
    2108           0 :         for (let evt of events.Create)
    2109             :         {
    2110           0 :                 let ent = gameState.getEntityById(evt.entity);
    2111           0 :                 if (!ent || !ent.position() || !ent.hasClass("Structure"))
    2112           0 :                         continue;
    2113           0 :                 if (!gameState.isPlayerEnemy(ent.owner()))
    2114           0 :                         continue;
    2115           0 :                 let access = PETRA.getLandAccess(gameState, ent);
    2116           0 :                 for (const base of gameState.ai.HQ.baseManagers())
    2117             :                 {
    2118           0 :                         if (!base.anchor || !base.anchor.position())
    2119           0 :                                 continue;
    2120           0 :                         if (base.accessIndex != access)
    2121           0 :                                 continue;
    2122           0 :                         this.overseas = 0;
    2123           0 :                         this.rallyPoint = base.anchor.position();
    2124             :                 }
    2125             :         }
    2126             : };
    2127             : 
    2128           0 : PETRA.AttackPlan.prototype.waitingForTransport = function()
    2129             : {
    2130           0 :         for (let ent of this.unitCollection.values())
    2131           0 :                 if (ent.getMetadata(PlayerID, "transport") !== undefined)
    2132           0 :                         return true;
    2133           0 :         return false;
    2134             : };
    2135             : 
    2136           0 : PETRA.AttackPlan.prototype.hasSiegeUnits = function()
    2137             : {
    2138           0 :         for (let ent of this.unitCollection.values())
    2139           0 :                 if (PETRA.isSiegeUnit(ent))
    2140           0 :                         return true;
    2141           0 :         return false;
    2142             : };
    2143             : 
    2144           0 : PETRA.AttackPlan.prototype.hasForceOrder = function(data, value)
    2145             : {
    2146           0 :         for (let ent of this.unitCollection.values())
    2147             :         {
    2148           0 :                 if (data && +ent.getMetadata(PlayerID, data) !== value)
    2149           0 :                         continue;
    2150           0 :                 let orders = ent.unitAIOrderData();
    2151           0 :                 for (let order of orders)
    2152           0 :                         if (order.force)
    2153           0 :                                 return true;
    2154             :         }
    2155           0 :         return false;
    2156             : };
    2157             : 
    2158             : /**
    2159             :  * The center position of this attack may be in an inaccessible area. So we use the access
    2160             :  * of the unit nearest to this center position.
    2161             :  */
    2162           0 : PETRA.AttackPlan.prototype.getAttackAccess = function(gameState)
    2163             : {
    2164           0 :         for (let ent of this.unitCollection.filterNearest(this.position, 1).values())
    2165           0 :                 return PETRA.getLandAccess(gameState, ent);
    2166             : 
    2167           0 :         return 0;
    2168             : };
    2169             : 
    2170           0 : PETRA.AttackPlan.prototype.debugAttack = function()
    2171             : {
    2172           0 :         API3.warn("---------- attack " + this.name);
    2173           0 :         for (let unitCat in this.unitStat)
    2174             :         {
    2175           0 :                 let Unit = this.unitStat[unitCat];
    2176           0 :                 API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize);
    2177             :         }
    2178           0 :         API3.warn("------------------------------");
    2179             : };
    2180             : 
    2181           0 : PETRA.AttackPlan.prototype.Serialize = function()
    2182             : {
    2183           0 :         let properties = {
    2184             :                 "name": this.name,
    2185             :                 "type": this.type,
    2186             :                 "state": this.state,
    2187             :                 "forced": this.forced,
    2188             :                 "rallyPoint": this.rallyPoint,
    2189             :                 "overseas": this.overseas,
    2190             :                 "paused": this.paused,
    2191             :                 "maxCompletingTime": this.maxCompletingTime,
    2192             :                 "neededShips": this.neededShips,
    2193             :                 "unitStat": this.unitStat,
    2194             :                 "siegeState": this.siegeState,
    2195             :                 "position5TurnsAgo": this.position5TurnsAgo,
    2196             :                 "lastPosition": this.lastPosition,
    2197             :                 "position": this.position,
    2198             :                 "isBlocked": this.isBlocked,
    2199             :                 "targetPlayer": this.targetPlayer,
    2200             :                 "target": this.target !== undefined ? this.target.id() : undefined,
    2201             :                 "targetPos": this.targetPos,
    2202             :                 "uniqueTargetId": this.uniqueTargetId,
    2203             :                 "path": this.path
    2204             :         };
    2205             : 
    2206           0 :         return { "properties": properties };
    2207             : };
    2208             : 
    2209           0 : PETRA.AttackPlan.prototype.Deserialize = function(gameState, data)
    2210             : {
    2211           0 :         for (let key in data.properties)
    2212           0 :                 this[key] = data.properties[key];
    2213             : 
    2214           0 :         if (this.target)
    2215           0 :                 this.target = gameState.getEntityById(this.target);
    2216             : 
    2217           0 :         this.failed = undefined;
    2218             : };

Generated by: LCOV version 1.14