Line data Source code
1 : /** returns true if this unit should be considered as a siege unit */
2 0 : PETRA.isSiegeUnit = function(ent)
3 : {
4 0 : return ent.hasClasses(["Siege", "Elephant+Melee"]);
5 : };
6 :
7 : /** returns true if this unit should be considered as "fast". */
8 0 : PETRA.isFastMoving = function(ent)
9 : {
10 : // TODO: use clever logic based on walkspeed comparisons.
11 0 : return ent.hasClass("FastMoving");
12 : };
13 :
14 : /** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */
15 0 : PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClass)
16 : {
17 0 : let strength = 0;
18 0 : let attackTypes = ent.attackTypes();
19 0 : let damageTypes = Object.keys(DamageTypeImportance);
20 0 : if (!attackTypes)
21 0 : return strength;
22 :
23 0 : for (let type of attackTypes)
24 : {
25 0 : if (type == "Slaughter")
26 0 : continue;
27 :
28 0 : let attackStrength = ent.attackStrengths(type);
29 0 : for (let str in attackStrength)
30 : {
31 0 : let val = parseFloat(attackStrength[str]);
32 0 : if (againstClass)
33 0 : val *= ent.getMultiplierAgainst(type, againstClass);
34 0 : if (DamageTypeImportance[str])
35 0 : strength += DamageTypeImportance[str] * val / damageTypes.length;
36 0 : else if (debugLevel > 0)
37 0 : API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength (please add " + str + " to config.js).");
38 : }
39 :
40 0 : let attackRange = ent.attackRange(type);
41 0 : if (attackRange)
42 0 : strength += attackRange.max * 0.0125;
43 :
44 0 : let attackTimes = ent.attackTimes(type);
45 0 : for (let str in attackTimes)
46 : {
47 0 : let val = parseFloat(attackTimes[str]);
48 0 : switch (str)
49 : {
50 : case "repeat":
51 0 : strength += val / 100000;
52 0 : break;
53 : case "prepare":
54 0 : strength -= val / 100000;
55 0 : break;
56 : default:
57 0 : API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength");
58 : }
59 : }
60 : }
61 :
62 0 : let resistanceStrength = ent.resistanceStrengths();
63 :
64 0 : if (resistanceStrength.Damage)
65 0 : for (let str in resistanceStrength.Damage)
66 : {
67 0 : let val = +resistanceStrength.Damage[str];
68 0 : if (DamageTypeImportance[str])
69 0 : strength += DamageTypeImportance[str] * val / damageTypes.length;
70 0 : else if (debugLevel > 0)
71 0 : API3.warn("Petra: " + str + " unknown resistanceStrength in getMaxStrength (please add " + str + " to config.js).");
72 : }
73 :
74 : // ToDo: Add support for StatusEffects and Capture.
75 :
76 0 : return strength * ent.maxHitpoints() / 100.0;
77 : };
78 :
79 : /** Get access and cache it (except for units as it can change) in metadata if not already done */
80 0 : PETRA.getLandAccess = function(gameState, ent)
81 : {
82 0 : if (ent.hasClass("Unit"))
83 : {
84 0 : let pos = ent.position();
85 0 : if (!pos)
86 : {
87 0 : let holder = PETRA.getHolder(gameState, ent);
88 0 : if (holder)
89 0 : return PETRA.getLandAccess(gameState, holder);
90 :
91 0 : API3.warn("Petra error: entity without position, but not garrisoned");
92 0 : PETRA.dumpEntity(ent);
93 0 : return undefined;
94 : }
95 0 : return gameState.ai.accessibility.getAccessValue(pos);
96 : }
97 :
98 0 : let access = ent.getMetadata(PlayerID, "access");
99 0 : if (!access)
100 : {
101 0 : access = gameState.ai.accessibility.getAccessValue(ent.position());
102 : // Docks are sometimes not as expected
103 0 : if (access < 2 && ent.buildPlacementType() == "shore")
104 : {
105 0 : let halfDepth = 0;
106 0 : if (ent.get("Footprint/Square"))
107 0 : halfDepth = +ent.get("Footprint/Square/@depth") / 2;
108 0 : else if (ent.get("Footprint/Circle"))
109 0 : halfDepth = +ent.get("Footprint/Circle/@radius");
110 0 : let entPos = ent.position();
111 0 : let cosa = Math.cos(ent.angle());
112 0 : let sina = Math.sin(ent.angle());
113 0 : for (let d = 3; d < halfDepth; d += 3)
114 : {
115 0 : let pos = [ entPos[0] - d * sina,
116 : entPos[1] - d * cosa];
117 0 : access = gameState.ai.accessibility.getAccessValue(pos);
118 0 : if (access > 1)
119 0 : break;
120 : }
121 : }
122 0 : ent.setMetadata(PlayerID, "access", access);
123 : }
124 0 : return access;
125 : };
126 :
127 : /** Sea access always cached as it never changes */
128 0 : PETRA.getSeaAccess = function(gameState, ent)
129 : {
130 0 : let sea = ent.getMetadata(PlayerID, "sea");
131 0 : if (!sea)
132 : {
133 0 : sea = gameState.ai.accessibility.getAccessValue(ent.position(), true);
134 : // Docks are sometimes not as expected
135 0 : if (sea < 2 && ent.buildPlacementType() == "shore")
136 : {
137 0 : let entPos = ent.position();
138 0 : let cosa = Math.cos(ent.angle());
139 0 : let sina = Math.sin(ent.angle());
140 0 : for (let d = 3; d < 15; d += 3)
141 : {
142 0 : let pos = [ entPos[0] + d * sina,
143 : entPos[1] + d * cosa];
144 0 : sea = gameState.ai.accessibility.getAccessValue(pos, true);
145 0 : if (sea > 1)
146 0 : break;
147 : }
148 : }
149 0 : ent.setMetadata(PlayerID, "sea", sea);
150 : }
151 0 : return sea;
152 : };
153 :
154 0 : PETRA.setSeaAccess = function(gameState, ent)
155 : {
156 0 : PETRA.getSeaAccess(gameState, ent);
157 : };
158 :
159 : /** Decide if we should try to capture (returns true) or destroy (return false) */
160 0 : PETRA.allowCapture = function(gameState, ent, target)
161 : {
162 0 : if (!target.isCapturable() || !ent.canCapture(target))
163 0 : return false;
164 0 : if (target.isInvulnerable())
165 0 : return true;
166 : // always try to recapture capture points from an allied, except if it's decaying
167 0 : if (gameState.isPlayerAlly(target.owner()))
168 0 : return !target.decaying();
169 :
170 0 : let antiCapture = target.defaultRegenRate();
171 0 : if (target.isGarrisonHolder())
172 : {
173 0 : const garrisonRegenRate = target.garrisonRegenRate();
174 0 : for (const garrisonedEntity of target.garrisoned())
175 0 : antiCapture += garrisonRegenRate * (gameState.getEntityById(garrisonedEntity)?.captureStrength() || 0);
176 : }
177 :
178 0 : if (target.decaying())
179 0 : antiCapture -= target.territoryDecayRate();
180 :
181 : let capture;
182 0 : let capturableTargets = gameState.ai.HQ.capturableTargets;
183 0 : if (!capturableTargets.has(target.id()))
184 : {
185 0 : capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
186 0 : capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) });
187 : }
188 : else
189 : {
190 0 : let capturable = capturableTargets.get(target.id());
191 0 : if (!capturable.ents.has(ent.id()))
192 : {
193 0 : capturable.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
194 0 : capturable.ents.add(ent.id());
195 : }
196 0 : capture = capturable.strength;
197 : }
198 0 : capture *= 1 / (0.1 + 0.9*target.healthLevel());
199 0 : let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b);
200 0 : if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned())
201 0 : return capture > antiCapture + sumCapturePoints/50;
202 0 : return capture > antiCapture + sumCapturePoints/80;
203 : };
204 :
205 0 : PETRA.getAttackBonus = function(ent, target, type)
206 : {
207 0 : let attackBonus = 1;
208 0 : if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses"))
209 0 : return attackBonus;
210 0 : let bonuses = ent.get("Attack/" + type + "/Bonuses");
211 0 : for (let key in bonuses)
212 : {
213 0 : let bonus = bonuses[key];
214 0 : if (bonus.Civ && bonus.Civ !== target.civ())
215 0 : continue;
216 0 : if (!bonus.Classes || target.hasClasses(bonus.Classes))
217 0 : attackBonus *= bonus.Multiplier;
218 : }
219 0 : return attackBonus;
220 : };
221 :
222 : /** Makes the worker deposit the currently carried resources at the closest accessible dropsite */
223 0 : PETRA.returnResources = function(gameState, ent)
224 : {
225 0 : if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position())
226 0 : return false;
227 :
228 0 : let resource = ent.resourceCarrying()[0].type;
229 :
230 : let closestDropsite;
231 0 : let distmin = Math.min();
232 0 : let access = PETRA.getLandAccess(gameState, ent);
233 0 : let dropsiteCollection = gameState.playerData.hasSharedDropsites ?
234 : gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource);
235 0 : for (let dropsite of dropsiteCollection.values())
236 : {
237 0 : if (!dropsite.position())
238 0 : continue;
239 0 : let owner = dropsite.owner();
240 : // owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again
241 0 : if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
242 0 : continue;
243 0 : if (PETRA.getLandAccess(gameState, dropsite) != access)
244 0 : continue;
245 0 : let dist = API3.SquareVectorDistance(ent.position(), dropsite.position());
246 0 : if (dist > distmin)
247 0 : continue;
248 0 : distmin = dist;
249 0 : closestDropsite = dropsite;
250 : }
251 :
252 0 : if (!closestDropsite)
253 0 : return false;
254 0 : ent.returnResources(closestDropsite);
255 0 : return true;
256 : };
257 :
258 : /** is supply full taking into account gatherers affected during this turn */
259 0 : PETRA.IsSupplyFull = function(gameState, ent)
260 : {
261 0 : return ent.isFull() === true ||
262 : ent.resourceSupplyNumGatherers() + gameState.ai.HQ.basesManager.GetTCGatherer(ent.id()) >= ent.maxGatherers();
263 : };
264 :
265 : /**
266 : * Get the best base (in terms of distance and accessIndex) for an entity.
267 : * It should be on the same accessIndex for structures.
268 : * If nothing found, return the noBase for units and undefined for structures.
269 : * If exclude is given, we exclude the base with ID = exclude.
270 : */
271 0 : PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false)
272 : {
273 0 : let pos = ent.position();
274 : let accessIndex;
275 0 : if (!pos)
276 : {
277 0 : let holder = PETRA.getHolder(gameState, ent);
278 0 : if (!holder || !holder.position())
279 : {
280 0 : API3.warn("Petra error: entity without position, but not garrisoned");
281 0 : PETRA.dumpEntity(ent);
282 0 : return gameState.ai.HQ.basesManager.baselessBase();
283 : }
284 0 : pos = holder.position();
285 0 : accessIndex = PETRA.getLandAccess(gameState, holder);
286 : }
287 : else
288 0 : accessIndex = PETRA.getLandAccess(gameState, ent);
289 :
290 0 : let distmin = Math.min();
291 : let dist;
292 : let bestbase;
293 0 : for (const base of gameState.ai.HQ.baseManagers())
294 : {
295 0 : if (base.ID == gameState.ai.HQ.basesManager.baselessBase().ID || exclude && base.ID == exclude)
296 0 : continue;
297 0 : if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined))
298 0 : continue;
299 0 : if (ent.hasClass("Structure") && base.accessIndex != accessIndex)
300 0 : continue;
301 0 : if (base.anchor && base.anchor.position())
302 0 : dist = API3.SquareVectorDistance(base.anchor.position(), pos);
303 : else
304 : {
305 0 : let found = false;
306 0 : for (let structure of base.buildings.values())
307 : {
308 0 : if (!structure.position())
309 0 : continue;
310 0 : dist = API3.SquareVectorDistance(structure.position(), pos);
311 0 : found = true;
312 0 : break;
313 : }
314 0 : if (!found)
315 0 : continue;
316 : }
317 0 : if (base.accessIndex != accessIndex)
318 0 : dist += 50000000;
319 0 : if (!base.anchor)
320 0 : dist += 50000000;
321 0 : if (dist > distmin)
322 0 : continue;
323 0 : distmin = dist;
324 0 : bestbase = base;
325 : }
326 0 : if (!bestbase && !ent.hasClass("Structure"))
327 0 : bestbase = gameState.ai.HQ.basesManager.baselessBase();
328 0 : return bestbase;
329 : };
330 :
331 0 : PETRA.getHolder = function(gameState, ent)
332 : {
333 0 : for (let holder of gameState.getEntities().values())
334 : {
335 0 : if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1)
336 0 : return holder;
337 : }
338 0 : return undefined;
339 : };
340 :
341 : /** return the template of the built foundation if a foundation, otherwise return the entity itself */
342 0 : PETRA.getBuiltEntity = function(gameState, ent)
343 : {
344 0 : if (ent.foundationProgress() !== undefined)
345 0 : return gameState.getBuiltTemplate(ent.templateName());
346 :
347 0 : return ent;
348 : };
349 :
350 : /**
351 : * return true if it is not worth finishing this building (it would surely decay)
352 : * TODO implement the other conditions
353 : */
354 0 : PETRA.isNotWorthBuilding = function(gameState, ent)
355 : {
356 0 : if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID)
357 : {
358 0 : let buildTerritories = ent.buildTerritories();
359 0 : if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own"))
360 0 : return true;
361 : }
362 0 : return false;
363 : };
364 :
365 : /**
366 : * Check if the straight line between the two positions crosses an enemy territory
367 : */
368 0 : PETRA.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70)
369 : {
370 0 : let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1;
371 0 : let stepx = (pos2[0] - pos1[0]) / n;
372 0 : let stepy = (pos2[1] - pos1[1]) / n;
373 0 : for (let i = 1; i < n; ++i)
374 : {
375 0 : let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy];
376 0 : let owner = gameState.ai.HQ.territoryMap.getOwner(pos);
377 0 : if (owner && gameState.isPlayerEnemy(owner))
378 0 : return true;
379 : }
380 0 : return false;
381 : };
382 :
383 0 : PETRA.gatherTreasure = function(gameState, ent, water = false)
384 : {
385 0 : if (!gameState.ai.HQ.treasures.hasEntities())
386 0 : return false;
387 0 : if (!ent || !ent.position())
388 0 : return false;
389 0 : if (!ent.isTreasureCollector())
390 0 : return false;
391 : let treasureFound;
392 0 : let distmin = Math.min();
393 0 : let access = water ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent);
394 0 : for (let treasure of gameState.ai.HQ.treasures.values())
395 : {
396 : // let some time for the previous gatherer to reach the treasure before trying again
397 0 : let lastGathered = treasure.getMetadata(PlayerID, "lastGathered");
398 0 : if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20)
399 0 : continue;
400 0 : if (!water && access != PETRA.getLandAccess(gameState, treasure))
401 0 : continue;
402 0 : if (water && access != PETRA.getSeaAccess(gameState, treasure))
403 0 : continue;
404 0 : let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position());
405 0 : if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner))
406 0 : continue;
407 0 : let dist = API3.SquareVectorDistance(ent.position(), treasure.position());
408 0 : if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit
409 0 : continue;
410 0 : if (dist > distmin)
411 0 : continue;
412 0 : distmin = dist;
413 0 : treasureFound = treasure;
414 : }
415 0 : if (!treasureFound)
416 0 : return false;
417 0 : treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime);
418 0 : ent.collectTreasure(treasureFound);
419 0 : ent.setMetadata(PlayerID, "treasure", treasureFound.id());
420 0 : return true;
421 : };
422 :
423 0 : PETRA.dumpEntity = function(ent)
424 : {
425 0 : if (!ent)
426 0 : return;
427 0 : API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() +
428 : " state " + ent.unitAIState());
429 0 : API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") +
430 : " subrole " + ent.getMetadata(PlayerID, "subrole"));
431 0 : API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() +
432 : " foundationProgress " + ent.foundationProgress());
433 0 : API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") +
434 : " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") +
435 : " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport"));
436 0 : API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") +
437 : " gather-type " + ent.getMetadata(PlayerID, "gather-type") +
438 : " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") +
439 : " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy"));
440 : };
|