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