Line data Source code
1 : /**
2 : * Headquarters
3 : * Deal with high level logic for the AI. Most of the interesting stuff gets done here.
4 : * Some tasks:
5 : * -defining RESS needs
6 : * -BO decisions.
7 : * > training workers
8 : * > building stuff (though we'll send that to bases)
9 : * -picking strategy (specific manager?)
10 : * -diplomacy -> diplomacyManager
11 : * -planning attacks -> attackManager
12 : * -picking new CC locations.
13 : */
14 0 : PETRA.HQ = function(Config)
15 : {
16 0 : this.Config = Config;
17 0 : this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i
18 :
19 : // Cache various quantities.
20 0 : this.turnCache = {};
21 0 : this.lastFailedGather = {};
22 :
23 0 : this.firstBaseConfig = false;
24 :
25 : // Workers configuration.
26 0 : this.targetNumWorkers = this.Config.Economy.targetNumWorkers;
27 0 : this.supportRatio = this.Config.Economy.supportRatio;
28 :
29 0 : this.fortStartTime = 180; // Sentry towers, will start at fortStartTime + towerLapseTime.
30 0 : this.towerStartTime = 0; // Stone towers, will start as soon as available (town phase).
31 0 : this.towerLapseTime = this.Config.Military.towerLapseTime;
32 0 : this.fortressStartTime = 0; // Fortresses, will start as soon as available (city phase).
33 0 : this.fortressLapseTime = this.Config.Military.fortressLapseTime;
34 0 : this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive);
35 0 : this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive);
36 :
37 0 : this.basesManager = new PETRA.BasesManager(this.Config);
38 0 : this.attackManager = new PETRA.AttackManager(this.Config);
39 0 : this.buildManager = new PETRA.BuildManager();
40 0 : this.defenseManager = new PETRA.DefenseManager(this.Config);
41 0 : this.tradeManager = new PETRA.TradeManager(this.Config);
42 0 : this.navalManager = new PETRA.NavalManager(this.Config);
43 0 : this.researchManager = new PETRA.ResearchManager(this.Config);
44 0 : this.diplomacyManager = new PETRA.DiplomacyManager(this.Config);
45 0 : this.garrisonManager = new PETRA.GarrisonManager(this.Config);
46 0 : this.victoryManager = new PETRA.VictoryManager(this.Config);
47 0 : this.emergencyManager = new PETRA.EmergencyManager(this.Config);
48 :
49 0 : this.capturableTargets = new Map();
50 0 : this.capturableTargetsTime = 0;
51 : };
52 :
53 : /** More initialisation for stuff that needs the gameState */
54 0 : PETRA.HQ.prototype.init = function(gameState, queues)
55 : {
56 0 : this.territoryMap = PETRA.createTerritoryMap(gameState);
57 : // create borderMap: flag cells on the border of the map
58 : // then this map will be completed with our frontier in updateTerritories
59 0 : this.borderMap = PETRA.createBorderMap(gameState);
60 : // list of allowed regions
61 0 : this.landRegions = {};
62 : // try to determine if we have a water map
63 0 : this.navalMap = false;
64 0 : this.navalRegions = {};
65 :
66 0 : this.treasures = gameState.getEntities().filter(ent => ent.isTreasure());
67 0 : this.treasures.registerUpdates();
68 0 : this.currentPhase = gameState.currentPhase();
69 0 : this.decayingStructures = new Set();
70 0 : this.emergencyManager.init(gameState);
71 : };
72 :
73 : /**
74 : * initialization needed after deserialization (only called when deserialization)
75 : */
76 0 : PETRA.HQ.prototype.postinit = function(gameState)
77 : {
78 0 : this.basesManager.postinit(gameState);
79 0 : this.updateTerritories(gameState);
80 : };
81 :
82 : /**
83 : * returns the sea index linking regions 1 and region 2 (supposed to be different land region)
84 : * otherwise return undefined
85 : * for the moment, only the case land-sea-land is supported
86 : */
87 0 : PETRA.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2)
88 : {
89 0 : let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2);
90 0 : if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water")
91 0 : return path[1];
92 :
93 0 : if (this.Config.debug > 1)
94 : {
95 0 : API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path));
96 0 : API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1]));
97 0 : API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2]));
98 : }
99 0 : return undefined;
100 : };
101 :
102 0 : PETRA.HQ.prototype.checkEvents = function(gameState, events)
103 : {
104 0 : this.buildManager.checkEvents(gameState, events);
105 :
106 0 : if (events.TerritoriesChanged.length || events.DiplomacyChanged.length)
107 0 : this.updateTerritories(gameState);
108 :
109 0 : for (let evt of events.DiplomacyChanged)
110 : {
111 0 : if (evt.player != PlayerID && evt.otherPlayer != PlayerID)
112 0 : continue;
113 : // Reset the entities collections which depend on diplomacy
114 0 : gameState.resetOnDiplomacyChanged();
115 0 : break;
116 : }
117 :
118 0 : this.basesManager.checkEvents(gameState, events);
119 :
120 0 : for (let evt of events.ConstructionFinished)
121 : {
122 0 : if (evt.newentity == evt.entity) // repaired building
123 0 : continue;
124 0 : let ent = gameState.getEntityById(evt.newentity);
125 0 : if (!ent || ent.owner() != PlayerID)
126 0 : continue;
127 0 : if (ent.hasClass("Market") && this.maxFields)
128 0 : this.maxFields = false;
129 : }
130 :
131 0 : for (let evt of events.OwnershipChanged) // capture events
132 : {
133 0 : if (evt.to != PlayerID)
134 0 : continue;
135 0 : let ent = gameState.getEntityById(evt.entity);
136 0 : if (!ent)
137 0 : continue;
138 0 : if (!ent.hasClass("Unit"))
139 : {
140 0 : if (ent.decaying())
141 : {
142 0 : if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true))
143 0 : continue;
144 0 : if (!this.decayingStructures.has(evt.entity))
145 0 : this.decayingStructures.add(evt.entity);
146 : }
147 0 : continue;
148 : }
149 :
150 0 : ent.setMetadata(PlayerID, "role", undefined);
151 0 : ent.setMetadata(PlayerID, "subrole", undefined);
152 0 : ent.setMetadata(PlayerID, "plan", undefined);
153 0 : ent.setMetadata(PlayerID, "PartOfArmy", undefined);
154 0 : if (ent.hasClass("Trader"))
155 : {
156 0 : ent.setMetadata(PlayerID, "role", PETRA.Worker.ROLE_TRADER);
157 0 : ent.setMetadata(PlayerID, "route", undefined);
158 : }
159 0 : if (ent.hasClass("Worker"))
160 : {
161 0 : ent.setMetadata(PlayerID, "role", PETRA.Worker.ROLE_WORKER);
162 0 : ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE);
163 : }
164 0 : if (ent.hasClass("Ship"))
165 0 : PETRA.setSeaAccess(gameState, ent);
166 0 : if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined)
167 0 : ent.setMetadata(PlayerID, "plan", -1);
168 : }
169 :
170 : // deal with the different rally points of training units: the rally point is set when the training starts
171 : // for the time being, only autogarrison is used
172 :
173 0 : for (let evt of events.TrainingStarted)
174 : {
175 0 : let ent = gameState.getEntityById(evt.entity);
176 0 : if (!ent || !ent.isOwn(PlayerID))
177 0 : continue;
178 :
179 0 : if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length)
180 0 : continue;
181 0 : let metadata = ent._entity.trainingQueue[0].metadata;
182 0 : if (metadata && metadata.garrisonType)
183 0 : ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison
184 : else
185 0 : ent.unsetRallyPoint();
186 : }
187 :
188 0 : for (let evt of events.TrainingFinished)
189 : {
190 0 : for (let entId of evt.entities)
191 : {
192 0 : let ent = gameState.getEntityById(entId);
193 0 : if (!ent || !ent.isOwn(PlayerID))
194 0 : continue;
195 :
196 0 : if (!ent.position())
197 : {
198 : // we are autogarrisoned, check that the holder is registered in the garrisonManager
199 0 : let holder = gameState.getEntityById(ent.garrisonHolderID());
200 0 : if (holder)
201 0 : this.garrisonManager.registerHolder(gameState, holder);
202 : }
203 0 : else if (ent.getMetadata(PlayerID, "garrisonType"))
204 : {
205 : // we were supposed to be autogarrisoned, but this has failed (may-be full)
206 0 : ent.setMetadata(PlayerID, "garrisonType", undefined);
207 : }
208 :
209 : // Check if this unit is no more needed in its attack plan
210 : // (happen when the training ends after the attack is started or aborted)
211 0 : let plan = ent.getMetadata(PlayerID, "plan");
212 0 : if (plan !== undefined && plan >= 0)
213 : {
214 0 : let attack = this.attackManager.getPlan(plan);
215 0 : if (!attack || attack.state !== PETRA.AttackPlan.STATE_UNEXECUTED)
216 0 : ent.setMetadata(PlayerID, "plan", -1);
217 : }
218 : }
219 : }
220 :
221 0 : for (let evt of events.TerritoryDecayChanged)
222 : {
223 0 : let ent = gameState.getEntityById(evt.entity);
224 0 : if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined)
225 0 : continue;
226 0 : if (evt.to)
227 : {
228 0 : if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity))
229 0 : continue;
230 0 : if (!this.decayingStructures.has(evt.entity))
231 0 : this.decayingStructures.add(evt.entity);
232 : }
233 0 : else if (ent.isGarrisonHolder())
234 0 : this.garrisonManager.removeDecayingStructure(evt.entity);
235 : }
236 :
237 : // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties)
238 0 : if (this.Config.difficulty < PETRA.DIFFICULTY_EASY)
239 0 : return;
240 0 : for (let entId of this.decayingStructures)
241 : {
242 0 : let ent = gameState.getEntityById(entId);
243 0 : if (ent && ent.decaying() && ent.isOwn(PlayerID))
244 : {
245 0 : let capture = ent.capturePoints();
246 0 : if (!capture)
247 0 : continue;
248 0 : let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b);
249 0 : if (captureRatio < 0.50)
250 0 : continue;
251 0 : let decayToGaia = true;
252 0 : for (let i = 1; i < capture.length; ++i)
253 : {
254 0 : if (gameState.isPlayerAlly(i) || !capture[i])
255 0 : continue;
256 0 : decayToGaia = false;
257 0 : break;
258 : }
259 0 : if (decayToGaia)
260 0 : continue;
261 0 : let ratioMax = 0.7 + randFloat(0, 0.1);
262 0 : for (let evt of events.Attacked)
263 : {
264 0 : if (ent.id() != evt.target)
265 0 : continue;
266 0 : ratioMax = 0.85 + randFloat(0, 0.1);
267 0 : break;
268 : }
269 0 : if (captureRatio > ratioMax)
270 0 : continue;
271 0 : ent.destroy();
272 : }
273 0 : this.decayingStructures.delete(entId);
274 : }
275 : };
276 :
277 0 : PETRA.HQ.prototype.handleNewBase = function(gameState)
278 : {
279 0 : if (!this.firstBaseConfig)
280 : // This is our first base, let us configure our starting resources.
281 0 : this.configFirstBase(gameState);
282 : else
283 : {
284 : // Let us hope this new base will fix our possible resource shortage.
285 0 : this.saveResources = undefined;
286 0 : this.saveSpace = undefined;
287 0 : this.maxFields = false;
288 : }
289 : };
290 :
291 : /** Ensure that all requirements are met when phasing up*/
292 0 : PETRA.HQ.prototype.checkPhaseRequirements = function(gameState, queues)
293 : {
294 0 : if (gameState.getNumberOfPhases() == this.currentPhase)
295 0 : return;
296 :
297 0 : let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1);
298 : let plan;
299 : let queue;
300 0 : for (let entityReq of requirements)
301 : {
302 : // Village requirements are met elsewhere by constructing more houses
303 0 : if (entityReq.class == "Village" || entityReq.class == "NotField")
304 0 : continue;
305 0 : if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count)
306 0 : continue;
307 0 : switch (entityReq.class)
308 : {
309 : case "Town":
310 0 : if (!queues.economicBuilding.hasQueuedUnits() &&
311 : !queues.militaryBuilding.hasQueuedUnits())
312 : {
313 0 : if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() &&
314 : this.canBuild(gameState, "structures/{civ}/market"))
315 : {
316 0 : plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market", { "phaseUp": true });
317 0 : queue = "economicBuilding";
318 0 : break;
319 : }
320 0 : if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() &&
321 : this.canBuild(gameState, "structures/{civ}/temple"))
322 : {
323 0 : plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/temple", { "phaseUp": true });
324 0 : queue = "economicBuilding";
325 0 : break;
326 : }
327 0 : if (!gameState.getOwnEntitiesByClass("Forge", true).hasEntities() &&
328 : this.canBuild(gameState, "structures/{civ}/forge"))
329 : {
330 0 : plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge", { "phaseUp": true });
331 0 : queue = "militaryBuilding";
332 0 : break;
333 : }
334 : }
335 0 : break;
336 : default:
337 : // All classes not dealt with inside vanilla game.
338 : // We put them for the time being on the economic queue, except if wonder
339 0 : queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding";
340 0 : if (!queues[queue].hasQueuedUnits())
341 : {
342 0 : let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]);
343 0 : if (structure && this.canBuild(gameState, structure))
344 0 : plan = new PETRA.ConstructionPlan(gameState, structure, { "phaseUp": true });
345 : }
346 : }
347 :
348 0 : if (plan)
349 : {
350 0 : if (queue == "wonder")
351 : {
352 0 : gameState.ai.queueManager.changePriority("majorTech", 400, { "phaseUp": true });
353 0 : plan.queueToReset = "majorTech";
354 : }
355 : else
356 : {
357 0 : gameState.ai.queueManager.changePriority(queue, 1000, { "phaseUp": true });
358 0 : plan.queueToReset = queue;
359 : }
360 0 : queues[queue].addPlan(plan);
361 0 : return;
362 : }
363 : }
364 : };
365 :
366 : /** Called by any "phase" research plan once it's started */
367 0 : PETRA.HQ.prototype.OnPhaseUp = function(gameState, phase)
368 : {
369 : };
370 :
371 : /** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */
372 0 : PETRA.HQ.prototype.trainMoreWorkers = function(gameState, queues)
373 : {
374 : // default template
375 0 : let requirementsDef = [ ["costsResource", 1, "food"] ];
376 0 : const classesDef = ["Support+Worker"];
377 0 : let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef);
378 :
379 : // counting the workers that aren't part of a plan
380 0 : let numberOfWorkers = 0; // all workers
381 0 : let numberOfSupports = 0; // only support workers (i.e. non fighting)
382 0 : gameState.getOwnUnits().forEach(ent => {
383 0 : if (ent.getMetadata(PlayerID, "role") === PETRA.Worker.ROLE_WORKER && ent.getMetadata(PlayerID, "plan") === undefined)
384 : {
385 0 : ++numberOfWorkers;
386 0 : if (ent.hasClass("Support"))
387 0 : ++numberOfSupports;
388 : }
389 : });
390 0 : let numberInTraining = 0;
391 0 : gameState.getOwnTrainingFacilities().forEach(function(ent) {
392 0 : for (let item of ent.trainingQueue())
393 : {
394 0 : numberInTraining += item.count;
395 0 : if (item.metadata && item.metadata.role && item.metadata.role === PETRA.Worker.ROLE_WORKER &&
396 : item.metadata.plan === undefined)
397 : {
398 0 : numberOfWorkers += item.count;
399 0 : if (item.metadata.support)
400 0 : numberOfSupports += item.count;
401 : }
402 : }
403 : });
404 :
405 : // Anticipate the optimal batch size when this queue will start
406 : // and adapt the batch size of the first and second queued workers to the present population
407 : // to ease a possible recovery if our population was drastically reduced by an attack
408 : // (need to go up to second queued as it is accounted in queueManager)
409 0 : let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10));
410 0 : if (queues.villager.plans[0])
411 : {
412 0 : queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size);
413 0 : if (queues.villager.plans[1])
414 0 : queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size);
415 : }
416 0 : if (queues.citizenSoldier.plans[0])
417 : {
418 0 : queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size);
419 0 : if (queues.citizenSoldier.plans[1])
420 0 : queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size);
421 : }
422 :
423 0 : let numberOfQueuedSupports = queues.villager.countQueuedUnits();
424 0 : let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits();
425 0 : let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers;
426 0 : let numberTotal = numberOfWorkers + numberQueued;
427 :
428 0 : if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10)
429 0 : return;
430 0 : if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 &&
431 : this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))))
432 0 : return;
433 0 : if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15)
434 0 : return;
435 :
436 : // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers
437 : // with supportRatio fraction of support units. But we want to have more support (less cost) at startup.
438 : // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers))
439 : // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers
440 : // and gives a ratio alfa at startup.
441 :
442 0 : let supportRatio = this.supportRatio;
443 0 : let alpha = 0.85;
444 0 : if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field")))
445 0 : supportRatio = Math.min(this.supportRatio, 0.1);
446 0 : if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks[PETRA.AttackPlan.TYPE_RUSH].length)
447 0 : alpha = 0.7;
448 0 : if (gameState.isCeasefireActive())
449 0 : alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180;
450 0 : let supportMax = supportRatio * this.targetNumWorkers;
451 0 : let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax));
452 :
453 : let template;
454 0 : if (!templateDef || numberOfSupports + numberOfQueuedSupports > supportNum)
455 : {
456 : let requirements;
457 0 : if (numberTotal < 45)
458 0 : requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ];
459 : else
460 0 : requirements = [ ["strength", 1] ];
461 :
462 0 : const classes = [["CitizenSoldier", "Infantry"]];
463 : // We want at least 33% ranged and 33% melee.
464 0 : classes[0].push(pickRandom(["Ranged", "Melee", "Infantry"]));
465 :
466 0 : template = this.findBestTrainableUnit(gameState, classes, requirements);
467 : }
468 :
469 : // If the template variable is empty, the default unit (Support unit) will be used
470 : // base "0" means automatic choice of base
471 0 : if (!template && templateDef)
472 0 : queues.villager.addPlan(new PETRA.TrainingPlan(gameState, templateDef, { "role": PETRA.Worker.ROLE_WORKER, "base": 0, "support": true }, size, size));
473 0 : else if (template)
474 0 : queues.citizenSoldier.addPlan(new PETRA.TrainingPlan(gameState, template, { "role": PETRA.Worker.ROLE_WORKER, "base": 0 }, size, size));
475 : };
476 :
477 : /** picks the best template based on parameters and classes */
478 0 : PETRA.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements)
479 : {
480 : let units;
481 0 : if (classes.indexOf("Hero") != -1)
482 0 : units = gameState.findTrainableUnits(classes, []);
483 : // We do not want siege tower as AI does not know how to use it nor hero when not explicitely specified.
484 : else
485 0 : units = gameState.findTrainableUnits(classes, ["Hero", "SiegeTower"]);
486 :
487 0 : if (!units.length)
488 0 : return undefined;
489 :
490 0 : let parameters = requirements.slice();
491 0 : let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory
492 0 : let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources
493 0 : for (let type in remainingResources)
494 : {
495 0 : if (availableResources[type] > 800)
496 0 : continue;
497 0 : if (remainingResources[type] > 800)
498 0 : continue;
499 0 : let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2;
500 0 : let toAdd = true;
501 0 : for (let param of parameters)
502 : {
503 0 : if (param[0] != "costsResource" || param[2] != type)
504 0 : continue;
505 0 : param[1] = Math.min(param[1], costsResource);
506 0 : toAdd = false;
507 0 : break;
508 : }
509 0 : if (toAdd)
510 0 : parameters.push(["costsResource", costsResource, type]);
511 : }
512 :
513 0 : units.sort((a, b) => {
514 0 : let aCost = 1 + a[1].costSum();
515 0 : let bCost = 1 + b[1].costSum();
516 0 : let aValue = 0.1;
517 0 : let bValue = 0.1;
518 0 : for (let param of parameters)
519 : {
520 0 : if (param[0] == "strength")
521 : {
522 0 : aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1];
523 0 : bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1];
524 : }
525 0 : else if (param[0] == "siegeStrength")
526 : {
527 0 : aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1];
528 0 : bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1];
529 : }
530 0 : else if (param[0] == "speed")
531 : {
532 0 : aValue += a[1].walkSpeed() * param[1];
533 0 : bValue += b[1].walkSpeed() * param[1];
534 : }
535 0 : else if (param[0] == "costsResource")
536 : {
537 : // requires a third parameter which is the resource
538 0 : if (a[1].cost()[param[2]])
539 0 : aValue *= param[1];
540 0 : if (b[1].cost()[param[2]])
541 0 : bValue *= param[1];
542 : }
543 0 : else if (param[0] == "canGather")
544 : {
545 : // checking against wood, could be anything else really.
546 0 : if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"])
547 0 : aValue *= param[1];
548 0 : if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"])
549 0 : bValue *= param[1];
550 : }
551 : else
552 0 : API3.warn(" trainMoreUnits avec non prevu " + uneval(param));
553 : }
554 0 : return -aValue/aCost + bValue/bCost;
555 : });
556 0 : return units[0][0];
557 : };
558 :
559 : /**
560 : * returns an entity collection of workers through BaseManager.pickBuilders
561 : * TODO: when same accessIndex, sort by distance
562 : */
563 0 : PETRA.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number)
564 : {
565 0 : return this.basesManager.bulkPickWorkers(gameState, baseRef, number);
566 : };
567 :
568 0 : PETRA.HQ.prototype.getTotalResourceLevel = function(gameState, resources, proximity)
569 : {
570 0 : return this.basesManager.getTotalResourceLevel(gameState, resources, proximity);
571 : };
572 :
573 : /**
574 : * Returns the current gather rate
575 : * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that.
576 : */
577 0 : PETRA.HQ.prototype.GetCurrentGatherRates = function(gameState)
578 : {
579 0 : return this.basesManager.GetCurrentGatherRates(gameState);
580 : };
581 :
582 : /**
583 : * Returns the wanted gather rate.
584 : */
585 0 : PETRA.HQ.prototype.GetWantedGatherRates = function(gameState)
586 : {
587 0 : if (!this.turnCache.wantedRates)
588 0 : this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState);
589 :
590 0 : return this.turnCache.wantedRates;
591 : };
592 :
593 : /**
594 : * Pick the resource which most needs another worker
595 : * How this works:
596 : * We get the rates we would want to have to be able to deal with our plans
597 : * We get our current rates
598 : * We compare; we pick the one where the discrepancy is highest.
599 : * Need to balance long-term needs and possible short-term needs.
600 : */
601 0 : PETRA.HQ.prototype.pickMostNeededResources = function(gameState, allowedResources = [])
602 : {
603 0 : let wantedRates = this.GetWantedGatherRates(gameState);
604 0 : let currentRates = this.GetCurrentGatherRates(gameState);
605 0 : if (!allowedResources.length)
606 0 : allowedResources = Resources.GetCodes();
607 :
608 0 : let needed = [];
609 0 : for (let res of allowedResources)
610 0 : needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] });
611 :
612 0 : needed.sort((a, b) => {
613 0 : if (a.current < a.wanted && b.current < b.wanted)
614 : {
615 0 : if (a.current && b.current)
616 0 : return b.wanted / b.current - a.wanted / a.current;
617 0 : if (a.current)
618 0 : return 1;
619 0 : if (b.current)
620 0 : return -1;
621 0 : return b.wanted - a.wanted;
622 : }
623 0 : if (a.current < a.wanted || a.wanted && !b.wanted)
624 0 : return -1;
625 0 : if (b.current < b.wanted || b.wanted && !a.wanted)
626 0 : return 1;
627 0 : return a.current - a.wanted - b.current + b.wanted;
628 : });
629 0 : return needed;
630 : };
631 :
632 : /**
633 : * Returns the best position to build a new Civil Center
634 : * Whose primary function would be to reach new resources of type "resource".
635 : */
636 0 : PETRA.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic)
637 : {
638 : // This builds a map. The procedure is fairly simple. It adds the resource maps
639 : // (which are dynamically updated and are made so that they will facilitate DP placement)
640 : // Then look for a good spot.
641 :
642 0 : Engine.ProfileStart("findEconomicCCLocation");
643 :
644 : // obstruction map
645 0 : let obstructions = PETRA.createObstructionMap(gameState, 0, template);
646 0 : let halfSize = 0;
647 0 : if (template.get("Footprint/Square"))
648 0 : halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
649 0 : else if (template.get("Footprint/Circle"))
650 0 : halfSize = +template.get("Footprint/Circle/@radius");
651 :
652 0 : let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
653 0 : const dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClasses(["CivCentre", "Unit"])));
654 0 : let ccList = [];
655 0 : for (let cc of ccEnts.values())
656 0 : ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) });
657 0 : let dpList = [];
658 0 : for (let dp of dpEnts.values())
659 0 : dpList.push({ "ent": dp, "pos": dp.position(), "territory": this.territoryMap.getOwner(dp.position()) });
660 :
661 : let bestIdx;
662 : let bestVal;
663 0 : let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
664 0 : let scale = 250 * 250;
665 : let proxyAccess;
666 0 : let nbShips = this.navalManager.transportShips.length;
667 0 : if (proximity) // this is our first base
668 : {
669 : // if our first base, ensure room around
670 0 : radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize);
671 : // scale is the typical scale at which we want to find a location for our first base
672 : // look for bigger scale if we start from a ship (access < 2) or from a small island
673 0 : let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize;
674 0 : proxyAccess = gameState.ai.accessibility.getAccessValue(proximity);
675 0 : if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000)
676 0 : scale = 400 * 400;
677 : }
678 :
679 0 : let width = this.territoryMap.width;
680 0 : let cellSize = this.territoryMap.cellSize;
681 :
682 : // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands).
683 0 : let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive;
684 0 : let nearbyRejected = Math.square(120); // Reject if too near from any cc
685 0 : let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc
686 0 : let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc
687 0 : let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc
688 0 : let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc
689 0 : let maxNoAccessDisfavored = Math.square(500); // Disfavor if quite far from an inaccessible ally cc
690 :
691 0 : let cut = 60;
692 0 : if (fromStrategic || proximity) // be less restrictive
693 0 : cut = 30;
694 :
695 0 : for (let j = 0; j < this.territoryMap.length; ++j)
696 : {
697 0 : if (this.territoryMap.getOwnerIndex(j) != 0)
698 0 : continue;
699 : // With enough room around to build the cc
700 0 : let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
701 0 : if (i < 0)
702 0 : continue;
703 : // We require that it is accessible
704 0 : let index = gameState.ai.accessibility.landPassMap[i];
705 0 : if (!this.landRegions[index])
706 0 : continue;
707 0 : if (proxyAccess && nbShips == 0 && proxyAccess != index)
708 0 : continue;
709 :
710 0 : let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps
711 : // Checking distance to other cc
712 0 : let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
713 : // We will be more tolerant for cc around our oversea docks
714 0 : let oversea = false;
715 :
716 0 : if (proximity) // This is our first cc, let's do it near our units
717 0 : norm /= 1 + API3.SquareVectorDistance(proximity, pos) / scale;
718 : else
719 : {
720 0 : let minDist = Math.min();
721 0 : let accessible = false;
722 :
723 0 : for (let cc of ccList)
724 : {
725 0 : let dist = API3.SquareVectorDistance(cc.pos, pos);
726 0 : if (dist < nearbyRejected)
727 : {
728 0 : norm = 0;
729 0 : break;
730 : }
731 0 : if (!cc.ally)
732 0 : continue;
733 0 : if (dist < nearbyAllyRejected)
734 : {
735 0 : norm = 0;
736 0 : break;
737 : }
738 0 : if (dist < nearbyAllyDisfavored)
739 0 : norm *= 0.5;
740 :
741 0 : if (dist < minDist)
742 0 : minDist = dist;
743 0 : accessible = accessible || index == PETRA.getLandAccess(gameState, cc.ent);
744 : }
745 0 : if (norm == 0)
746 0 : continue;
747 :
748 0 : if (accessible && minDist > maxAccessRejected)
749 0 : continue;
750 :
751 0 : if (minDist > maxAccessDisfavored) // Disfavor if quite far from any allied cc
752 : {
753 0 : if (!accessible)
754 : {
755 0 : if (minDist > maxNoAccessDisfavored)
756 0 : norm *= 0.5;
757 : else
758 0 : norm *= 0.8;
759 : }
760 : else
761 0 : norm *= 0.5;
762 : }
763 :
764 : // Not near any of our dropsite, except for oversea docks
765 0 : oversea = !accessible && dpList.some(dp => PETRA.getLandAccess(gameState, dp.ent) == index);
766 0 : if (!oversea)
767 : {
768 0 : for (let dp of dpList)
769 : {
770 0 : let dist = API3.SquareVectorDistance(dp.pos, pos);
771 0 : if (dist < 3600)
772 : {
773 0 : norm = 0;
774 0 : break;
775 : }
776 0 : else if (dist < 6400)
777 0 : norm *= 0.5;
778 : }
779 : }
780 0 : if (norm == 0)
781 0 : continue;
782 : }
783 :
784 0 : if (this.borderMap.map[j] & PETRA.fullBorder_Mask) // disfavor the borders of the map
785 0 : norm *= 0.5;
786 :
787 0 : let val = 2 * gameState.sharedScript.ccResourceMaps[resource].map[j];
788 0 : for (let res in gameState.sharedScript.resourceMaps)
789 0 : if (res != "food")
790 0 : val += gameState.sharedScript.ccResourceMaps[res].map[j];
791 0 : val *= norm;
792 :
793 : // If oversea, be just above threshold to be accepted if nothing else
794 0 : if (oversea)
795 0 : val = Math.max(val, cut + 0.1);
796 :
797 0 : if (bestVal !== undefined && val < bestVal)
798 0 : continue;
799 0 : if (this.isDangerousLocation(gameState, pos, halfSize))
800 0 : continue;
801 0 : bestVal = val;
802 0 : bestIdx = i;
803 : }
804 :
805 0 : Engine.ProfileStop();
806 :
807 0 : if (bestVal === undefined)
808 0 : return false;
809 0 : if (this.Config.debug > 1)
810 0 : API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal);
811 : // not good enough.
812 0 : if (bestVal < cut)
813 0 : return false;
814 :
815 0 : let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
816 0 : let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
817 :
818 : // Define a minimal number of wanted ships in the seas reaching this new base
819 0 : let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx];
820 0 : for (const base of this.baseManagers())
821 : {
822 0 : if (!base.anchor || base.accessIndex == indexIdx)
823 0 : continue;
824 0 : let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx);
825 0 : if (sea !== undefined)
826 0 : this.navalManager.setMinimalTransportShips(gameState, sea, 1);
827 : }
828 :
829 0 : return [x, z];
830 : };
831 :
832 : /**
833 : * Returns the best position to build a new Civil Center
834 : * Whose primary function would be to assure territorial continuity with our allies
835 : */
836 0 : PETRA.HQ.prototype.findStrategicCCLocation = function(gameState, template)
837 : {
838 : // This builds a map. The procedure is fairly simple.
839 : // We minimize the Sum((dist - 300)^2) where the sum is on the three nearest allied CC
840 : // with the constraints that all CC have dist > 200 and at least one have dist < 400
841 : // This needs at least 2 CC. Otherwise, go back to economic CC.
842 :
843 0 : let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
844 0 : let ccList = [];
845 0 : let numAllyCC = 0;
846 0 : for (let cc of ccEnts.values())
847 : {
848 0 : let ally = gameState.isPlayerAlly(cc.owner());
849 0 : ccList.push({ "pos": cc.position(), "ally": ally });
850 0 : if (ally)
851 0 : ++numAllyCC;
852 : }
853 0 : if (numAllyCC < 2)
854 0 : return this.findEconomicCCLocation(gameState, template, "wood", undefined, true);
855 :
856 0 : Engine.ProfileStart("findStrategicCCLocation");
857 :
858 : // obstruction map
859 0 : let obstructions = PETRA.createObstructionMap(gameState, 0, template);
860 0 : let halfSize = 0;
861 0 : if (template.get("Footprint/Square"))
862 0 : halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
863 0 : else if (template.get("Footprint/Circle"))
864 0 : halfSize = +template.get("Footprint/Circle/@radius");
865 :
866 : let bestIdx;
867 : let bestVal;
868 0 : let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
869 :
870 0 : let width = this.territoryMap.width;
871 0 : let cellSize = this.territoryMap.cellSize;
872 : let currentVal, delta;
873 : let distcc0, distcc1, distcc2;
874 0 : let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 40 * this.Config.personality.defensive;
875 :
876 0 : for (let j = 0; j < this.territoryMap.length; ++j)
877 : {
878 0 : if (this.territoryMap.getOwnerIndex(j) != 0)
879 0 : continue;
880 : // with enough room around to build the cc
881 0 : let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
882 0 : if (i < 0)
883 0 : continue;
884 : // we require that it is accessible
885 0 : let index = gameState.ai.accessibility.landPassMap[i];
886 0 : if (!this.landRegions[index])
887 0 : continue;
888 :
889 : // checking distances to other cc
890 0 : let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
891 0 : let minDist = Math.min();
892 0 : distcc0 = undefined;
893 :
894 0 : for (let cc of ccList)
895 : {
896 0 : let dist = API3.SquareVectorDistance(cc.pos, pos);
897 0 : if (dist < 14000) // Reject if too near from any cc
898 : {
899 0 : minDist = 0;
900 0 : break;
901 : }
902 0 : if (!cc.ally)
903 0 : continue;
904 0 : if (dist < 62000) // Reject if quite near from ally cc
905 : {
906 0 : minDist = 0;
907 0 : break;
908 : }
909 0 : if (dist < minDist)
910 0 : minDist = dist;
911 :
912 0 : if (!distcc0 || dist < distcc0)
913 : {
914 0 : distcc2 = distcc1;
915 0 : distcc1 = distcc0;
916 0 : distcc0 = dist;
917 : }
918 0 : else if (!distcc1 || dist < distcc1)
919 : {
920 0 : distcc2 = distcc1;
921 0 : distcc1 = dist;
922 : }
923 0 : else if (!distcc2 || dist < distcc2)
924 0 : distcc2 = dist;
925 : }
926 0 : if (minDist < 1 || minDist > 170000 && !this.navalMap)
927 0 : continue;
928 :
929 0 : delta = Math.sqrt(distcc0) - favoredDistance;
930 0 : currentVal = delta*delta;
931 0 : delta = Math.sqrt(distcc1) - favoredDistance;
932 0 : currentVal += delta*delta;
933 0 : if (distcc2)
934 : {
935 0 : delta = Math.sqrt(distcc2) - favoredDistance;
936 0 : currentVal += delta*delta;
937 : }
938 : // disfavor border of the map
939 0 : if (this.borderMap.map[j] & PETRA.fullBorder_Mask)
940 0 : currentVal += 10000;
941 :
942 0 : if (bestVal !== undefined && currentVal > bestVal)
943 0 : continue;
944 0 : if (this.isDangerousLocation(gameState, pos, halfSize))
945 0 : continue;
946 0 : bestVal = currentVal;
947 0 : bestIdx = i;
948 : }
949 :
950 0 : if (this.Config.debug > 1)
951 0 : API3.warn("We've found a strategic base with bestVal = " + bestVal);
952 :
953 0 : Engine.ProfileStop();
954 :
955 0 : if (bestVal === undefined)
956 0 : return undefined;
957 :
958 0 : let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
959 0 : let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
960 :
961 : // Define a minimal number of wanted ships in the seas reaching this new base
962 0 : let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx];
963 0 : for (const base of this.baseManagers())
964 : {
965 0 : if (!base.anchor || base.accessIndex == indexIdx)
966 0 : continue;
967 0 : let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx);
968 0 : if (sea !== undefined)
969 0 : this.navalManager.setMinimalTransportShips(gameState, sea, 1);
970 : }
971 :
972 0 : return [x, z];
973 : };
974 :
975 : /**
976 : * Returns the best position to build a new market: if the allies already have a market, build it as far as possible
977 : * from it, although not in our border to be able to defend it easily. If no allied market, our second market will
978 : * follow the same logic.
979 : * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance
980 : * for performance reasons.
981 : */
982 0 : PETRA.HQ.prototype.findMarketLocation = function(gameState, template)
983 : {
984 0 : let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Trade"), gameState.getExclusiveAllyEntities()).toEntityArray();
985 0 : if (!markets.length)
986 0 : markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Trade"), gameState.getOwnStructures()).toEntityArray();
987 :
988 0 : if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan
989 0 : return [-1, -1, -1, 0];
990 :
991 : // No need for more than one market when we cannot trade.
992 0 : if (!Resources.GetTradableCodes().length)
993 0 : return false;
994 :
995 : // obstruction map
996 0 : let obstructions = PETRA.createObstructionMap(gameState, 0, template);
997 0 : let halfSize = 0;
998 0 : if (template.get("Footprint/Square"))
999 0 : halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
1000 0 : else if (template.get("Footprint/Circle"))
1001 0 : halfSize = +template.get("Footprint/Circle/@radius");
1002 :
1003 : let bestIdx;
1004 : let bestJdx;
1005 : let bestVal;
1006 : let bestDistSq;
1007 : let bestGainMult;
1008 0 : let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
1009 0 : const isNavalMarket = template.hasClasses(["Naval+Trade"]);
1010 :
1011 0 : let width = this.territoryMap.width;
1012 0 : let cellSize = this.territoryMap.cellSize;
1013 :
1014 0 : let traderTemplatesGains = gameState.getTraderTemplatesGains();
1015 :
1016 0 : for (let j = 0; j < this.territoryMap.length; ++j)
1017 : {
1018 : // do not try on the narrow border of our territory
1019 0 : if (this.borderMap.map[j] & PETRA.narrowFrontier_Mask)
1020 0 : continue;
1021 0 : if (this.baseAtIndex(j) == 0) // only in our territory
1022 0 : continue;
1023 : // with enough room around to build the market
1024 0 : let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
1025 0 : if (i < 0)
1026 0 : continue;
1027 0 : let index = gameState.ai.accessibility.landPassMap[i];
1028 0 : if (!this.landRegions[index])
1029 0 : continue;
1030 0 : let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
1031 : // checking distances to other markets
1032 0 : let maxVal = 0;
1033 : let maxDistSq;
1034 : let maxGainMult;
1035 : let gainMultiplier;
1036 0 : for (let market of markets)
1037 : {
1038 0 : if (isNavalMarket && template.hasClasses(["Naval+Trade"]))
1039 : {
1040 0 : if (PETRA.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true))
1041 0 : continue;
1042 0 : gainMultiplier = traderTemplatesGains.navalGainMultiplier;
1043 : }
1044 0 : else if (PETRA.getLandAccess(gameState, market) == index &&
1045 : !PETRA.isLineInsideEnemyTerritory(gameState, market.position(), pos))
1046 0 : gainMultiplier = traderTemplatesGains.landGainMultiplier;
1047 : else
1048 0 : continue;
1049 0 : if (!gainMultiplier)
1050 0 : continue;
1051 0 : let distSq = API3.SquareVectorDistance(market.position(), pos);
1052 0 : if (gainMultiplier * distSq > maxVal)
1053 : {
1054 0 : maxVal = gainMultiplier * distSq;
1055 0 : maxDistSq = distSq;
1056 0 : maxGainMult = gainMultiplier;
1057 : }
1058 : }
1059 0 : if (maxVal == 0)
1060 0 : continue;
1061 0 : if (bestVal !== undefined && maxVal < bestVal)
1062 0 : continue;
1063 0 : if (this.isDangerousLocation(gameState, pos, halfSize))
1064 0 : continue;
1065 0 : bestVal = maxVal;
1066 0 : bestDistSq = maxDistSq;
1067 0 : bestGainMult = maxGainMult;
1068 0 : bestIdx = i;
1069 0 : bestJdx = j;
1070 : }
1071 :
1072 0 : if (this.Config.debug > 1)
1073 0 : API3.warn("We found a market position with bestVal = " + bestVal);
1074 :
1075 0 : if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan
1076 0 : return [-1, -1, -1, 0];
1077 0 : let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize));
1078 0 : if (this.Config.debug > 1)
1079 0 : API3.warn("this would give a trading gain of " + expectedGain);
1080 : // Do not keep it if gain is too small, except if this is our first Market.
1081 : let idx;
1082 0 : if (expectedGain < this.tradeManager.minimalGain)
1083 : {
1084 0 : if (template.hasClass("Market") &&
1085 : !gameState.getOwnEntitiesByClass("Market", true).hasEntities())
1086 0 : idx = -1; // Needed by queueplanBuilding manager to keep that Market.
1087 : else
1088 0 : return false;
1089 : }
1090 : else
1091 0 : idx = this.baseAtIndex(bestJdx);
1092 :
1093 0 : let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
1094 0 : let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
1095 0 : return [x, z, idx, expectedGain];
1096 : };
1097 :
1098 : /**
1099 : * Returns the best position to build defensive buildings (fortress and towers)
1100 : * Whose primary function is to defend our borders
1101 : */
1102 0 : PETRA.HQ.prototype.findDefensiveLocation = function(gameState, template)
1103 : {
1104 : // We take the point in our territory which is the nearest to any enemy cc
1105 : // but requiring a minimal distance with our other defensive structures
1106 : // and not in range of any enemy defensive structure to avoid building under fire.
1107 :
1108 0 : const ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClasses(["Fortress", "Tower"])).toEntityArray();
1109 0 : let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))).
1110 : filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"]));
1111 0 : if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals
1112 : {
1113 0 : enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))).
1114 : filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"]));
1115 0 : if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory())
1116 0 : enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))).
1117 : filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"]));
1118 0 : if (!enemyStructures.hasEntities())
1119 0 : return undefined;
1120 : }
1121 0 : enemyStructures = enemyStructures.toEntityArray();
1122 :
1123 0 : let wonderMode = gameState.getVictoryConditions().has("wonder");
1124 : let wonderDistmin;
1125 : let wonders;
1126 0 : if (wonderMode)
1127 : {
1128 0 : wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray();
1129 0 : wonderMode = wonders.length != 0;
1130 0 : if (wonderMode)
1131 0 : wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius());
1132 : }
1133 :
1134 : // obstruction map
1135 0 : let obstructions = PETRA.createObstructionMap(gameState, 0, template);
1136 0 : let halfSize = 0;
1137 0 : if (template.get("Footprint/Square"))
1138 0 : halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
1139 0 : else if (template.get("Footprint/Circle"))
1140 0 : halfSize = +template.get("Footprint/Circle/@radius");
1141 :
1142 : let bestIdx;
1143 : let bestJdx;
1144 : let bestVal;
1145 0 : let width = this.territoryMap.width;
1146 0 : let cellSize = this.territoryMap.cellSize;
1147 :
1148 0 : let isTower = template.hasClass("Tower");
1149 0 : let isFortress = template.hasClass("Fortress");
1150 : let radius;
1151 0 : if (isFortress)
1152 0 : radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize);
1153 : else
1154 0 : radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
1155 :
1156 0 : for (let j = 0; j < this.territoryMap.length; ++j)
1157 : {
1158 0 : if (!wonderMode)
1159 : {
1160 : // do not try if well inside or outside territory
1161 0 : if (!(this.borderMap.map[j] & PETRA.fullFrontier_Mask))
1162 0 : continue;
1163 0 : if (this.borderMap.map[j] & PETRA.largeFrontier_Mask && isTower)
1164 0 : continue;
1165 : }
1166 0 : if (this.baseAtIndex(j) == 0) // inaccessible cell
1167 0 : continue;
1168 : // with enough room around to build the cc
1169 0 : let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions);
1170 0 : if (i < 0)
1171 0 : continue;
1172 :
1173 0 : let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
1174 : // checking distances to other structures
1175 0 : let minDist = Math.min();
1176 :
1177 0 : let dista = 0;
1178 0 : if (wonderMode)
1179 : {
1180 0 : dista = API3.SquareVectorDistance(wonders[0].position(), pos);
1181 0 : if (dista < wonderDistmin)
1182 0 : continue;
1183 0 : dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder
1184 : }
1185 :
1186 0 : for (let str of enemyStructures)
1187 : {
1188 0 : if (str.foundationProgress() !== undefined)
1189 0 : continue;
1190 0 : let strPos = str.position();
1191 0 : if (!strPos)
1192 0 : continue;
1193 0 : let dist = API3.SquareVectorDistance(strPos, pos);
1194 0 : if (dist < 6400) // TODO check on true attack range instead of this 80×80
1195 : {
1196 0 : minDist = -1;
1197 0 : break;
1198 : }
1199 0 : if (str.hasClass("CivCentre") && dist + dista < minDist)
1200 0 : minDist = dist + dista;
1201 : }
1202 0 : if (minDist < 0)
1203 0 : continue;
1204 :
1205 0 : let cutDist = 900; // 30×30 TODO maybe increase it
1206 0 : for (let str of ownStructures)
1207 : {
1208 0 : let strPos = str.position();
1209 0 : if (!strPos)
1210 0 : continue;
1211 0 : if (API3.SquareVectorDistance(strPos, pos) < cutDist)
1212 : {
1213 0 : minDist = -1;
1214 0 : break;
1215 : }
1216 : }
1217 0 : if (minDist < 0 || minDist == Math.min())
1218 0 : continue;
1219 0 : if (bestVal !== undefined && minDist > bestVal)
1220 0 : continue;
1221 0 : if (this.isDangerousLocation(gameState, pos, halfSize))
1222 0 : continue;
1223 0 : bestVal = minDist;
1224 0 : bestIdx = i;
1225 0 : bestJdx = j;
1226 : }
1227 :
1228 0 : if (bestVal === undefined)
1229 0 : return undefined;
1230 :
1231 0 : let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
1232 0 : let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
1233 0 : return [x, z, this.baseAtIndex(bestJdx)];
1234 : };
1235 :
1236 0 : PETRA.HQ.prototype.buildTemple = function(gameState, queues)
1237 : {
1238 : // at least one market (which have the same queue) should be build before any temple
1239 0 : if (queues.economicBuilding.hasQueuedUnits() ||
1240 : gameState.getOwnEntitiesByClass("Temple", true).hasEntities() ||
1241 : !gameState.getOwnEntitiesByClass("Market", true).hasEntities())
1242 0 : return;
1243 : // Try to build a temple earlier if in regicide to recruit healer guards
1244 0 : if (this.currentPhase < 3 && !gameState.getVictoryConditions().has("regicide"))
1245 0 : return;
1246 :
1247 0 : let templateName = "structures/{civ}/temple";
1248 0 : if (this.canBuild(gameState, "structures/{civ}/temple_vesta"))
1249 0 : templateName = "structures/{civ}/temple_vesta";
1250 0 : else if (!this.canBuild(gameState, templateName))
1251 0 : return;
1252 0 : queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, templateName));
1253 : };
1254 :
1255 0 : PETRA.HQ.prototype.buildMarket = function(gameState, queues)
1256 : {
1257 0 : if (gameState.getOwnEntitiesByClass("Market", true).hasEntities() ||
1258 : !this.canBuild(gameState, "structures/{civ}/market"))
1259 0 : return;
1260 :
1261 0 : if (queues.economicBuilding.hasQueuedUnitsWithClass("Market"))
1262 : {
1263 0 : if (!queues.economicBuilding.paused)
1264 : {
1265 : // Put available resources in this market
1266 0 : let queueManager = gameState.ai.queueManager;
1267 0 : let cost = queues.economicBuilding.plans[0].getCost();
1268 0 : queueManager.setAccounts(gameState, cost, "economicBuilding");
1269 0 : if (!queueManager.canAfford("economicBuilding", cost))
1270 : {
1271 0 : for (let q in queueManager.queues)
1272 : {
1273 0 : if (q == "economicBuilding")
1274 0 : continue;
1275 0 : queueManager.transferAccounts(cost, q, "economicBuilding");
1276 0 : if (queueManager.canAfford("economicBuilding", cost))
1277 0 : break;
1278 : }
1279 : }
1280 : }
1281 0 : return;
1282 : }
1283 :
1284 0 : gameState.ai.queueManager.changePriority("economicBuilding", 3 * this.Config.priorities.economicBuilding);
1285 0 : let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market");
1286 0 : plan.queueToReset = "economicBuilding";
1287 0 : queues.economicBuilding.addPlan(plan);
1288 : };
1289 :
1290 : /** Build a farmstead */
1291 0 : PETRA.HQ.prototype.buildFarmstead = function(gameState, queues)
1292 : {
1293 : // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs)
1294 0 : if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities())
1295 0 : return;
1296 : // Wait to have at least one dropsite and house before the farmstead
1297 0 : if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities())
1298 0 : return;
1299 0 : if (!gameState.getOwnEntitiesByClass("House", true).hasEntities())
1300 0 : return;
1301 0 : if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood"))
1302 0 : return;
1303 0 : if (!this.canBuild(gameState, "structures/{civ}/farmstead"))
1304 0 : return;
1305 :
1306 0 : queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/farmstead"));
1307 : };
1308 :
1309 : /**
1310 : * Try to build a wonder when required
1311 : * force = true when called from the victoryManager in case of Wonder victory condition.
1312 : */
1313 0 : PETRA.HQ.prototype.buildWonder = function(gameState, queues, force = false)
1314 : {
1315 0 : if (queues.wonder && queues.wonder.hasQueuedUnits() ||
1316 : gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() ||
1317 : !this.canBuild(gameState, "structures/{civ}/wonder"))
1318 0 : return;
1319 :
1320 0 : if (!force)
1321 : {
1322 0 : let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/wonder"));
1323 : // Check that we have enough resources to start thinking to build a wonder
1324 0 : let cost = template.cost();
1325 0 : let resources = gameState.getResources();
1326 0 : let highLevel = 0;
1327 0 : let lowLevel = 0;
1328 0 : for (let res in cost)
1329 : {
1330 0 : if (resources[res] && resources[res] > 0.7 * cost[res])
1331 0 : ++highLevel;
1332 0 : else if (!resources[res] || resources[res] < 0.3 * cost[res])
1333 0 : ++lowLevel;
1334 : }
1335 0 : if (highLevel == 0 || lowLevel > 1)
1336 0 : return;
1337 : }
1338 :
1339 0 : queues.wonder.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/wonder"));
1340 : };
1341 :
1342 : /** Build a corral, and train animals there */
1343 0 : PETRA.HQ.prototype.manageCorral = function(gameState, queues)
1344 : {
1345 0 : if (queues.corral.hasQueuedUnits())
1346 0 : return;
1347 :
1348 0 : let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length;
1349 0 : if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field")) &&
1350 : nCorral < this.currentPhase && gameState.getPopulation() > 30 * nCorral)
1351 : {
1352 0 : if (this.canBuild(gameState, "structures/{civ}/corral"))
1353 : {
1354 0 : queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral"));
1355 0 : return;
1356 : }
1357 0 : if (!nCorral)
1358 0 : return;
1359 : }
1360 :
1361 : // And train some animals
1362 0 : let civ = gameState.getPlayerCiv();
1363 0 : for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values())
1364 : {
1365 0 : if (corral.foundationProgress() !== undefined)
1366 0 : continue;
1367 0 : let trainables = corral.trainableEntities(civ);
1368 0 : for (let trainable of trainables)
1369 : {
1370 0 : if (gameState.isTemplateDisabled(trainable))
1371 0 : continue;
1372 0 : let template = gameState.getTemplate(trainable);
1373 0 : if (!template || !template.isHuntable())
1374 0 : continue;
1375 0 : let count = gameState.countEntitiesByType(trainable, true);
1376 0 : for (let item of corral.trainingQueue())
1377 0 : count += item.count;
1378 0 : if (count > nCorral)
1379 0 : continue;
1380 0 : queues.corral.addPlan(new PETRA.TrainingPlan(gameState, trainable, { "trainer": corral.id() }));
1381 0 : return;
1382 : }
1383 : }
1384 : };
1385 :
1386 : /**
1387 : * build more houses if needed.
1388 : * kinda ugly, lots of special cases to both build enough houses but not tooo many…
1389 : */
1390 0 : PETRA.HQ.prototype.buildMoreHouses = function(gameState, queues)
1391 : {
1392 0 : let houseTemplateString = "structures/{civ}/apartment";
1393 0 : if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)) ||
1394 : !this.canBuild(gameState, houseTemplateString))
1395 : {
1396 0 : houseTemplateString = "structures/{civ}/house";
1397 0 : if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)))
1398 0 : return;
1399 : }
1400 0 : if (gameState.getPopulationMax() <= gameState.getPopulationLimit())
1401 0 : return;
1402 :
1403 0 : let numPlanned = queues.house.length();
1404 0 : if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80)
1405 : {
1406 0 : let plan = new PETRA.ConstructionPlan(gameState, houseTemplateString);
1407 : // change the starting condition according to the situation.
1408 0 : plan.goRequirement = "houseNeeded";
1409 0 : queues.house.addPlan(plan);
1410 : }
1411 :
1412 0 : if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length)
1413 : {
1414 0 : let houseTemplateName = gameState.applyCiv(houseTemplateString);
1415 0 : let houseTemplate = gameState.getTemplate(houseTemplateName);
1416 :
1417 0 : let needed = 0;
1418 0 : for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing))
1419 : {
1420 0 : if (!houseTemplate.hasClass(entityReq.class))
1421 0 : continue;
1422 :
1423 0 : let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length;
1424 0 : if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName))
1425 : {
1426 0 : if (this.Config.debug > 1)
1427 0 : API3.warn("no room to place a house ... try to be less restrictive");
1428 0 : this.buildManager.setBuildable(houseTemplateName);
1429 0 : this.requireHouses = true;
1430 : }
1431 0 : needed = Math.max(needed, entityReq.count - count);
1432 : }
1433 :
1434 0 : let houseQueue = queues.house.plans;
1435 0 : for (let i = 0; i < numPlanned; ++i)
1436 0 : if (houseQueue[i].isGo(gameState))
1437 0 : --needed;
1438 0 : else if (needed > 0)
1439 : {
1440 0 : houseQueue[i].goRequirement = undefined;
1441 0 : --needed;
1442 : }
1443 : }
1444 :
1445 0 : if (this.requireHouses)
1446 : {
1447 0 : let houseTemplate = gameState.getTemplate(gameState.applyCiv(houseTemplateString));
1448 0 : if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req =>
1449 0 : !houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count))
1450 0 : this.requireHouses = undefined;
1451 : }
1452 :
1453 : // When population limit too tight
1454 : // - if no room to build, try to improve with technology
1455 : // - otherwise increase temporarily the priority of houses
1456 0 : let house = gameState.applyCiv(houseTemplateString);
1457 0 : let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length;
1458 0 : let popBonus = gameState.getTemplate(house).getPopulationBonus();
1459 0 : let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState);
1460 : let priority;
1461 0 : if (freeSlots < 5)
1462 : {
1463 0 : if (this.buildManager.isUnbuildable(gameState, house))
1464 : {
1465 0 : if (this.Config.debug > 1)
1466 0 : API3.warn("no room to place a house ... try to improve with technology");
1467 0 : this.researchManager.researchPopulationBonus(gameState, queues);
1468 : }
1469 : else
1470 0 : priority = 2 * this.Config.priorities.house;
1471 : }
1472 : else
1473 0 : priority = this.Config.priorities.house;
1474 :
1475 0 : if (priority && priority != gameState.ai.queueManager.getPriority("house"))
1476 0 : gameState.ai.queueManager.changePriority("house", priority);
1477 : };
1478 :
1479 : /** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */
1480 0 : PETRA.HQ.prototype.checkBaseExpansion = function(gameState, queues)
1481 : {
1482 0 : if (queues.civilCentre.hasQueuedUnits())
1483 0 : return;
1484 : // First build one cc if all have been destroyed
1485 0 : if (!this.hasPotentialBase())
1486 : {
1487 0 : this.buildFirstBase(gameState);
1488 0 : return;
1489 : }
1490 : // Then expand if we have not enough room available for buildings
1491 0 : if (this.buildManager.numberMissingRoom(gameState) > 1)
1492 : {
1493 0 : if (this.Config.debug > 2)
1494 0 : API3.warn("try to build a new base because not enough room to build ");
1495 0 : this.buildNewBase(gameState, queues);
1496 0 : return;
1497 : }
1498 : // If we've already planned to phase up, wait a bit before trying to expand
1499 0 : if (this.phasing)
1500 0 : return;
1501 : // Finally expand if we have lots of units (threshold depending on the aggressivity value)
1502 0 : let activeBases = this.numActiveBases();
1503 0 : let numUnits = gameState.getOwnUnits().length;
1504 0 : let numvar = 10 * (1 - this.Config.personality.aggressive);
1505 0 : if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50)
1506 : {
1507 0 : if (this.Config.debug > 2)
1508 0 : API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs");
1509 0 : this.buildNewBase(gameState, queues);
1510 : }
1511 : };
1512 :
1513 0 : PETRA.HQ.prototype.buildNewBase = function(gameState, queues, resource)
1514 : {
1515 0 : if (this.hasPotentialBase() && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))
1516 0 : return false;
1517 0 : if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits())
1518 0 : return false;
1519 :
1520 : let template;
1521 : // We require at least one of this civ civCentre as they may allow specific units or techs
1522 0 : let hasOwnCC = false;
1523 0 : for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values())
1524 : {
1525 0 : if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}/civil_centre"))
1526 0 : continue;
1527 0 : hasOwnCC = true;
1528 0 : break;
1529 : }
1530 0 : if (hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony"))
1531 0 : template = "structures/{civ}/military_colony";
1532 0 : else if (this.canBuild(gameState, "structures/{civ}/civil_centre"))
1533 0 : template = "structures/{civ}/civil_centre";
1534 0 : else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony"))
1535 0 : template = "structures/{civ}/military_colony";
1536 : else
1537 0 : return false;
1538 :
1539 : // base "-1" means new base.
1540 0 : if (this.Config.debug > 1)
1541 0 : API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource);
1542 0 : queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": -1, "resource": resource }));
1543 0 : return true;
1544 : };
1545 :
1546 : /** Deals with building fortresses and towers along our border with enemies. */
1547 0 : PETRA.HQ.prototype.buildDefenses = function(gameState, queues)
1548 : {
1549 0 : if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits())
1550 0 : return;
1551 :
1552 0 : if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3))))
1553 : {
1554 : // Try to build fortresses.
1555 0 : if (this.canBuild(gameState, "structures/{civ}/fortress"))
1556 : {
1557 0 : let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length;
1558 0 : if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10 * numFortresses) * this.fortressLapseTime + this.fortressStartTime) &&
1559 : numFortresses < this.numActiveBases() + 1 + this.extraFortresses &&
1560 : numFortresses < Math.floor(gameState.getPopulation() / 25) &&
1561 : gameState.getOwnFoundationsByClass("Fortress").length < 2)
1562 : {
1563 0 : this.fortressStartTime = gameState.ai.elapsedTime;
1564 0 : if (!numFortresses)
1565 0 : gameState.ai.queueManager.changePriority("defenseBuilding", 2 * this.Config.priorities.defenseBuilding);
1566 0 : let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/fortress");
1567 0 : plan.queueToReset = "defenseBuilding";
1568 0 : queues.defenseBuilding.addPlan(plan);
1569 0 : return;
1570 : }
1571 : }
1572 : }
1573 :
1574 0 : if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}/sentry_tower"))
1575 : {
1576 : // Count all towers + wall towers.
1577 0 : let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length + gameState.getOwnEntitiesByClass("WallTower", true).length;
1578 0 : let towerLapseTime = this.saveResource ? (1 + 0.5 * numTowers) * this.towerLapseTime : this.towerLapseTime;
1579 0 : if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime)
1580 : {
1581 0 : this.fortStartTime = gameState.ai.elapsedTime;
1582 0 : queues.defenseBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/sentry_tower"));
1583 : }
1584 0 : return;
1585 : }
1586 :
1587 0 : if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}/defense_tower"))
1588 0 : return;
1589 :
1590 0 : let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length;
1591 0 : let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime;
1592 0 : if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1 * numTowers) * towerLapseTime + this.towerStartTime) &&
1593 : numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers &&
1594 : numTowers < Math.floor(gameState.getPopulation() / 8) &&
1595 : gameState.getOwnFoundationsByClass("Tower").length < 3)
1596 : {
1597 0 : this.towerStartTime = gameState.ai.elapsedTime;
1598 0 : if (numTowers > 2 * this.numActiveBases() + 3)
1599 0 : gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7 * this.Config.priorities.defenseBuilding));
1600 0 : let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/defense_tower");
1601 0 : plan.queueToReset = "defenseBuilding";
1602 0 : queues.defenseBuilding.addPlan(plan);
1603 : }
1604 : };
1605 :
1606 0 : PETRA.HQ.prototype.buildForge = function(gameState, queues)
1607 : {
1608 0 : if (this.getAccountedPopulation(gameState) < this.Config.Military.popForForge ||
1609 : queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Forge", true).length)
1610 0 : return;
1611 : // Build a Market before the Forge.
1612 0 : if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities())
1613 0 : return;
1614 :
1615 0 : if (this.canBuild(gameState, "structures/{civ}/forge"))
1616 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge"));
1617 : };
1618 :
1619 : /**
1620 : * Deals with constructing military buildings (e.g. barracks, stable).
1621 : * They are mostly defined by Config.js. This is unreliable since changes could be done easily.
1622 : */
1623 0 : PETRA.HQ.prototype.constructTrainingBuildings = function(gameState, queues)
1624 : {
1625 0 : if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits())
1626 0 : return;
1627 :
1628 0 : let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length;
1629 0 : if (this.saveResources && numBarracks != 0)
1630 0 : return;
1631 :
1632 0 : let barracksTemplate = this.canBuild(gameState, "structures/{civ}/barracks") ? "structures/{civ}/barracks" : undefined;
1633 :
1634 0 : let rangeTemplate = this.canBuild(gameState, "structures/{civ}/range") ? "structures/{civ}/range" : undefined;
1635 0 : let numRanges = gameState.getOwnEntitiesByClass("Range", true).length;
1636 :
1637 0 : let stableTemplate = this.canBuild(gameState, "structures/{civ}/stable") ? "structures/{civ}/stable" : undefined;
1638 0 : let numStables = gameState.getOwnEntitiesByClass("Stable", true).length;
1639 :
1640 0 : if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 ||
1641 : this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5)
1642 : {
1643 : // First barracks/range and stable.
1644 0 : if (numBarracks + numRanges == 0)
1645 : {
1646 0 : let template = barracksTemplate || rangeTemplate;
1647 0 : if (template)
1648 : {
1649 0 : gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding);
1650 0 : let plan = new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true });
1651 0 : plan.queueToReset = "militaryBuilding";
1652 0 : queues.militaryBuilding.addPlan(plan);
1653 0 : return;
1654 : }
1655 : }
1656 0 : if (numStables == 0 && stableTemplate)
1657 : {
1658 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true }));
1659 0 : return;
1660 : }
1661 :
1662 : // Second barracks/range and stable.
1663 0 : if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2)
1664 : {
1665 0 : let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate);
1666 0 : if (template)
1667 : {
1668 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true }));
1669 0 : return;
1670 : }
1671 : }
1672 0 : if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2)
1673 : {
1674 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true }));
1675 0 : return;
1676 : }
1677 :
1678 : // Third barracks/range and stable, if needed.
1679 0 : if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30)
1680 : {
1681 0 : let template = barracksTemplate || stableTemplate || rangeTemplate;
1682 0 : if (template)
1683 : {
1684 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true }));
1685 0 : return;
1686 : }
1687 : }
1688 : }
1689 :
1690 0 : if (this.saveResources)
1691 0 : return;
1692 :
1693 0 : if (this.currentPhase < 3)
1694 0 : return;
1695 :
1696 0 : if (this.canBuild(gameState, "structures/{civ}/elephant_stable") && !gameState.getOwnEntitiesByClass("ElephantStable", true).hasEntities())
1697 : {
1698 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/elephant_stable", { "militaryBase": true }));
1699 0 : return;
1700 : }
1701 :
1702 0 : if (this.canBuild(gameState, "structures/{civ}/arsenal") && !gameState.getOwnEntitiesByClass("Arsenal", true).hasEntities())
1703 : {
1704 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/arsenal", { "militaryBase": true }));
1705 0 : return;
1706 : }
1707 :
1708 0 : if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length)
1709 0 : return;
1710 :
1711 : // Build advanced military buildings
1712 0 : let nAdvanced = 0;
1713 0 : for (let advanced of this.bAdvanced)
1714 0 : nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true);
1715 :
1716 0 : if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110)
1717 : {
1718 0 : for (let advanced of this.bAdvanced)
1719 : {
1720 0 : if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced))
1721 0 : continue;
1722 0 : let template = gameState.getTemplate(advanced);
1723 0 : if (!template)
1724 0 : continue;
1725 0 : let civ = gameState.getPlayerCiv();
1726 0 : if (template.hasDefensiveFire() || template.trainableEntities(civ))
1727 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced, { "militaryBase": true }));
1728 : else // not a military building, but still use this queue
1729 0 : queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced));
1730 0 : return;
1731 : }
1732 : }
1733 : };
1734 :
1735 : /**
1736 : * Find base nearest to ennemies for military buildings.
1737 : */
1738 0 : PETRA.HQ.prototype.findBestBaseForMilitary = function(gameState)
1739 : {
1740 0 : let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray();
1741 : let bestBase;
1742 0 : let enemyFound = false;
1743 0 : let distMin = Math.min();
1744 0 : for (let cce of ccEnts)
1745 : {
1746 0 : if (gameState.isPlayerAlly(cce.owner()))
1747 0 : continue;
1748 0 : if (enemyFound && !gameState.isPlayerEnemy(cce.owner()))
1749 0 : continue;
1750 0 : let access = PETRA.getLandAccess(gameState, cce);
1751 0 : let isEnemy = gameState.isPlayerEnemy(cce.owner());
1752 0 : for (let cc of ccEnts)
1753 : {
1754 0 : if (cc.owner() != PlayerID)
1755 0 : continue;
1756 0 : if (PETRA.getLandAccess(gameState, cc) != access)
1757 0 : continue;
1758 0 : let dist = API3.SquareVectorDistance(cc.position(), cce.position());
1759 0 : if (!enemyFound && isEnemy)
1760 0 : enemyFound = true;
1761 0 : else if (dist > distMin)
1762 0 : continue;
1763 0 : bestBase = cc.getMetadata(PlayerID, "base");
1764 0 : distMin = dist;
1765 : }
1766 : }
1767 0 : return bestBase;
1768 : };
1769 :
1770 : /**
1771 : * train with highest priority ranged infantry in the nearest civil center from a given set of positions
1772 : * and garrison them there for defense
1773 : */
1774 0 : PETRA.HQ.prototype.trainEmergencyUnits = function(gameState, positions)
1775 : {
1776 0 : if (gameState.ai.queues.emergency.hasQueuedUnits())
1777 0 : return false;
1778 :
1779 0 : let civ = gameState.getPlayerCiv();
1780 : // find nearest base anchor
1781 0 : let distcut = 20000;
1782 : let nearestAnchor;
1783 : let distmin;
1784 0 : for (let pos of positions)
1785 : {
1786 0 : let access = gameState.ai.accessibility.getAccessValue(pos);
1787 : // check nearest base anchor
1788 0 : for (const base of this.baseManagers())
1789 : {
1790 0 : if (!base.anchor || !base.anchor.position())
1791 0 : continue;
1792 0 : if (PETRA.getLandAccess(gameState, base.anchor) != access)
1793 0 : continue;
1794 0 : if (!base.anchor.trainableEntities(civ)) // base still in construction
1795 0 : continue;
1796 0 : let queue = base.anchor._entity.trainingQueue;
1797 0 : if (queue)
1798 : {
1799 0 : let time = 0;
1800 0 : for (let item of queue)
1801 0 : if (item.progress > 0 || item.metadata && item.metadata.garrisonType)
1802 0 : time += item.timeRemaining;
1803 0 : if (time/1000 > 5)
1804 0 : continue;
1805 : }
1806 0 : let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
1807 0 : if (nearestAnchor && dist > distmin)
1808 0 : continue;
1809 0 : distmin = dist;
1810 0 : nearestAnchor = base.anchor;
1811 : }
1812 : }
1813 0 : if (!nearestAnchor || distmin > distcut)
1814 0 : return false;
1815 :
1816 : // We will choose randomly ranged and melee units, except when garrisonHolder is full
1817 : // in which case we prefer melee units
1818 0 : let numGarrisoned = this.garrisonManager.numberOfGarrisonedSlots(nearestAnchor);
1819 0 : if (nearestAnchor._entity.trainingQueue)
1820 : {
1821 0 : for (let item of nearestAnchor._entity.trainingQueue)
1822 : {
1823 0 : if (item.metadata && item.metadata.garrisonType)
1824 0 : numGarrisoned += item.count;
1825 0 : else if (!item.progress && (!item.metadata || !item.metadata.trainer))
1826 0 : nearestAnchor.stopProduction(item.id);
1827 : }
1828 : }
1829 0 : let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() &&
1830 : nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints();
1831 0 : let rangedWanted = randBool() && autogarrison;
1832 :
1833 0 : let total = gameState.getResources();
1834 : let templateFound;
1835 0 : let trainables = nearestAnchor.trainableEntities(civ);
1836 0 : let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses();
1837 0 : for (let trainable of trainables)
1838 : {
1839 0 : if (gameState.isTemplateDisabled(trainable))
1840 0 : continue;
1841 0 : let template = gameState.getTemplate(trainable);
1842 0 : if (!template || !template.hasClasses(["Infantry+CitizenSoldier"]))
1843 0 : continue;
1844 0 : if (autogarrison && !template.hasClasses(garrisonArrowClasses))
1845 0 : continue;
1846 0 : if (!total.canAfford(new API3.Resources(template.cost())))
1847 0 : continue;
1848 0 : templateFound = [trainable, template];
1849 0 : if (template.hasClass("Ranged") == rangedWanted)
1850 0 : break;
1851 : }
1852 0 : if (!templateFound)
1853 0 : return false;
1854 :
1855 : // Check first if we can afford it without touching the other accounts
1856 : // and if not, take some of other accounted resources
1857 : // TODO sort the queues to be substracted
1858 0 : let queueManager = gameState.ai.queueManager;
1859 0 : let cost = new API3.Resources(templateFound[1].cost());
1860 0 : queueManager.setAccounts(gameState, cost, "emergency");
1861 0 : if (!queueManager.canAfford("emergency", cost))
1862 : {
1863 0 : for (let q in queueManager.queues)
1864 : {
1865 0 : if (q == "emergency")
1866 0 : continue;
1867 0 : queueManager.transferAccounts(cost, q, "emergency");
1868 0 : if (queueManager.canAfford("emergency", cost))
1869 0 : break;
1870 : }
1871 : }
1872 0 : const metadata = { "role": PETRA.Worker.ROLE_WORKER, "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() };
1873 0 : if (autogarrison)
1874 0 : metadata.garrisonType = PETRA.GarrisonManager.TYPE_PROTECTION;
1875 0 : gameState.ai.queues.emergency.addPlan(new PETRA.TrainingPlan(gameState, templateFound[0], metadata, 1, 1));
1876 0 : return true;
1877 : };
1878 :
1879 0 : PETRA.HQ.prototype.canBuild = function(gameState, structure)
1880 : {
1881 0 : let type = gameState.applyCiv(structure);
1882 0 : if (this.buildManager.isUnbuildable(gameState, type))
1883 0 : return false;
1884 :
1885 0 : if (gameState.isTemplateDisabled(type))
1886 : {
1887 0 : this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled");
1888 0 : return false;
1889 : }
1890 :
1891 0 : let template = gameState.getTemplate(type);
1892 0 : if (!template)
1893 : {
1894 0 : this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate");
1895 0 : return false;
1896 : }
1897 :
1898 0 : if (!template.available(gameState))
1899 : {
1900 0 : this.buildManager.setUnbuildable(gameState, type, 30, "requirements");
1901 0 : return false;
1902 : }
1903 :
1904 0 : if (!this.buildManager.hasBuilder(type))
1905 : {
1906 0 : this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder");
1907 0 : return false;
1908 : }
1909 :
1910 0 : if (!this.hasActiveBase())
1911 : {
1912 : // if no base, check that we can build outside our territory
1913 0 : let buildTerritories = template.buildTerritories();
1914 0 : if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own"))
1915 : {
1916 0 : this.buildManager.setUnbuildable(gameState, type, 180, "room");
1917 0 : return false;
1918 : }
1919 : }
1920 :
1921 : // build limits
1922 0 : let limits = gameState.getEntityLimits();
1923 0 : let category = template.buildCategory();
1924 0 : if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category])
1925 : {
1926 0 : this.buildManager.setUnbuildable(gameState, type, 90, "limit");
1927 0 : return false;
1928 : }
1929 :
1930 0 : return true;
1931 : };
1932 :
1933 0 : PETRA.HQ.prototype.updateTerritories = function(gameState)
1934 : {
1935 0 : const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ];
1936 0 : let alliedVictory = gameState.getAlliedVictory();
1937 0 : let passabilityMap = gameState.getPassabilityMap();
1938 0 : let width = this.territoryMap.width;
1939 0 : let cellSize = this.territoryMap.cellSize;
1940 0 : let insideSmall = Math.round(45 / cellSize);
1941 0 : let insideLarge = Math.round(80 / cellSize); // should be about the range of towers
1942 0 : let expansion = 0;
1943 :
1944 0 : for (let j = 0; j < this.territoryMap.length; ++j)
1945 : {
1946 0 : if (this.borderMap.map[j] & PETRA.outside_Mask)
1947 0 : continue;
1948 0 : if (this.borderMap.map[j] & PETRA.fullFrontier_Mask)
1949 0 : this.borderMap.map[j] &= ~PETRA.fullFrontier_Mask; // reset the frontier
1950 :
1951 0 : if (this.territoryMap.getOwnerIndex(j) != PlayerID)
1952 0 : this.basesManager.removeBaseFromTerritoryIndex(j);
1953 : else
1954 : {
1955 : // Update the frontier
1956 0 : let ix = j%width;
1957 0 : let iz = Math.floor(j/width);
1958 0 : let onFrontier = false;
1959 0 : for (let a of around)
1960 : {
1961 0 : let jx = ix + Math.round(insideSmall*a[0]);
1962 0 : if (jx < 0 || jx >= width)
1963 0 : continue;
1964 0 : let jz = iz + Math.round(insideSmall*a[1]);
1965 0 : if (jz < 0 || jz >= width)
1966 0 : continue;
1967 0 : if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask)
1968 0 : continue;
1969 0 : let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz);
1970 0 : if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner)))
1971 : {
1972 0 : this.borderMap.map[j] |= PETRA.narrowFrontier_Mask;
1973 0 : break;
1974 : }
1975 0 : jx = ix + Math.round(insideLarge*a[0]);
1976 0 : if (jx < 0 || jx >= width)
1977 0 : continue;
1978 0 : jz = iz + Math.round(insideLarge*a[1]);
1979 0 : if (jz < 0 || jz >= width)
1980 0 : continue;
1981 0 : if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask)
1982 0 : continue;
1983 0 : territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz);
1984 0 : if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner)))
1985 0 : onFrontier = true;
1986 : }
1987 0 : if (onFrontier && !(this.borderMap.map[j] & PETRA.narrowFrontier_Mask))
1988 0 : this.borderMap.map[j] |= PETRA.largeFrontier_Mask;
1989 :
1990 0 : if (this.basesManager.addTerritoryIndexToBase(gameState, j, passabilityMap))
1991 0 : expansion++;
1992 : }
1993 : }
1994 :
1995 0 : if (!expansion)
1996 0 : return;
1997 : // We've increased our territory, so we may have some new room to build
1998 0 : this.buildManager.resetMissingRoom(gameState);
1999 : // And if sufficient expansion, check if building a new market would improve our present trade routes
2000 0 : let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize;
2001 0 : if (expansion * cellArea > 960)
2002 0 : this.tradeManager.routeProspection = true;
2003 : };
2004 :
2005 : /**
2006 : * returns the base corresponding to baseID
2007 : */
2008 0 : PETRA.HQ.prototype.getBaseByID = function(baseID)
2009 : {
2010 0 : return this.basesManager.getBaseByID(baseID);
2011 : };
2012 :
2013 : /**
2014 : * returns the number of bases with a cc
2015 : * ActiveBases includes only those with a built cc
2016 : * PotentialBases includes also those with a cc in construction
2017 : */
2018 0 : PETRA.HQ.prototype.numActiveBases = function()
2019 : {
2020 0 : return this.basesManager.numActiveBases();
2021 : };
2022 :
2023 0 : PETRA.HQ.prototype.hasActiveBase = function()
2024 : {
2025 0 : return this.basesManager.hasActiveBase();
2026 : };
2027 :
2028 0 : PETRA.HQ.prototype.numPotentialBases = function()
2029 : {
2030 0 : return this.basesManager.numPotentialBases();
2031 : };
2032 :
2033 0 : PETRA.HQ.prototype.hasPotentialBase = function()
2034 : {
2035 0 : return this.basesManager.hasPotentialBase();
2036 : };
2037 :
2038 0 : PETRA.HQ.prototype.isDangerousLocation = function(gameState, pos, radius)
2039 : {
2040 0 : return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius);
2041 : };
2042 :
2043 : /** Check that the chosen position is not too near from an invading army */
2044 0 : PETRA.HQ.prototype.isNearInvadingArmy = function(pos)
2045 : {
2046 0 : for (let army of this.defenseManager.armies)
2047 0 : if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000)
2048 0 : return true;
2049 0 : return false;
2050 : };
2051 :
2052 0 : PETRA.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0)
2053 : {
2054 0 : if (!this.turnCache.firingStructures)
2055 0 : this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures());
2056 0 : for (let ent of this.turnCache.firingStructures.values())
2057 : {
2058 0 : let range = radius + ent.attackRange("Ranged").max;
2059 0 : if (API3.SquareVectorDistance(ent.position(), pos) < range*range)
2060 0 : return true;
2061 : }
2062 0 : return false;
2063 : };
2064 :
2065 : /** Compute the capture strength of all units attacking a capturable target */
2066 0 : PETRA.HQ.prototype.updateCaptureStrength = function(gameState)
2067 : {
2068 0 : this.capturableTargets.clear();
2069 0 : for (let ent of gameState.getOwnUnits().values())
2070 : {
2071 0 : if (!ent.canCapture())
2072 0 : continue;
2073 0 : let state = ent.unitAIState();
2074 0 : if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT")
2075 0 : continue;
2076 0 : let orderData = ent.unitAIOrderData();
2077 0 : if (!orderData || !orderData.length || !orderData[0].target)
2078 0 : continue;
2079 0 : let targetId = orderData[0].target;
2080 0 : let target = gameState.getEntityById(targetId);
2081 0 : if (!target || !target.isCapturable() || !ent.canCapture(target))
2082 0 : continue;
2083 0 : if (!this.capturableTargets.has(targetId))
2084 0 : this.capturableTargets.set(targetId, {
2085 : "strength": ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"),
2086 : "ents": new Set([ent.id()])
2087 : });
2088 : else
2089 : {
2090 0 : let capturableTarget = this.capturableTargets.get(target.id());
2091 0 : capturableTarget.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
2092 0 : capturableTarget.ents.add(ent.id());
2093 : }
2094 : }
2095 :
2096 0 : for (let [targetId, capturableTarget] of this.capturableTargets)
2097 : {
2098 0 : let target = gameState.getEntityById(targetId);
2099 : let allowCapture;
2100 0 : for (let entId of capturableTarget.ents)
2101 : {
2102 0 : let ent = gameState.getEntityById(entId);
2103 0 : if (allowCapture === undefined)
2104 0 : allowCapture = PETRA.allowCapture(gameState, ent, target);
2105 0 : let orderData = ent.unitAIOrderData();
2106 0 : if (!orderData || !orderData.length || !orderData[0].attackType)
2107 0 : continue;
2108 0 : if ((orderData[0].attackType == "Capture") !== allowCapture)
2109 0 : ent.attack(targetId, allowCapture);
2110 : }
2111 : }
2112 :
2113 0 : this.capturableTargetsTime = gameState.ai.elapsedTime;
2114 : };
2115 :
2116 : /**
2117 : * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around)
2118 : */
2119 0 : PETRA.HQ.prototype.isDefendable = function(ent)
2120 : {
2121 0 : if (!this.turnCache.numAround)
2122 0 : this.turnCache.numAround = {};
2123 0 : if (this.turnCache.numAround[ent.id()] === undefined)
2124 0 : this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130);
2125 0 : return +this.turnCache.numAround[ent.id()] > 8;
2126 : };
2127 :
2128 : /**
2129 : * Get the number of population already accounted for
2130 : */
2131 0 : PETRA.HQ.prototype.getAccountedPopulation = function(gameState)
2132 : {
2133 0 : if (this.turnCache.accountedPopulation == undefined)
2134 : {
2135 0 : let pop = gameState.getPopulation();
2136 0 : for (let ent of gameState.getOwnTrainingFacilities().values())
2137 : {
2138 0 : for (let item of ent.trainingQueue())
2139 : {
2140 0 : if (!item.unitTemplate)
2141 0 : continue;
2142 0 : let unitPop = gameState.getTemplate(item.unitTemplate).get("Cost/Population");
2143 0 : if (unitPop)
2144 0 : pop += item.count * unitPop;
2145 : }
2146 : }
2147 0 : this.turnCache.accountedPopulation = pop;
2148 : }
2149 0 : return this.turnCache.accountedPopulation;
2150 : };
2151 :
2152 : /**
2153 : * Get the number of workers already accounted for
2154 : */
2155 0 : PETRA.HQ.prototype.getAccountedWorkers = function(gameState)
2156 : {
2157 0 : if (this.turnCache.accountedWorkers == undefined)
2158 : {
2159 0 : let workers = gameState.getOwnEntitiesByRole(PETRA.Worker.ROLE_WORKER, true).length;
2160 0 : for (let ent of gameState.getOwnTrainingFacilities().values())
2161 : {
2162 0 : for (let item of ent.trainingQueue())
2163 : {
2164 0 : if (!item.metadata || !item.metadata.role || item.metadata.role !== PETRA.Worker.ROLE_WORKER)
2165 0 : continue;
2166 0 : workers += item.count;
2167 : }
2168 : }
2169 0 : this.turnCache.accountedWorkers = workers;
2170 : }
2171 0 : return this.turnCache.accountedWorkers;
2172 : };
2173 :
2174 0 : PETRA.HQ.prototype.baseManagers = function()
2175 : {
2176 0 : return this.basesManager.baseManagers;
2177 : };
2178 :
2179 : /**
2180 : * @param {number} territoryIndex - The index to get the map for.
2181 : * @return {number} - The ID of the base at the given territory index.
2182 : */
2183 0 : PETRA.HQ.prototype.baseAtIndex = function(territoryIndex)
2184 : {
2185 0 : return this.basesManager.baseAtIndex(territoryIndex);
2186 : };
2187 :
2188 : /**
2189 : * Some functions are run every turn
2190 : * Others once in a while
2191 : */
2192 0 : PETRA.HQ.prototype.update = function(gameState, queues, events)
2193 : {
2194 0 : Engine.ProfileStart("Headquarters update");
2195 0 : this.emergencyManager.update(gameState);
2196 0 : this.turnCache = {};
2197 0 : this.territoryMap = PETRA.createTerritoryMap(gameState);
2198 0 : this.canBarter = gameState.getOwnEntitiesByClass("Market", true).filter(API3.Filters.isBuilt()).hasEntities();
2199 : // TODO find a better way to update
2200 0 : if (this.currentPhase != gameState.currentPhase())
2201 : {
2202 0 : if (this.Config.debug > 0)
2203 0 : API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase +
2204 : " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime +
2205 : " phasing " + this.phasing);
2206 0 : this.currentPhase = gameState.currentPhase();
2207 :
2208 : // In principle, this.phasing should be already reset to 0 when starting the research
2209 : // but this does not work in case of an autoResearch tech
2210 0 : if (this.phasing)
2211 0 : this.phasing = 0;
2212 : }
2213 :
2214 : /*
2215 : if (this.Config.debug > 1)
2216 : {
2217 : gameState.getOwnUnits().forEach (function (ent) {
2218 : if (!ent.position())
2219 : return;
2220 : PETRA.dumpEntity(ent);
2221 : });
2222 : }
2223 : */
2224 :
2225 0 : this.checkEvents(gameState, events);
2226 0 : this.navalManager.checkEvents(gameState, queues, events);
2227 :
2228 0 : if (this.phasing)
2229 0 : this.checkPhaseRequirements(gameState, queues);
2230 : else
2231 0 : this.researchManager.checkPhase(gameState, queues);
2232 :
2233 0 : if (this.hasActiveBase())
2234 : {
2235 0 : if (gameState.ai.playedTurn % 4 == 0)
2236 0 : this.trainMoreWorkers(gameState, queues);
2237 :
2238 0 : if (gameState.ai.playedTurn % 4 == 1)
2239 0 : this.buildMoreHouses(gameState, queues);
2240 :
2241 0 : if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2)
2242 0 : this.buildFarmstead(gameState, queues);
2243 :
2244 0 : if (this.needCorral && gameState.ai.playedTurn % 4 == 3)
2245 0 : this.manageCorral(gameState, queues);
2246 :
2247 0 : if (gameState.ai.playedTurn % 5 == 1)
2248 0 : this.researchManager.update(gameState, queues);
2249 : }
2250 :
2251 0 : if (!this.hasPotentialBase() ||
2252 : this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1)
2253 0 : this.checkBaseExpansion(gameState, queues);
2254 :
2255 0 : if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0)
2256 : {
2257 0 : if (!this.canBarter)
2258 0 : this.buildMarket(gameState, queues);
2259 :
2260 0 : if (!this.saveResources)
2261 : {
2262 0 : this.buildForge(gameState, queues);
2263 0 : this.buildTemple(gameState, queues);
2264 : }
2265 :
2266 0 : if (gameState.ai.playedTurn % 30 == 0 &&
2267 : gameState.getPopulation() > 0.9 * gameState.getPopulationMax())
2268 0 : this.buildWonder(gameState, queues, false);
2269 : }
2270 :
2271 0 : this.tradeManager.update(gameState, events, queues);
2272 :
2273 0 : this.garrisonManager.update(gameState, events);
2274 0 : this.defenseManager.update(gameState, events);
2275 :
2276 0 : if (gameState.ai.playedTurn % 3 == 0)
2277 : {
2278 0 : this.constructTrainingBuildings(gameState, queues);
2279 0 : if (this.Config.difficulty > PETRA.DIFFICULTY_SANDBOX)
2280 0 : this.buildDefenses(gameState, queues);
2281 : }
2282 :
2283 0 : this.basesManager.update(gameState, queues, events);
2284 :
2285 0 : this.navalManager.update(gameState, queues, events);
2286 :
2287 0 : if (this.Config.difficulty > PETRA.DIFFICULTY_SANDBOX && (this.hasActiveBase() || !this.canBuildUnits))
2288 0 : this.attackManager.update(gameState, queues, events);
2289 :
2290 0 : this.diplomacyManager.update(gameState, events);
2291 :
2292 0 : this.victoryManager.update(gameState, events, queues);
2293 :
2294 : // We update the capture strength at the end as it can change attack orders
2295 0 : if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3)
2296 0 : this.updateCaptureStrength(gameState);
2297 :
2298 0 : Engine.ProfileStop();
2299 : };
2300 :
2301 0 : PETRA.HQ.prototype.Serialize = function()
2302 : {
2303 0 : let properties = {
2304 : "phasing": this.phasing,
2305 : "lastFailedGather": this.lastFailedGather,
2306 : "firstBaseConfig": this.firstBaseConfig,
2307 : "supportRatio": this.supportRatio,
2308 : "targetNumWorkers": this.targetNumWorkers,
2309 : "fortStartTime": this.fortStartTime,
2310 : "towerStartTime": this.towerStartTime,
2311 : "fortressStartTime": this.fortressStartTime,
2312 : "bAdvanced": this.bAdvanced,
2313 : "saveResources": this.saveResources,
2314 : "saveSpace": this.saveSpace,
2315 : "needCorral": this.needCorral,
2316 : "needFarm": this.needFarm,
2317 : "needFish": this.needFish,
2318 : "maxFields": this.maxFields,
2319 : "canExpand": this.canExpand,
2320 : "canBuildUnits": this.canBuildUnits,
2321 : "navalMap": this.navalMap,
2322 : "landRegions": this.landRegions,
2323 : "navalRegions": this.navalRegions,
2324 : "decayingStructures": this.decayingStructures,
2325 : "capturableTargets": this.capturableTargets,
2326 : "capturableTargetsTime": this.capturableTargetsTime
2327 : };
2328 :
2329 0 : if (this.Config.debug == -100)
2330 : {
2331 0 : API3.warn(" HQ serialization ---------------------");
2332 0 : API3.warn(" properties " + uneval(properties));
2333 0 : API3.warn(" basesManager " + uneval(this.basesManager.Serialize()));
2334 0 : API3.warn(" attackManager " + uneval(this.attackManager.Serialize()));
2335 0 : API3.warn(" buildManager " + uneval(this.buildManager.Serialize()));
2336 0 : API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize()));
2337 0 : API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize()));
2338 0 : API3.warn(" navalManager " + uneval(this.navalManager.Serialize()));
2339 0 : API3.warn(" researchManager " + uneval(this.researchManager.Serialize()));
2340 0 : API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize()));
2341 0 : API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize()));
2342 0 : API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize()));
2343 0 : API3.warn(" emergencyManager " + uneval(this.emergencyManager.Serialize()));
2344 : }
2345 :
2346 0 : return {
2347 : "properties": properties,
2348 :
2349 : "basesManager": this.basesManager.Serialize(),
2350 : "attackManager": this.attackManager.Serialize(),
2351 : "buildManager": this.buildManager.Serialize(),
2352 : "defenseManager": this.defenseManager.Serialize(),
2353 : "tradeManager": this.tradeManager.Serialize(),
2354 : "navalManager": this.navalManager.Serialize(),
2355 : "researchManager": this.researchManager.Serialize(),
2356 : "diplomacyManager": this.diplomacyManager.Serialize(),
2357 : "garrisonManager": this.garrisonManager.Serialize(),
2358 : "victoryManager": this.victoryManager.Serialize(),
2359 : "emergencyManager": this.emergencyManager.Serialize(),
2360 : };
2361 : };
2362 :
2363 0 : PETRA.HQ.prototype.Deserialize = function(gameState, data)
2364 : {
2365 0 : for (let key in data.properties)
2366 0 : this[key] = data.properties[key];
2367 :
2368 :
2369 0 : this.basesManager = new PETRA.BasesManager(this.Config);
2370 0 : this.basesManager.init(gameState);
2371 0 : this.basesManager.Deserialize(gameState, data.basesManager);
2372 :
2373 0 : this.navalManager = new PETRA.NavalManager(this.Config);
2374 0 : this.navalManager.init(gameState, true);
2375 0 : this.navalManager.Deserialize(gameState, data.navalManager);
2376 :
2377 0 : this.attackManager = new PETRA.AttackManager(this.Config);
2378 0 : this.attackManager.Deserialize(gameState, data.attackManager);
2379 0 : this.attackManager.init(gameState);
2380 0 : this.attackManager.Deserialize(gameState, data.attackManager);
2381 :
2382 0 : this.buildManager = new PETRA.BuildManager();
2383 0 : this.buildManager.Deserialize(data.buildManager);
2384 :
2385 0 : this.defenseManager = new PETRA.DefenseManager(this.Config);
2386 0 : this.defenseManager.Deserialize(gameState, data.defenseManager);
2387 :
2388 0 : this.tradeManager = new PETRA.TradeManager(this.Config);
2389 0 : this.tradeManager.init(gameState);
2390 0 : this.tradeManager.Deserialize(gameState, data.tradeManager);
2391 :
2392 0 : this.researchManager = new PETRA.ResearchManager(this.Config);
2393 0 : this.researchManager.Deserialize(data.researchManager);
2394 :
2395 0 : this.diplomacyManager = new PETRA.DiplomacyManager(this.Config);
2396 0 : this.diplomacyManager.Deserialize(data.diplomacyManager);
2397 :
2398 0 : this.garrisonManager = new PETRA.GarrisonManager(this.Config);
2399 0 : this.garrisonManager.Deserialize(data.garrisonManager);
2400 :
2401 0 : this.victoryManager = new PETRA.VictoryManager(this.Config);
2402 0 : this.victoryManager.Deserialize(data.victoryManager);
2403 :
2404 0 : this.emergencyManager = new PETRA.EmergencyManager(this.Config);
2405 0 : this.emergencyManager.Deserialize(data.emergencyManager);
2406 : };
|