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