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

          Line data    Source code
       1             : /**
       2             :  * Base Manager
       3             :  * Handles lower level economic stuffs.
       4             :  * Some tasks:
       5             :  *  -tasking workers: gathering/hunting/building/repairing?/scouting/plans.
       6             :  *  -giving feedback/estimates on GR
       7             :  *  -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans.
       8             :  *  -getting good spots for dropsites
       9             :  *  -managing dropsite use in the base
      10             :  *  -updating whatever needs updating, keeping track of stuffs (rebuilding needs…)
      11             :  */
      12             : 
      13           0 : PETRA.BaseManager = function(gameState, basesManager)
      14             : {
      15           0 :         this.Config = basesManager.Config;
      16           0 :         this.ID = gameState.ai.uniqueIDs.bases++;
      17           0 :         this.basesManager = basesManager;
      18             : 
      19             :         // anchor building: seen as the main building of the base. Needs to have territorial influence
      20           0 :         this.anchor = undefined;
      21           0 :         this.anchorId = undefined;
      22           0 :         this.accessIndex = undefined;
      23             : 
      24             :         // Maximum distance (from any dropsite) to look for resources
      25             :         // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max
      26           0 :         this.maxDistResourceSquare = 360*360;
      27             : 
      28           0 :         this.constructing = false;
      29             :         // Defenders to train in this cc when its construction is finished
      30           0 :         this.neededDefenders = this.Config.difficulty > PETRA.DIFFICULTY_EASY ? 3 + 2*(this.Config.difficulty - 3) : 0;
      31             : 
      32             :         // vector for iterating, to check one use the HQ map.
      33           0 :         this.territoryIndices = [];
      34             : 
      35           0 :         this.timeNextIdleCheck = 0;
      36             : };
      37             : 
      38             : 
      39           0 : PETRA.BaseManager.STATE_WITH_ANCHOR = "anchored";
      40             : 
      41             : /**
      42             :  * New base with a foundation anchor.
      43             :  */
      44           0 : PETRA.BaseManager.STATE_UNCONSTRUCTED = "unconstructed";
      45             : 
      46             : /**
      47             :  * Captured base with an anchor.
      48             :  */
      49           0 : PETRA.BaseManager.STATE_CAPTURED = "captured";
      50             : 
      51             : /**
      52             :  * Anchorless base, currently with dock.
      53             :  */
      54           0 : PETRA.BaseManager.STATE_ANCHORLESS = "anchorless";
      55             : 
      56           0 : PETRA.BaseManager.prototype.init = function(gameState, state)
      57             : {
      58           0 :         if (state === PETRA.BaseManager.STATE_UNCONSTRUCTED)
      59           0 :                 this.constructing = true;
      60           0 :         else if (state !== PETRA.BaseManager.STATE_CAPTURED)
      61           0 :                 this.neededDefenders = 0;
      62           0 :         this.workerObject = new PETRA.Worker(this);
      63             :         // entitycollections
      64           0 :         this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
      65           0 :         this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", PETRA.Worker.ROLE_WORKER));
      66           0 :         this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
      67           0 :         this.mobileDropsites = this.units.filter(API3.Filters.isDropsite());
      68             : 
      69           0 :         this.units.registerUpdates();
      70           0 :         this.workers.registerUpdates();
      71           0 :         this.buildings.registerUpdates();
      72           0 :         this.mobileDropsites.registerUpdates();
      73             : 
      74             :         // array of entity IDs, with each being
      75           0 :         this.dropsites = {};
      76           0 :         this.dropsiteSupplies = {};
      77           0 :         this.gatherers = {};
      78           0 :         for (let res of Resources.GetCodes())
      79             :         {
      80           0 :                 this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] };
      81           0 :                 this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 };
      82             :         }
      83             : };
      84             : 
      85           0 : PETRA.BaseManager.prototype.reset = function(gameState, state)
      86             : {
      87           0 :         if (state === PETRA.BaseManager.STATE_UNCONSTRUCTED)
      88           0 :                 this.constructing = true;
      89             :         else
      90           0 :                 this.constructing = false;
      91           0 :         if (state !== PETRA.BaseManager.STATE_CAPTURED || this.Config.difficulty < PETRA.DIFFICULTY_MEDIUM)
      92           0 :                 this.neededDefenders = 0;
      93             :         else
      94           0 :                 this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3);
      95             : };
      96             : 
      97           0 : PETRA.BaseManager.prototype.assignEntity = function(gameState, ent)
      98             : {
      99           0 :         ent.setMetadata(PlayerID, "base", this.ID);
     100           0 :         this.units.updateEnt(ent);
     101           0 :         this.workers.updateEnt(ent);
     102           0 :         this.buildings.updateEnt(ent);
     103           0 :         if (ent.resourceDropsiteTypes() && !ent.hasClass("Unit"))
     104           0 :                 this.assignResourceToDropsite(gameState, ent);
     105             : };
     106             : 
     107           0 : PETRA.BaseManager.prototype.setAnchor = function(gameState, anchorEntity)
     108             : {
     109           0 :         if (!anchorEntity.hasClass("CivCentre"))
     110           0 :                 API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor.");
     111             :         else
     112             :         {
     113           0 :                 this.anchor = anchorEntity;
     114           0 :                 this.anchorId = anchorEntity.id();
     115           0 :                 this.anchor.setMetadata(PlayerID, "baseAnchor", true);
     116           0 :                 this.basesManager.resetBaseCache();
     117             :         }
     118           0 :         anchorEntity.setMetadata(PlayerID, "base", this.ID);
     119           0 :         this.buildings.updateEnt(anchorEntity);
     120           0 :         this.accessIndex = PETRA.getLandAccess(gameState, anchorEntity);
     121           0 :         return true;
     122             : };
     123             : 
     124             : /* we lost our anchor. Let's reassign our units and buildings */
     125           0 : PETRA.BaseManager.prototype.anchorLost = function(gameState, ent)
     126             : {
     127           0 :         this.anchor = undefined;
     128           0 :         this.anchorId = undefined;
     129           0 :         this.neededDefenders = 0;
     130           0 :         this.basesManager.resetBaseCache();
     131             : };
     132             : 
     133             : /** Set a building of an anchorless base */
     134           0 : PETRA.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent)
     135             : {
     136           0 :         if (!this.buildings.hasEntities())
     137             :         {
     138           0 :                 if (!PETRA.getBuiltEntity(gameState, ent).resourceDropsiteTypes())
     139           0 :                         API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin.");
     140           0 :                 this.accessIndex = PETRA.getLandAccess(gameState, ent);
     141             :         }
     142           0 :         else if (this.accessIndex !== PETRA.getLandAccess(gameState, ent))
     143           0 :                 API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex +
     144             :                           " has been assigned " + ent.templateName() + " with access" + PETRA.getLandAccess(gameState, ent));
     145             : 
     146           0 :         ent.setMetadata(PlayerID, "base", this.ID);
     147           0 :         this.buildings.updateEnt(ent);
     148           0 :         return true;
     149             : };
     150             : 
     151             : /**
     152             :  * Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area.
     153             :  * Moving resources (animals) and buildable resources (fields) are treated elsewhere.
     154             :  */
     155           0 : PETRA.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite)
     156             : {
     157           0 :         if (this.dropsites[dropsite.id()])
     158             :         {
     159           0 :                 if (this.Config.debug > 0)
     160           0 :                         warn("assignResourceToDropsite: dropsite already in the list. Should never happen");
     161           0 :                 return;
     162             :         }
     163             : 
     164           0 :         let accessIndex = this.accessIndex;
     165           0 :         let dropsitePos = dropsite.position();
     166           0 :         let dropsiteId = dropsite.id();
     167           0 :         this.dropsites[dropsiteId] = true;
     168             : 
     169           0 :         if (this.ID == this.basesManager.baselessBase().ID)
     170           0 :                 accessIndex = PETRA.getLandAccess(gameState, dropsite);
     171             : 
     172           0 :         let maxDistResourceSquare = this.maxDistResourceSquare;
     173           0 :         for (let type of dropsite.resourceDropsiteTypes())
     174             :         {
     175           0 :                 let resources = gameState.getResourceSupplies(type);
     176           0 :                 if (!resources.length)
     177           0 :                         continue;
     178             : 
     179           0 :                 let nearby = this.dropsiteSupplies[type].nearby;
     180           0 :                 let medium = this.dropsiteSupplies[type].medium;
     181           0 :                 let faraway = this.dropsiteSupplies[type].faraway;
     182             : 
     183           0 :                 resources.forEach(function(supply)
     184             :                 {
     185           0 :                         if (!supply.position())
     186           0 :                                 return;
     187             :                         // Moving resources and fields are treated differently.
     188           0 :                         if (supply.hasClasses(["Animal", "Field"]))
     189           0 :                                 return;
     190             :                         // quick accessibility check
     191           0 :                         if (PETRA.getLandAccess(gameState, supply) != accessIndex)
     192           0 :                                 return;
     193             : 
     194           0 :                         let dist = API3.SquareVectorDistance(supply.position(), dropsitePos);
     195           0 :                         if (dist < maxDistResourceSquare)
     196             :                         {
     197           0 :                                 if (dist < maxDistResourceSquare/16)        // distmax/4
     198           0 :                                         nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
     199           0 :                                 else if (dist < maxDistResourceSquare/4)    // distmax/2
     200           0 :                                         medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
     201             :                                 else
     202           0 :                                         faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
     203             :                         }
     204             :                 });
     205             : 
     206           0 :                 nearby.sort((r1, r2) => r1.dist - r2.dist);
     207           0 :                 medium.sort((r1, r2) => r1.dist - r2.dist);
     208           0 :                 faraway.sort((r1, r2) => r1.dist - r2.dist);
     209             : 
     210             :                 /*
     211             :                 let debug = false;
     212             :                 if (debug)
     213             :                 {
     214             :                         faraway.forEach(function(res){
     215             :                                 Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]});
     216             :                         });
     217             :                         medium.forEach(function(res){
     218             :                                 Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]});
     219             :                         });
     220             :                         nearby.forEach(function(res){
     221             :                                 Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]});
     222             :                         });
     223             :                 }
     224             :                 */
     225             :         }
     226             : 
     227             :         // Allows all allies to use this dropsite except if base anchor to be sure to keep
     228             :         // a minimum of resources for this base
     229           0 :         Engine.PostCommand(PlayerID, {
     230             :                 "type": "set-dropsite-sharing",
     231             :                 "entities": [dropsiteId],
     232             :                 "shared": dropsiteId != this.anchorId
     233             :         });
     234             : };
     235             : 
     236           0 : PETRA.BaseManager.prototype.removeFromAssignedDropsite = function(ent)
     237             : {
     238           0 :         for (const type in this.dropsiteSupplies)
     239           0 :                 for (const proxim in this.dropsiteSupplies[type])
     240             :                 {
     241           0 :                         const resourcesList = this.dropsiteSupplies[type][proxim];
     242           0 :                         for (let i = 0; i < resourcesList.length; ++i)
     243           0 :                                 if (resourcesList[i].id === ent.id())
     244           0 :                                         resourcesList.splice(i--, 1);
     245             :                 }
     246             : };
     247             : 
     248             : // completely remove the dropsite resources from our list.
     249           0 : PETRA.BaseManager.prototype.removeDropsite = function(gameState, ent)
     250             : {
     251           0 :         if (!ent.id())
     252           0 :                 return;
     253             : 
     254           0 :         let removeSupply = function(entId, supply){
     255           0 :                 for (let i = 0; i < supply.length; ++i)
     256             :                 {
     257             :                         // exhausted resource, remove it from this list
     258           0 :                         if (!supply[i].ent || !gameState.getEntityById(supply[i].id))
     259           0 :                                 supply.splice(i--, 1);
     260             :                         // resource assigned to the removed dropsite, remove it
     261           0 :                         else if (supply[i].dropsite == entId)
     262           0 :                                 supply.splice(i--, 1);
     263             :                 }
     264             :         };
     265             : 
     266           0 :         for (let type in this.dropsiteSupplies)
     267             :         {
     268           0 :                 removeSupply(ent.id(), this.dropsiteSupplies[type].nearby);
     269           0 :                 removeSupply(ent.id(), this.dropsiteSupplies[type].medium);
     270           0 :                 removeSupply(ent.id(), this.dropsiteSupplies[type].faraway);
     271             :         }
     272             : 
     273           0 :         this.dropsites[ent.id()] = undefined;
     274             : };
     275             : 
     276             : /**
     277             :  * @return {Object} - The position of the best place to build a new dropsite for the specified resource,
     278             :  *                      its quality and its template name.
     279             :  */
     280           0 : PETRA.BaseManager.prototype.findBestDropsiteAndLocation = function(gameState, resource)
     281             : {
     282           0 :         let bestResult = {
     283             :                 "quality": 0,
     284             :                 "pos": [0, 0]
     285             :         };
     286           0 :         for (const templateName of gameState.ai.HQ.buildManager.findStructuresByFilter(gameState, API3.Filters.isDropsite(resource)))
     287             :         {
     288           0 :                 const dp = this.findBestDropsiteLocation(gameState, resource, templateName);
     289           0 :                 if (dp.quality < bestResult.quality)
     290           0 :                         continue;
     291           0 :                 bestResult = dp;
     292           0 :                 bestResult.templateName = templateName;
     293             :         }
     294           0 :         return bestResult;
     295             : };
     296             : 
     297             : /**
     298             :  * Returns the position of the best place to build a new dropsite for the specified resource and dropsite template.
     299             :  */
     300           0 : PETRA.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource, templateName)
     301             : {
     302           0 :         const template = gameState.getTemplate(gameState.applyCiv(templateName));
     303             : 
     304             :         // CCs and Docks are handled elsewhere.
     305           0 :         if (template.hasClasses(["CivCentre", "Dock"]))
     306           0 :                 return { "quality": 0, "pos": [0, 0] };
     307             : 
     308           0 :         let halfSize = 0;
     309           0 :         if (template.get("Footprint/Square"))
     310           0 :                 halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
     311           0 :         else if (template.get("Footprint/Circle"))
     312           0 :                 halfSize = +template.get("Footprint/Circle/@radius");
     313             : 
     314             :         // This builds a map. The procedure is fairly simple. It adds the resource maps
     315             :         //      (which are dynamically updated and are made so that they will facilitate DP placement)
     316             :         // Then checks for a good spot in the territory. If none, and town/city phase, checks outside
     317             :         // The AI will currently not build a CC if it wouldn't connect with an existing CC.
     318             : 
     319           0 :         let obstructions = PETRA.createObstructionMap(gameState, this.accessIndex, template);
     320             : 
     321           0 :         const dpEnts = gameState.getOwnStructures().filter(API3.Filters.isDropsite(resource)).toEntityArray();
     322             : 
     323             :         // Foundations don't have the dropsite properties yet, so treat them separately.
     324           0 :         for (const foundation of gameState.getOwnFoundations().toEntityArray())
     325           0 :                 if (PETRA.getBuiltEntity(gameState, foundation).isResourceDropsite(resource))
     326           0 :                         dpEnts.push(foundation);
     327             : 
     328             :         let bestIdx;
     329           0 :         let bestVal = 0;
     330           0 :         let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
     331             : 
     332           0 :         let territoryMap = gameState.ai.HQ.territoryMap;
     333           0 :         let width = territoryMap.width;
     334           0 :         let cellSize = territoryMap.cellSize;
     335             : 
     336           0 :         const droppableResources = template.resourceDropsiteTypes();
     337             : 
     338           0 :         for (let j of this.territoryIndices)
     339             :         {
     340           0 :                 let i = territoryMap.getNonObstructedTile(j, radius, obstructions);
     341           0 :                 if (i < 0)  // no room around
     342           0 :                         continue;
     343             : 
     344             :                 // We add 3 times the needed resource and once others that can be dropped here.
     345           0 :                 let total = 2 * gameState.sharedScript.resourceMaps[resource].map[j];
     346           0 :                 for (const res in gameState.sharedScript.resourceMaps)
     347           0 :                         if (droppableResources.indexOf(res) != -1)
     348           0 :                                 total += gameState.sharedScript.resourceMaps[res].map[j];
     349             : 
     350           0 :                 total *= 0.7;   // Just a normalisation factor as the locateMap is limited to 255
     351           0 :                 if (total <= bestVal)
     352           0 :                         continue;
     353             : 
     354           0 :                 let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
     355             : 
     356           0 :                 for (let dp of dpEnts)
     357             :                 {
     358           0 :                         let dpPos = dp.position();
     359           0 :                         if (!dpPos)
     360           0 :                                 continue;
     361           0 :                         let dist = API3.SquareVectorDistance(dpPos, pos);
     362           0 :                         if (dist < 3600)
     363             :                         {
     364           0 :                                 total = 0;
     365           0 :                                 break;
     366             :                         }
     367           0 :                         else if (dist < 6400)
     368           0 :                                 total *= (Math.sqrt(dist)-60)/20;
     369             :                 }
     370           0 :                 if (total <= bestVal)
     371           0 :                         continue;
     372             : 
     373           0 :                 if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize))
     374           0 :                         continue;
     375           0 :                 bestVal = total;
     376           0 :                 bestIdx = i;
     377             :         }
     378             : 
     379           0 :         if (this.Config.debug > 2)
     380           0 :                 warn(" for dropsite best is " + bestVal);
     381             : 
     382           0 :         if (bestVal <= 0)
     383           0 :                 return { "quality": bestVal, "pos": [0, 0] };
     384             : 
     385           0 :         let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
     386           0 :         let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
     387           0 :         return { "quality": bestVal, "pos": [x, z] };
     388             : };
     389             : 
     390           0 : PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, distances = ["nearby", "medium", "faraway"])
     391             : {
     392           0 :         let count = 0;
     393           0 :         let check = {};
     394           0 :         for (const proxim of distances)
     395           0 :                 for (const supply of this.dropsiteSupplies[type][proxim])
     396             :                 {
     397           0 :                         if (check[supply.id])    // avoid double counting as same resource can appear several time
     398           0 :                                 continue;
     399           0 :                         check[supply.id] = true;
     400           0 :                         count += supply.ent.resourceSupplyAmount();
     401             :                 }
     402           0 :         return count;
     403             : };
     404             : 
     405             : /** check our resource levels and react accordingly */
     406           0 : PETRA.BaseManager.prototype.checkResourceLevels = function(gameState, queues)
     407             : {
     408           0 :         for (let type of Resources.GetCodes())
     409             :         {
     410           0 :                 if (type == "food")
     411             :                 {
     412           0 :                         const prox = ["nearby"];
     413           0 :                         if (gameState.currentPhase() < 2)
     414           0 :                                 prox.push("medium");
     415           0 :                         if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field"))    // let's see if we need to add new farms.
     416             :                         {
     417           0 :                                 const count = this.getResourceLevel(gameState, type, prox);  // animals are not accounted
     418           0 :                                 let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length;  // including foundations
     419           0 :                                 let numQueue = queues.field.countQueuedUnits();
     420             : 
     421             :                                 // TODO  if not yet farms, add a check on time used/lost and build farmstead if needed
     422           0 :                                 if (numFarms + numQueue == 0)   // starting game, rely on fruits as long as we have enough of them
     423             :                                 {
     424           0 :                                         if (count < 600)
     425             :                                         {
     426           0 :                                                 queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID }));
     427           0 :                                                 gameState.ai.HQ.needFarm = true;
     428             :                                         }
     429             :                                 }
     430           0 :                                 else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields)
     431             :                                 {
     432           0 :                                         let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length;
     433           0 :                                         let goal = this.Config.Economy.provisionFields;
     434           0 :                                         if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5)
     435           0 :                                                 goal = Math.max(goal-1, 1);
     436           0 :                                         if (numFound + numQueue < goal)
     437           0 :                                                 queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID }));
     438             :                                 }
     439           0 :                                 else if (gameState.ai.HQ.needCorral && !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() &&
     440             :                                          !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral"))
     441           0 :                                         queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID }));
     442           0 :                                 continue;
     443             :                         }
     444           0 :                         if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() &&
     445             :                             !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral"))
     446             :                         {
     447           0 :                                 const count = this.getResourceLevel(gameState, type, prox);  // animals are not accounted
     448           0 :                                 if (count < 900)
     449             :                                 {
     450           0 :                                         queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID }));
     451           0 :                                         gameState.ai.HQ.needCorral = true;
     452             :                                 }
     453             :                         }
     454           0 :                         continue;
     455             :                 }
     456             :                 // Non food stuff
     457           0 :                 if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() ||
     458             :                     gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).hasEntities())
     459             :                 {
     460           0 :                         this.gatherers[type].nextCheck = gameState.ai.playedTurn;
     461           0 :                         this.gatherers[type].used = 0;
     462           0 :                         this.gatherers[type].lost = 0;
     463           0 :                         continue;
     464             :                 }
     465           0 :                 if (gameState.ai.playedTurn < this.gatherers[type].nextCheck)
     466           0 :                         continue;
     467           0 :                 for (let ent of this.gatherersByType(gameState, type).values())
     468             :                 {
     469           0 :                         if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING")
     470           0 :                                 ++this.gatherers[type].used;
     471           0 :                         else if (ent.unitAIState() == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING")
     472           0 :                                 ++this.gatherers[type].lost;
     473             :                 }
     474             :                 // TODO  add also a test on remaining resources.
     475           0 :                 let total = this.gatherers[type].used + this.gatherers[type].lost;
     476           0 :                 if (total > 150 || total > 60 && type != "wood")
     477             :                 {
     478           0 :                         let ratio = this.gatherers[type].lost / total;
     479           0 :                         if (ratio > 0.15)
     480             :                         {
     481           0 :                                 const newDP = this.findBestDropsiteAndLocation(gameState, type);
     482           0 :                                 if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, newDP.templateName))
     483           0 :                                         queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.ID, "type": type }, newDP.pos));
     484           0 :                                 else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits())
     485             :                                 {
     486             :                                         // No good dropsite, try to build a new base if no base already planned,
     487             :                                         // and if not possible, be less strict on dropsite quality.
     488           0 :                                         if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) &&
     489             :                                             newDP.quality > Math.min(25, 50*0.15/ratio) &&
     490             :                                             gameState.ai.HQ.canBuild(gameState, newDP.templateName))
     491           0 :                                                 queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.ID, "type": type }, newDP.pos));
     492             :                                 }
     493             :                         }
     494           0 :                         this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20;
     495           0 :                         this.gatherers[type].used = 0;
     496           0 :                         this.gatherers[type].lost = 0;
     497             :                 }
     498           0 :                 else if (total == 0)
     499           0 :                         this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10;
     500             :         }
     501             : 
     502             : };
     503             : 
     504             : /** Adds the estimated gather rates from this base to the currentRates */
     505           0 : PETRA.BaseManager.prototype.addGatherRates = function(gameState, currentRates)
     506             : {
     507           0 :         for (let res in currentRates)
     508             :         {
     509             :                 // I calculate the exact gathering rate for each unit.
     510             :                 // I must then lower that to account for travel time.
     511             :                 // Given that the faster you gather, the more travel time matters,
     512             :                 // I use some logarithms.
     513             :                 // TODO: this should take into account for unit speed and/or distance to target
     514             : 
     515           0 :                 this.gatherersByType(gameState, res).forEach(ent => {
     516           0 :                         if (ent.isIdle() || !ent.position())
     517           0 :                                 return;
     518           0 :                         let gRate = ent.currentGatherRate();
     519           0 :                         if (gRate)
     520           0 :                                 currentRates[res] += Math.log(1+gRate)/1.1;
     521             :                 });
     522           0 :                 if (res == "food")
     523             :                 {
     524           0 :                         this.workersBySubrole(gameState, PETRA.Worker.SUBROLE_HUNTER).forEach(ent => {
     525           0 :                                 if (ent.isIdle() || !ent.position())
     526           0 :                                         return;
     527           0 :                                 let gRate = ent.currentGatherRate();
     528           0 :                                 if (gRate)
     529           0 :                                         currentRates[res] += Math.log(1+gRate)/1.1;
     530             :                         });
     531           0 :                         this.workersBySubrole(gameState, PETRA.Worker.SUBROLE_FISHER).forEach(ent => {
     532           0 :                                 if (ent.isIdle() || !ent.position())
     533           0 :                                         return;
     534           0 :                                 let gRate = ent.currentGatherRate();
     535           0 :                                 if (gRate)
     536           0 :                                         currentRates[res] += Math.log(1+gRate)/1.1;
     537             :                         });
     538             :                 }
     539             :         }
     540             : };
     541             : 
     542           0 : PETRA.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless)
     543             : {
     544           0 :         if (!roleless)
     545           0 :                 roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values();
     546             : 
     547           0 :         for (let ent of roleless)
     548             :         {
     549           0 :                 if (ent.hasClasses(["Worker", "CitizenSoldier", "FishingBoat"]))
     550           0 :                         ent.setMetadata(PlayerID, "role", PETRA.Worker.ROLE_WORKER);
     551           0 :                 else if (ent.hasClass("Support") && ent.hasClass("Elephant"))
     552           0 :                         ent.setMetadata(PlayerID, "role", "worker");
     553             :         }
     554             : };
     555             : 
     556             : /**
     557             :  * If the numbers of workers on the resources is unbalanced then set some of workers to idle so
     558             :  * they can be reassigned by reassignIdleWorkers.
     559             :  * TODO: actually this probably should be in the HQ.
     560             :  */
     561           0 : PETRA.BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
     562             : {
     563           0 :         this.timeNextIdleCheck = gameState.ai.elapsedTime + 8;
     564             :         // change resource only towards one which is more needed, and if changing will not change this order
     565           0 :         let nb = 1;    // no more than 1 change per turn (otherwise we should update the rates)
     566           0 :         let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
     567           0 :         let sumWanted = 0;
     568           0 :         let sumCurrent = 0;
     569           0 :         for (let need of mostNeeded)
     570             :         {
     571           0 :                 sumWanted += need.wanted;
     572           0 :                 sumCurrent += need.current;
     573             :         }
     574           0 :         let scale = 1;
     575           0 :         if (sumWanted > 0)
     576           0 :                 scale = sumCurrent / sumWanted;
     577             : 
     578           0 :         for (let i = mostNeeded.length-1; i > 0; --i)
     579             :         {
     580           0 :                 let lessNeed = mostNeeded[i];
     581           0 :                 for (let j = 0; j < i; ++j)
     582             :                 {
     583           0 :                         let moreNeed = mostNeeded[j];
     584           0 :                         let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type];
     585           0 :                         if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
     586           0 :                                 continue;
     587             :                         // Ensure that the most wanted resource is not exhausted
     588           0 :                         if (moreNeed.type != "food" && this.basesManager.isResourceExhausted(moreNeed.type))
     589             :                         {
     590           0 :                                 if (lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type))
     591           0 :                                         continue;
     592             : 
     593             :                                 // And if so, move the gatherer to the less wanted one.
     594           0 :                                 nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb);
     595           0 :                                 if (nb == 0)
     596           0 :                                         return;
     597             :                         }
     598             : 
     599             :                         // If we assume a mean rate of 0.5 per gatherer, this diff should be > 1
     600             :                         // but we require a bit more to avoid too frequent changes
     601           0 :                         if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 ||
     602             :                             lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type))
     603             :                         {
     604           0 :                                 nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb);
     605           0 :                                 if (nb == 0)
     606           0 :                                         return;
     607             :                         }
     608             :                 }
     609             :         }
     610             : };
     611             : 
     612             : /**
     613             :  * Switch some gatherers (limited to number) from resource "from" to resource "to"
     614             :  * and return remaining number of possible switches.
     615             :  * Prefer FemaleCitizen for food and CitizenSoldier for other resources.
     616             :  */
     617           0 : PETRA.BaseManager.prototype.switchGatherer = function(gameState, from, to, number)
     618             : {
     619           0 :         let num = number;
     620             :         let only;
     621           0 :         let gatherers = this.gatherersByType(gameState, from);
     622           0 :         if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities())
     623           0 :                 only = "CitizenSoldier";
     624           0 :         else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities())
     625           0 :                 only = "FemaleCitizen";
     626             : 
     627           0 :         for (let ent of gatherers.values())
     628             :         {
     629           0 :                 if (num == 0)
     630           0 :                         return num;
     631           0 :                 if (!ent.canGather(to))
     632           0 :                         continue;
     633           0 :                 if (only && !ent.hasClass(only))
     634           0 :                         continue;
     635           0 :                 --num;
     636           0 :                 ent.stopMoving();
     637           0 :                 ent.setMetadata(PlayerID, "gather-type", to);
     638           0 :                 this.basesManager.AddTCResGatherer(to);
     639             :         }
     640           0 :         return num;
     641             : };
     642             : 
     643           0 : PETRA.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers)
     644             : {
     645             :         // Search for idle workers, and tell them to gather resources based on demand
     646           0 :         if (!idleWorkers)
     647             :         {
     648           0 :                 const filter = API3.Filters.byMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE);
     649           0 :                 idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values();
     650             :         }
     651             : 
     652           0 :         for (let ent of idleWorkers)
     653             :         {
     654             :                 // Check that the worker isn't garrisoned
     655           0 :                 if (!ent.position())
     656           0 :                         continue;
     657             :                 // Support elephant can only be builders
     658           0 :                 if (ent.hasClass("Support") && ent.hasClass("Elephant"))
     659             :                 {
     660           0 :                         ent.setMetadata(PlayerID, "subrole", "idle");
     661           0 :                         continue;
     662             :                 }
     663             : 
     664           0 :                 if (ent.hasClass("Worker"))
     665             :                 {
     666             :                         // Just emergency repairing here. It is better managed in assignToFoundations
     667           0 :                         if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() &&
     668             :                                 gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2)
     669           0 :                                 ent.repair(this.anchor);
     670           0 :                         else if (ent.isGatherer())
     671             :                         {
     672           0 :                                 let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
     673           0 :                                 for (let needed of mostNeeded)
     674             :                                 {
     675           0 :                                         if (!ent.canGather(needed.type))
     676           0 :                                                 continue;
     677           0 :                                         let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type];
     678           0 :                                         if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
     679           0 :                                                 continue;
     680           0 :                                         if (needed.type != "food" && this.basesManager.isResourceExhausted(needed.type))
     681           0 :                                                 continue;
     682           0 :                                         ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_GATHERER);
     683           0 :                                         ent.setMetadata(PlayerID, "gather-type", needed.type);
     684           0 :                                         this.basesManager.AddTCResGatherer(needed.type);
     685           0 :                                         break;
     686             :                                 }
     687             :                         }
     688             :                 }
     689           0 :                 else if (PETRA.isFastMoving(ent) && ent.canGather("food") && ent.canAttackClass("Animal"))
     690           0 :                         ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_HUNTER);
     691           0 :                 else if (ent.hasClass("FishingBoat"))
     692           0 :                         ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_FISHER);
     693             :         }
     694             : };
     695             : 
     696           0 : PETRA.BaseManager.prototype.workersBySubrole = function(gameState, subrole)
     697             : {
     698           0 :         return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers);
     699             : };
     700             : 
     701           0 : PETRA.BaseManager.prototype.gatherersByType = function(gameState, type)
     702             : {
     703           0 :         return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, PETRA.Worker.SUBROLE_GATHERER));
     704             : };
     705             : 
     706             : /**
     707             :  * returns an entity collection of workers.
     708             :  * They are idled immediatly and their subrole set to idle.
     709             :  */
     710           0 : PETRA.BaseManager.prototype.pickBuilders = function(gameState, workers, number)
     711             : {
     712           0 :         let availableWorkers = this.workers.filter(ent => {
     713           0 :                 if (!ent.position() || !ent.isBuilder())
     714           0 :                         return false;
     715           0 :                 if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
     716           0 :                         return false;
     717           0 :                 if (ent.getMetadata(PlayerID, "transport"))
     718           0 :                         return false;
     719           0 :                 return true;
     720             :         }).toEntityArray();
     721           0 :         availableWorkers.sort((a, b) => {
     722           0 :                 let vala = 0;
     723           0 :                 let valb = 0;
     724           0 :                 if (a.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_BUILDER)
     725           0 :                         vala = 100;
     726           0 :                 if (b.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_BUILDER)
     727           0 :                         valb = 100;
     728           0 :                 if (a.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_IDLE)
     729           0 :                         vala = -50;
     730           0 :                 if (b.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_IDLE)
     731           0 :                         valb = -50;
     732           0 :                 if (a.getMetadata(PlayerID, "plan") === undefined)
     733           0 :                         vala = -20;
     734           0 :                 if (b.getMetadata(PlayerID, "plan") === undefined)
     735           0 :                         valb = -20;
     736           0 :                 return vala - valb;
     737             :         });
     738           0 :         let needed = Math.min(number, availableWorkers.length - 3);
     739           0 :         for (let i = 0; i < needed; ++i)
     740             :         {
     741           0 :                 availableWorkers[i].stopMoving();
     742           0 :                 availableWorkers[i].setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE);
     743           0 :                 workers.addEnt(availableWorkers[i]);
     744             :         }
     745           0 :         return;
     746             : };
     747             : 
     748             : /**
     749             :  * If we have some foundations, and we don't have enough builder-workers,
     750             :  * try reassigning some other workers who are nearby
     751             :  * AI tries to use builders sensibly, not completely stopping its econ.
     752             :  */
     753           0 : PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
     754             : {
     755           0 :         let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field"))));
     756             : 
     757           0 :         let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair());
     758             : 
     759             :         // Check if nothing to build
     760           0 :         if (!foundations.length && !damagedBuildings.length)
     761           0 :                 return;
     762             : 
     763           0 :         let workers = this.workers.filter(ent => ent.isBuilder());
     764           0 :         const builderWorkers = this.workersBySubrole(gameState, PETRA.Worker.SUBROLE_BUILDER);
     765           0 :         let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle());
     766             : 
     767             :         // if we're constructing and we have the foundations to our base anchor, only try building that.
     768           0 :         if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities())
     769             :         {
     770           0 :                 foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true));
     771           0 :                 let tID = foundations.toEntityArray()[0].id();
     772           0 :                 workers.forEach(ent => {
     773           0 :                         let target = ent.getMetadata(PlayerID, "target-foundation");
     774           0 :                         if (target && target != tID)
     775             :                         {
     776           0 :                                 ent.stopMoving();
     777           0 :                                 ent.setMetadata(PlayerID, "target-foundation", tID);
     778             :                         }
     779             :                 });
     780             :         }
     781             : 
     782           0 :         if (workers.length < 3)
     783             :         {
     784           0 :                 const fromOtherBase = this.basesManager.bulkPickWorkers(gameState, this, 2);
     785           0 :                 if (fromOtherBase)
     786             :                 {
     787           0 :                         let baseID = this.ID;
     788           0 :                         fromOtherBase.forEach(worker => {
     789           0 :                                 worker.setMetadata(PlayerID, "base", baseID);
     790           0 :                                 worker.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER);
     791           0 :                                 workers.updateEnt(worker);
     792           0 :                                 builderWorkers.updateEnt(worker);
     793           0 :                                 idleBuilderWorkers.updateEnt(worker);
     794             :                         });
     795             :                 }
     796             :         }
     797             : 
     798           0 :         let builderTot = builderWorkers.length - idleBuilderWorkers.length;
     799             : 
     800             :         // Make the limit on number of builders depends on the available resources
     801           0 :         let availableResources = gameState.ai.queueManager.getAvailableResources(gameState);
     802           0 :         let builderRatio = 1;
     803           0 :         for (let res of Resources.GetCodes())
     804             :         {
     805           0 :                 if (availableResources[res] < 200)
     806             :                 {
     807           0 :                         builderRatio = 0.2;
     808           0 :                         break;
     809             :                 }
     810           0 :                 else if (availableResources[res] < 1000)
     811           0 :                         builderRatio = Math.min(builderRatio, availableResources[res] / 1000);
     812             :         }
     813             : 
     814           0 :         for (let target of foundations.values())
     815             :         {
     816           0 :                 if (target.hasClass("Field"))
     817           0 :                         continue; // we do not build fields
     818             : 
     819           0 :                 if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
     820           0 :                         if (!target.hasClasses(["CivCentre", "Wall"]) &&
     821             :                             (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder")))
     822           0 :                                 continue;
     823             : 
     824             :                 // if our territory has shrinked since this foundation was positioned, do not build it
     825           0 :                 if (PETRA.isNotWorthBuilding(gameState, target))
     826           0 :                         continue;
     827             : 
     828           0 :                 let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
     829           0 :                 let maxTotalBuilders = Math.ceil(workers.length * builderRatio);
     830           0 :                 if (maxTotalBuilders < 2 && workers.length > 1)
     831           0 :                         maxTotalBuilders = 2;
     832           0 :                 if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 &&
     833             :                     gameState.getPopulationLimit() < gameState.getPopulationMax())
     834           0 :                         maxTotalBuilders += 2;
     835           0 :                 let targetNB = 2;
     836           0 :                 if (target.hasClasses(["Fortress", "Wonder"]) ||
     837             :                     target.getMetadata(PlayerID, "phaseUp") == true)
     838           0 :                         targetNB = 7;
     839           0 :                 else if (target.hasClasses(["Barracks", "Range", "Stable", "Tower", "Market"]))
     840           0 :                         targetNB = 4;
     841           0 :                 else if (target.hasClasses(["House", "DropsiteWood"]))
     842           0 :                         targetNB = 3;
     843             : 
     844           0 :                 if (target.getMetadata(PlayerID, "baseAnchor") == true ||
     845             :                     target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder"))
     846             :                 {
     847           0 :                         targetNB = 15;
     848           0 :                         maxTotalBuilders = Math.max(maxTotalBuilders, 15);
     849             :                 }
     850             : 
     851           0 :                 if (!this.basesManager.hasActiveBase())
     852             :                 {
     853           0 :                         targetNB = workers.length;
     854           0 :                         maxTotalBuilders = targetNB;
     855             :                 }
     856             : 
     857           0 :                 if (assigned >= targetNB)
     858           0 :                         continue;
     859           0 :                 idleBuilderWorkers.forEach(function(ent) {
     860           0 :                         if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
     861           0 :                                 return;
     862           0 :                         if (assigned >= targetNB || !ent.position() ||
     863             :                             API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
     864           0 :                                 return;
     865           0 :                         ++assigned;
     866           0 :                         ++builderTot;
     867           0 :                         ent.setMetadata(PlayerID, "target-foundation", target.id());
     868             :                 });
     869           0 :                 if (assigned >= targetNB || builderTot >= maxTotalBuilders)
     870           0 :                         continue;
     871           0 :                 let nonBuilderWorkers = workers.filter(function(ent) {
     872           0 :                         if (ent.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_BUILDER)
     873           0 :                                 return false;
     874           0 :                         if (!ent.position())
     875           0 :                                 return false;
     876           0 :                         if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
     877           0 :                                 return false;
     878           0 :                         if (ent.getMetadata(PlayerID, "transport"))
     879           0 :                                 return false;
     880           0 :                         return true;
     881             :                 }).toEntityArray();
     882           0 :                 let time = target.buildTime();
     883           0 :                 nonBuilderWorkers.sort((workerA, workerB) => {
     884           0 :                         let coeffA = API3.SquareVectorDistance(target.position(), workerA.position());
     885             :                         // elephant moves slowly, so when far away they are only useful if build time is long
     886           0 :                         if (workerA.hasClass("Elephant"))
     887           0 :                                 coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time);
     888           0 :                         else if (workerA.getMetadata(PlayerID, "gather-type") == "food")
     889           0 :                                 coeffA *= 3;
     890           0 :                         let coeffB = API3.SquareVectorDistance(target.position(), workerB.position());
     891           0 :                         if (workerB.hasClass("Elephant"))
     892           0 :                                 coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time);
     893           0 :                         else if (workerB.getMetadata(PlayerID, "gather-type") == "food")
     894           0 :                                 coeffB *= 3;
     895           0 :                         return coeffA - coeffB;
     896             :                 });
     897           0 :                 let current = 0;
     898           0 :                 let nonBuilderTot = nonBuilderWorkers.length;
     899           0 :                 while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot)
     900             :                 {
     901           0 :                         ++assigned;
     902           0 :                         ++builderTot;
     903           0 :                         let ent = nonBuilderWorkers[current++];
     904           0 :                         ent.stopMoving();
     905           0 :                         ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER);
     906           0 :                         ent.setMetadata(PlayerID, "target-foundation", target.id());
     907             :                 }
     908             :         }
     909             : 
     910           0 :         for (let target of damagedBuildings.values())
     911             :         {
     912             :                 // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building
     913             :                 // that's being destroyed.
     914           0 :                 if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
     915             :                 {
     916           0 :                         if (target.healthLevel() > 0.5 ||
     917             :                             !target.hasClasses(["CivCentre", "Wall"]) &&
     918             :                             (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder")))
     919           0 :                                 continue;
     920             :                 }
     921           0 :                 else if (noRepair && !target.hasClass("CivCentre"))
     922           0 :                         continue;
     923             : 
     924           0 :                 if (target.decaying())
     925           0 :                         continue;
     926             : 
     927           0 :                 let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
     928           0 :                 let maxTotalBuilders = Math.ceil(workers.length * builderRatio);
     929           0 :                 let targetNB = 1;
     930           0 :                 if (target.hasClasses(["Fortress", "Wonder"]))
     931           0 :                         targetNB = 3;
     932           0 :                 if (target.getMetadata(PlayerID, "baseAnchor") == true ||
     933             :                     target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder"))
     934             :                 {
     935           0 :                         maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio));
     936           0 :                         targetNB = 5;
     937           0 :                         if (target.healthLevel() < 0.3)
     938             :                         {
     939           0 :                                 maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio));
     940           0 :                                 targetNB = 7;
     941             :                         }
     942             : 
     943             :                 }
     944             : 
     945           0 :                 if (assigned >= targetNB)
     946           0 :                         continue;
     947           0 :                 idleBuilderWorkers.forEach(function(ent) {
     948           0 :                         if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
     949           0 :                                 return;
     950           0 :                         if (assigned >= targetNB || !ent.position() ||
     951             :                             API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
     952           0 :                                 return;
     953           0 :                         ++assigned;
     954           0 :                         ++builderTot;
     955           0 :                         ent.setMetadata(PlayerID, "target-foundation", target.id());
     956             :                 });
     957           0 :                 if (assigned >= targetNB || builderTot >= maxTotalBuilders)
     958           0 :                         continue;
     959           0 :                 let nonBuilderWorkers = workers.filter(function(ent) {
     960           0 :                         if (ent.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_BUILDER)
     961           0 :                                 return false;
     962           0 :                         if (!ent.position())
     963           0 :                                 return false;
     964           0 :                         if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
     965           0 :                                 return false;
     966           0 :                         if (ent.getMetadata(PlayerID, "transport"))
     967           0 :                                 return false;
     968           0 :                         return true;
     969             :                 });
     970           0 :                 let num = Math.min(nonBuilderWorkers.length, targetNB-assigned);
     971           0 :                 let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num);
     972             : 
     973           0 :                 nearestNonBuilders.forEach(function(ent) {
     974           0 :                         ++assigned;
     975           0 :                         ++builderTot;
     976           0 :                         ent.stopMoving();
     977           0 :                         ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER);
     978           0 :                         ent.setMetadata(PlayerID, "target-foundation", target.id());
     979             :                 });
     980             :         }
     981             : };
     982             : 
     983             : /** Return false when the base is not active (no workers on it) */
     984           0 : PETRA.BaseManager.prototype.update = function(gameState, queues, events)
     985             : {
     986           0 :         if (this.ID == this.basesManager.baselessBase().ID)
     987             :         {
     988             :                 // if some active base, reassigns the workers/buildings
     989             :                 // otherwise look for anything useful to do, i.e. treasures to gather
     990           0 :                 if (this.basesManager.hasActiveBase())
     991             :                 {
     992           0 :                         for (let ent of this.units.values())
     993             :                         {
     994           0 :                                 let bestBase = PETRA.getBestBase(gameState, ent);
     995           0 :                                 if (bestBase.ID != this.ID)
     996           0 :                                         bestBase.assignEntity(gameState, ent);
     997             :                         }
     998           0 :                         for (let ent of this.buildings.values())
     999             :                         {
    1000           0 :                                 let bestBase = PETRA.getBestBase(gameState, ent);
    1001           0 :                                 if (!bestBase)
    1002             :                                 {
    1003           0 :                                         if (ent.hasClass("Dock"))
    1004           0 :                                                 API3.warn("Petra: dock in 'noBase' baseManager. It may be useful to do an anchorless base for " + ent.templateName());
    1005           0 :                                         continue;
    1006             :                                 }
    1007           0 :                                 if (ent.resourceDropsiteTypes())
    1008           0 :                                         this.removeDropsite(gameState, ent);
    1009           0 :                                 bestBase.assignEntity(gameState, ent);
    1010             :                         }
    1011             :                 }
    1012           0 :                 else if (gameState.ai.HQ.canBuildUnits)
    1013             :                 {
    1014           0 :                         this.assignToFoundations(gameState);
    1015           0 :                         if (gameState.ai.elapsedTime > this.timeNextIdleCheck)
    1016           0 :                                 this.setWorkersIdleByPriority(gameState);
    1017           0 :                         this.assignRolelessUnits(gameState);
    1018           0 :                         this.reassignIdleWorkers(gameState);
    1019           0 :                         for (let ent of this.workers.values())
    1020           0 :                                 this.workerObject.update(gameState, ent);
    1021           0 :                         for (let ent of this.mobileDropsites.values())
    1022           0 :                                 this.workerObject.moveToGatherer(gameState, ent, false);
    1023             :                 }
    1024           0 :                 return false;
    1025             :         }
    1026             : 
    1027           0 :         if (!this.anchor)   // This anchor has been destroyed, but the base may still be usable
    1028             :         {
    1029           0 :                 if (!this.buildings.hasEntities())
    1030             :                 {
    1031             :                         // Reassign all remaining entities to its nearest base
    1032           0 :                         for (let ent of this.units.values())
    1033             :                         {
    1034           0 :                                 let base = PETRA.getBestBase(gameState, ent, false, this.ID);
    1035           0 :                                 base.assignEntity(gameState, ent);
    1036             :                         }
    1037           0 :                         return false;
    1038             :                 }
    1039             :                 // If we have a base with anchor on the same land, reassign everything to it
    1040             :                 let reassignedBase;
    1041           0 :                 for (let ent of this.buildings.values())
    1042             :                 {
    1043           0 :                         if (!ent.position())
    1044           0 :                                 continue;
    1045           0 :                         let base = PETRA.getBestBase(gameState, ent);
    1046           0 :                         if (base.anchor)
    1047           0 :                                 reassignedBase = base;
    1048           0 :                         break;
    1049             :                 }
    1050             : 
    1051           0 :                 if (reassignedBase)
    1052             :                 {
    1053           0 :                         for (let ent of this.units.values())
    1054           0 :                                 reassignedBase.assignEntity(gameState, ent);
    1055           0 :                         for (let ent of this.buildings.values())
    1056             :                         {
    1057           0 :                                 if (ent.resourceDropsiteTypes())
    1058           0 :                                         this.removeDropsite(gameState, ent);
    1059           0 :                                 reassignedBase.assignEntity(gameState, ent);
    1060             :                         }
    1061           0 :                         return false;
    1062             :                 }
    1063             : 
    1064           0 :                 this.assignToFoundations(gameState);
    1065           0 :                 if (gameState.ai.elapsedTime > this.timeNextIdleCheck)
    1066           0 :                         this.setWorkersIdleByPriority(gameState);
    1067           0 :                 this.assignRolelessUnits(gameState);
    1068           0 :                 this.reassignIdleWorkers(gameState);
    1069           0 :                 for (let ent of this.workers.values())
    1070           0 :                         this.workerObject.update(gameState, ent);
    1071           0 :                 for (let ent of this.mobileDropsites.values())
    1072           0 :                         this.workerObject.moveToGatherer(gameState, ent, false);
    1073           0 :                 return true;
    1074             :         }
    1075             : 
    1076           0 :         Engine.ProfileStart("Base update - base " + this.ID);
    1077             : 
    1078           0 :         this.checkResourceLevels(gameState, queues);
    1079           0 :         this.assignToFoundations(gameState);
    1080             : 
    1081           0 :         if (this.constructing)
    1082             :         {
    1083           0 :                 let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position());
    1084           0 :                 if(owner != 0 && !gameState.isPlayerAlly(owner))
    1085             :                 {
    1086             :                         // we're in enemy territory. If we're too close from the enemy, destroy us.
    1087           0 :                         let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
    1088           0 :                         for (let cc of ccEnts.values())
    1089             :                         {
    1090           0 :                                 if (cc.owner() != owner)
    1091           0 :                                         continue;
    1092           0 :                                 if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000)
    1093           0 :                                         continue;
    1094           0 :                                 this.anchor.destroy();
    1095           0 :                                 this.basesManager.resetBaseCache();
    1096           0 :                                 break;
    1097             :                         }
    1098             :                 }
    1099             :         }
    1100           0 :         else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()]))
    1101           0 :                 --this.neededDefenders;
    1102             : 
    1103           0 :         if (gameState.ai.elapsedTime > this.timeNextIdleCheck &&
    1104             :            (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2))
    1105           0 :                 this.setWorkersIdleByPriority(gameState);
    1106             : 
    1107           0 :         this.assignRolelessUnits(gameState);
    1108           0 :         this.reassignIdleWorkers(gameState);
    1109             :         // check if workers can find something useful to do
    1110           0 :         for (let ent of this.workers.values())
    1111           0 :                 this.workerObject.update(gameState, ent);
    1112           0 :         for (let ent of this.mobileDropsites.values())
    1113           0 :                 this.workerObject.moveToGatherer(gameState, ent, false);
    1114             : 
    1115           0 :         Engine.ProfileStop();
    1116           0 :         return true;
    1117             : };
    1118             : 
    1119           0 : PETRA.BaseManager.prototype.AddTCGatherer = function(supplyID)
    1120             : {
    1121           0 :         return this.basesManager.AddTCGatherer(supplyID);
    1122             : };
    1123             : 
    1124           0 : PETRA.BaseManager.prototype.RemoveTCGatherer = function(supplyID)
    1125             : {
    1126           0 :         this.basesManager.RemoveTCGatherer(supplyID);
    1127             : };
    1128             : 
    1129           0 : PETRA.BaseManager.prototype.GetTCGatherer = function(supplyID)
    1130             : {
    1131           0 :         return this.basesManager.GetTCGatherer(supplyID);
    1132             : };
    1133             : 
    1134           0 : PETRA.BaseManager.prototype.Serialize = function()
    1135             : {
    1136           0 :         return {
    1137             :                 "ID": this.ID,
    1138             :                 "anchorId": this.anchorId,
    1139             :                 "accessIndex": this.accessIndex,
    1140             :                 "maxDistResourceSquare": this.maxDistResourceSquare,
    1141             :                 "constructing": this.constructing,
    1142             :                 "gatherers": this.gatherers,
    1143             :                 "neededDefenders": this.neededDefenders,
    1144             :                 "territoryIndices": this.territoryIndices,
    1145             :                 "timeNextIdleCheck": this.timeNextIdleCheck
    1146             :         };
    1147             : };
    1148             : 
    1149           0 : PETRA.BaseManager.prototype.Deserialize = function(gameState, data)
    1150             : {
    1151           0 :         for (let key in data)
    1152           0 :                 this[key] = data[key];
    1153             : 
    1154           0 :         this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined;
    1155             : };

Generated by: LCOV version 1.14