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 : };
|