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