Line data Source code
1 0 : var API3 = function(m)
2 : {
3 :
4 : // defines a template.
5 0 : m.Template = m.Class({
6 :
7 : "_init": function(sharedAI, templateName, template)
8 : {
9 0 : this._templateName = templateName;
10 0 : this._template = template;
11 : // save a reference to the template tech modifications
12 0 : if (!sharedAI._templatesModifications[this._templateName])
13 0 : sharedAI._templatesModifications[this._templateName] = {};
14 0 : this._templateModif = sharedAI._templatesModifications[this._templateName];
15 0 : this._tpCache = new Map();
16 : },
17 :
18 : // Helper function to return a template value, adjusting for tech.
19 : "get": function(string)
20 : {
21 0 : if (this._entityModif && this._entityModif.has(string))
22 0 : return this._entityModif.get(string);
23 0 : else if (this._templateModif)
24 : {
25 0 : let owner = this._entity ? this._entity.owner : PlayerID;
26 0 : if (this._templateModif[owner] && this._templateModif[owner].has(string))
27 0 : return this._templateModif[owner].get(string);
28 : }
29 :
30 0 : if (!this._tpCache.has(string))
31 : {
32 0 : let value = this._template;
33 0 : let args = string.split("/");
34 0 : for (let arg of args)
35 : {
36 0 : value = value[arg];
37 0 : if (value == undefined)
38 0 : break;
39 : }
40 0 : this._tpCache.set(string, value);
41 : }
42 0 : return this._tpCache.get(string);
43 : },
44 :
45 0 : "templateName": function() { return this._templateName; },
46 :
47 0 : "genericName": function() { return this.get("Identity/GenericName"); },
48 :
49 0 : "civ": function() { return this.get("Identity/Civ"); },
50 :
51 : "matchLimit": function() {
52 0 : if (!this.get("TrainingRestrictions"))
53 0 : return undefined;
54 0 : return this.get("TrainingRestrictions/MatchLimit");
55 : },
56 :
57 : "classes": function() {
58 0 : let template = this.get("Identity");
59 0 : if (!template)
60 0 : return undefined;
61 0 : return GetIdentityClasses(template);
62 : },
63 :
64 : "hasClass": function(name) {
65 0 : if (!this._classes)
66 0 : this._classes = this.classes();
67 0 : return this._classes && this._classes.indexOf(name) != -1;
68 : },
69 :
70 : "hasClasses": function(array) {
71 0 : if (!this._classes)
72 0 : this._classes = this.classes();
73 0 : return this._classes && MatchesClassList(this._classes, array);
74 : },
75 :
76 : "requirements": function() {
77 0 : return this.get("Identity/Requirements");
78 : },
79 :
80 : "available": function(gameState) {
81 0 : const requirements = this.requirements();
82 0 : return !requirements || Sim.RequirementsHelper.AreRequirementsMet(requirements, PlayerID);
83 : },
84 :
85 : "cost": function(productionQueue) {
86 0 : if (!this.get("Cost"))
87 0 : return {};
88 :
89 0 : let ret = {};
90 0 : for (let type in this.get("Cost/Resources"))
91 0 : ret[type] = +this.get("Cost/Resources/" + type);
92 0 : return ret;
93 : },
94 :
95 : "costSum": function(productionQueue) {
96 0 : let cost = this.cost(productionQueue);
97 0 : if (!cost)
98 0 : return 0;
99 0 : let ret = 0;
100 0 : for (let type in cost)
101 0 : ret += cost[type];
102 0 : return ret;
103 : },
104 :
105 : "techCostMultiplier": function(type) {
106 0 : return +(this.get("Researcher/TechCostMultiplier/"+type) || 1);
107 : },
108 :
109 : /**
110 : * Returns { "max": max, "min": min } or undefined if no obstruction.
111 : * max: radius of the outer circle surrounding this entity's obstruction shape
112 : * min: radius of the inner circle
113 : */
114 : "obstructionRadius": function() {
115 0 : if (!this.get("Obstruction"))
116 0 : return undefined;
117 :
118 0 : if (this.get("Obstruction/Static"))
119 : {
120 0 : let w = +this.get("Obstruction/Static/@width");
121 0 : let h = +this.get("Obstruction/Static/@depth");
122 0 : return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
123 : }
124 :
125 0 : if (this.get("Obstruction/Unit"))
126 : {
127 0 : let r = +this.get("Obstruction/Unit/@radius");
128 0 : return { "max": r, "min": r };
129 : }
130 :
131 0 : let right = this.get("Obstruction/Obstructions/Right");
132 0 : let left = this.get("Obstruction/Obstructions/Left");
133 0 : if (left && right)
134 : {
135 0 : let w = +right["@x"] + right["@width"] / 2 - left["@x"] + left["@width"] / 2;
136 0 : let h = Math.max(+right["@z"] + right["@depth"] / 2, +left["@z"] + left["@depth"] / 2) -
137 : Math.min(+right["@z"] - right["@depth"] / 2, +left["@z"] - left["@depth"] / 2);
138 0 : return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
139 : }
140 :
141 0 : return { "max": 0, "min": 0 }; // Units have currently no obstructions
142 : },
143 :
144 : /**
145 : * Returns the radius of a circle surrounding this entity's footprint.
146 : */
147 : "footprintRadius": function() {
148 0 : if (!this.get("Footprint"))
149 0 : return undefined;
150 :
151 0 : if (this.get("Footprint/Square"))
152 : {
153 0 : let w = +this.get("Footprint/Square/@width");
154 0 : let h = +this.get("Footprint/Square/@depth");
155 0 : return Math.sqrt(w * w + h * h) / 2;
156 : }
157 :
158 0 : if (this.get("Footprint/Circle"))
159 0 : return +this.get("Footprint/Circle/@radius");
160 :
161 0 : return 0; // this should never happen
162 : },
163 :
164 0 : "maxHitpoints": function() { return +(this.get("Health/Max") || 0); },
165 :
166 : "isHealable": function()
167 : {
168 0 : if (this.get("Health") !== undefined)
169 0 : return this.get("Health/Unhealable") !== "true";
170 0 : return false;
171 : },
172 :
173 0 : "isRepairable": function() { return this.get("Repairable") !== undefined; },
174 :
175 : "getPopulationBonus": function() {
176 0 : if (!this.get("Population"))
177 0 : return 0;
178 :
179 0 : return +this.get("Population/Bonus");
180 : },
181 :
182 : "resistanceStrengths": function() {
183 0 : let resistanceTypes = this.get("Resistance");
184 0 : if (!resistanceTypes || !resistanceTypes.Entity)
185 0 : return undefined;
186 :
187 0 : let resistance = {};
188 0 : if (resistanceTypes.Entity.Capture)
189 0 : resistance.Capture = +this.get("Resistance/Entity/Capture");
190 :
191 0 : if (resistanceTypes.Entity.Damage)
192 : {
193 0 : resistance.Damage = {};
194 0 : for (let damageType in resistanceTypes.Entity.Damage)
195 0 : resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType);
196 : }
197 :
198 : // ToDo: Resistance to StatusEffects.
199 :
200 0 : return resistance;
201 : },
202 :
203 : "attackTypes": function() {
204 0 : let attack = this.get("Attack");
205 0 : if (!attack)
206 0 : return undefined;
207 :
208 0 : let ret = [];
209 0 : for (let type in attack)
210 0 : ret.push(type);
211 0 : return ret;
212 : },
213 :
214 : "attackRange": function(type) {
215 0 : if (!this.get("Attack/" + type))
216 0 : return undefined;
217 :
218 0 : return {
219 : "max": +this.get("Attack/" + type +"/MaxRange"),
220 : "min": +(this.get("Attack/" + type +"/MinRange") || 0)
221 : };
222 : },
223 :
224 : "attackStrengths": function(type) {
225 0 : let attackDamageTypes = this.get("Attack/" + type + "/Damage");
226 0 : if (!attackDamageTypes)
227 0 : return undefined;
228 :
229 0 : let damage = {};
230 0 : for (let damageType in attackDamageTypes)
231 0 : damage[damageType] = +attackDamageTypes[damageType];
232 :
233 0 : return damage;
234 : },
235 :
236 : "captureStrength": function() {
237 0 : if (!this.get("Attack/Capture"))
238 0 : return undefined;
239 :
240 0 : return +this.get("Attack/Capture/Capture") || 0;
241 : },
242 :
243 : "attackTimes": function(type) {
244 0 : if (!this.get("Attack/" + type))
245 0 : return undefined;
246 :
247 0 : return {
248 : "prepare": +(this.get("Attack/" + type + "/PrepareTime") || 0),
249 : "repeat": +(this.get("Attack/" + type + "/RepeatTime") || 1000)
250 : };
251 : },
252 :
253 : // returns the classes this templates counters:
254 : // Return type is [ [-neededClasses- , multiplier], … ].
255 : "getCounteredClasses": function() {
256 0 : let attack = this.get("Attack");
257 0 : if (!attack)
258 0 : return undefined;
259 :
260 0 : let Classes = [];
261 0 : for (let type in attack)
262 : {
263 0 : let bonuses = this.get("Attack/" + type + "/Bonuses");
264 0 : if (!bonuses)
265 0 : continue;
266 0 : for (let b in bonuses)
267 : {
268 0 : let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
269 0 : if (bonusClasses)
270 0 : Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses/" + b +"/Multiplier")]);
271 : }
272 : }
273 0 : return Classes;
274 : },
275 :
276 : // returns true if the entity counters the target entity.
277 : // TODO: refine using the multiplier
278 : "counters": function(target) {
279 0 : let attack = this.get("Attack");
280 0 : if (!attack)
281 0 : return false;
282 0 : let mcounter = [];
283 0 : for (let type in attack)
284 : {
285 0 : let bonuses = this.get("Attack/" + type + "/Bonuses");
286 0 : if (!bonuses)
287 0 : continue;
288 0 : for (let b in bonuses)
289 : {
290 0 : let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
291 0 : if (bonusClasses)
292 0 : mcounter.concat(bonusClasses.split(" "));
293 : }
294 : }
295 0 : return target.hasClasses(mcounter);
296 : },
297 :
298 : // returns, if it exists, the multiplier from each attack against a given class
299 : "getMultiplierAgainst": function(type, againstClass) {
300 0 : if (!this.get("Attack/" + type +""))
301 0 : return undefined;
302 :
303 0 : let bonuses = this.get("Attack/" + type + "/Bonuses");
304 0 : if (bonuses)
305 : {
306 0 : for (let b in bonuses)
307 : {
308 0 : let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
309 0 : if (!bonusClasses)
310 0 : continue;
311 0 : for (let bcl of bonusClasses.split(" "))
312 0 : if (bcl == againstClass)
313 0 : return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier");
314 : }
315 : }
316 0 : return 1;
317 : },
318 :
319 : "buildableEntities": function(civ) {
320 0 : let templates = this.get("Builder/Entities/_string");
321 0 : if (!templates)
322 0 : return [];
323 0 : return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
324 : },
325 :
326 : "trainableEntities": function(civ) {
327 0 : const templates = this.get("Trainer/Entities/_string");
328 0 : if (!templates)
329 0 : return undefined;
330 0 : return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
331 : },
332 :
333 : "researchableTechs": function(gameState, civ) {
334 0 : const templates = this.get("Researcher/Technologies/_string");
335 0 : if (!templates)
336 0 : return undefined;
337 0 : let techs = templates.split(/\s+/);
338 0 : for (let i = 0; i < techs.length; ++i)
339 : {
340 0 : let tech = techs[i];
341 0 : if (tech.indexOf("{civ}") == -1)
342 0 : continue;
343 0 : let civTech = tech.replace("{civ}", civ);
344 0 : techs[i] = TechnologyTemplates.Has(civTech) ?
345 : civTech : tech.replace("{civ}", "generic");
346 : }
347 0 : return techs;
348 : },
349 :
350 : "resourceSupplyType": function() {
351 0 : if (!this.get("ResourceSupply"))
352 0 : return undefined;
353 0 : let [type, subtype] = this.get("ResourceSupply/Type").split('.');
354 0 : return { "generic": type, "specific": subtype };
355 : },
356 :
357 : "getResourceType": function() {
358 0 : if (!this.get("ResourceSupply"))
359 0 : return undefined;
360 0 : return this.get("ResourceSupply/Type").split('.')[0];
361 : },
362 :
363 0 : "getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },
364 :
365 0 : "resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); },
366 :
367 0 : "maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); },
368 :
369 : "resourceGatherRates": function() {
370 0 : if (!this.get("ResourceGatherer"))
371 0 : return undefined;
372 0 : let ret = {};
373 0 : let baseSpeed = +this.get("ResourceGatherer/BaseSpeed");
374 0 : for (let r in this.get("ResourceGatherer/Rates"))
375 0 : ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed;
376 0 : return ret;
377 : },
378 :
379 : "resourceDropsiteTypes": function() {
380 0 : if (!this.get("ResourceDropsite"))
381 0 : return undefined;
382 :
383 0 : let types = this.get("ResourceDropsite/Types");
384 0 : return types ? types.split(/\s+/) : [];
385 : },
386 :
387 : "isResourceDropsite": function(resourceType) {
388 0 : const types = this.resourceDropsiteTypes();
389 0 : return types && (!resourceType || types.indexOf(resourceType) !== -1);
390 : },
391 :
392 0 : "isTreasure": function() { return this.get("Treasure") !== undefined; },
393 :
394 : "treasureResources": function() {
395 0 : if (!this.get("Treasure"))
396 0 : return undefined;
397 0 : let ret = {};
398 0 : for (let r in this.get("Treasure/Resources"))
399 0 : ret[r] = +this.get("Treasure/Resources/" + r);
400 0 : return ret;
401 : },
402 :
403 :
404 0 : "garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); },
405 :
406 0 : "garrisonMax": function() { return this.get("GarrisonHolder/Max"); },
407 :
408 0 : "garrisonSize": function() { return this.get("Garrisonable/Size"); },
409 :
410 0 : "garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); },
411 :
412 0 : "getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); },
413 :
414 0 : "getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); },
415 :
416 : "getGarrisonArrowClasses": function() {
417 0 : if (!this.get("BuildingAI"))
418 0 : return undefined;
419 0 : return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/);
420 : },
421 :
422 0 : "buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); },
423 :
424 0 : "promotion": function() { return this.get("Promotion/Entity"); },
425 :
426 0 : "isPackable": function() { return this.get("Pack") != undefined; },
427 :
428 : "isHuntable": function() {
429 : // Do not hunt retaliating animals (dead animals can be used).
430 : // Assume entities which can attack, will attack.
431 0 : return this.get("ResourceSupply/KillBeforeGather") &&
432 : (!this.get("Health") || !this.get("Attack"));
433 : },
434 :
435 0 : "walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); },
436 :
437 0 : "trainingCategory": function() { return this.get("TrainingRestrictions/Category"); },
438 :
439 : "buildTime": function(researcher) {
440 0 : let time = +this.get("Cost/BuildTime");
441 0 : if (researcher)
442 0 : time *= researcher.techCostMultiplier("time");
443 0 : return time;
444 : },
445 :
446 0 : "buildCategory": function() { return this.get("BuildRestrictions/Category"); },
447 :
448 : "buildDistance": function() {
449 0 : let distance = this.get("BuildRestrictions/Distance");
450 0 : if (!distance)
451 0 : return undefined;
452 0 : let ret = {};
453 0 : for (let key in distance)
454 0 : ret[key] = this.get("BuildRestrictions/Distance/" + key);
455 0 : return ret;
456 : },
457 :
458 0 : "buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); },
459 :
460 : "buildTerritories": function() {
461 0 : if (!this.get("BuildRestrictions"))
462 0 : return undefined;
463 0 : let territory = this.get("BuildRestrictions/Territory");
464 0 : return !territory ? undefined : territory.split(/\s+/);
465 : },
466 :
467 : "hasBuildTerritory": function(territory) {
468 0 : let territories = this.buildTerritories();
469 0 : return territories && territories.indexOf(territory) != -1;
470 : },
471 :
472 : "hasTerritoryInfluence": function() {
473 0 : return this.get("TerritoryInfluence") !== undefined;
474 : },
475 :
476 : "hasDefensiveFire": function() {
477 0 : if (!this.get("Attack/Ranged"))
478 0 : return false;
479 0 : return this.getDefaultArrow() || this.getArrowMultiplier();
480 : },
481 :
482 : "territoryInfluenceRadius": function() {
483 0 : if (this.get("TerritoryInfluence") !== undefined)
484 0 : return +this.get("TerritoryInfluence/Radius");
485 0 : return -1;
486 : },
487 :
488 : "territoryInfluenceWeight": function() {
489 0 : if (this.get("TerritoryInfluence") !== undefined)
490 0 : return +this.get("TerritoryInfluence/Weight");
491 0 : return -1;
492 : },
493 :
494 : "territoryDecayRate": function() {
495 0 : return +(this.get("TerritoryDecay/DecayRate") || 0);
496 : },
497 :
498 : "defaultRegenRate": function() {
499 0 : return +(this.get("Capturable/RegenRate") || 0);
500 : },
501 :
502 : "garrisonRegenRate": function() {
503 0 : return +(this.get("Capturable/GarrisonRegenRate") || 0);
504 : },
505 :
506 0 : "visionRange": function() { return +this.get("Vision/Range"); },
507 :
508 0 : "gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); },
509 :
510 0 : "isBuilder": function() { return this.get("Builder") !== undefined; },
511 :
512 0 : "isGatherer": function() { return this.get("ResourceGatherer") !== undefined; },
513 :
514 : "canGather": function(type) {
515 0 : let gatherRates = this.get("ResourceGatherer/Rates");
516 0 : if (!gatherRates)
517 0 : return false;
518 0 : for (let r in gatherRates)
519 0 : if (r.split('.')[0] === type)
520 0 : return true;
521 0 : return false;
522 : },
523 :
524 0 : "isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; },
525 :
526 0 : "isTurretHolder": function() { return this.get("TurretHolder") !== undefined; },
527 :
528 : /**
529 : * returns true if the tempalte can capture the given target entity
530 : * if no target is given, returns true if the template has the Capture attack
531 : */
532 : "canCapture": function(target)
533 : {
534 0 : if (!this.get("Attack/Capture"))
535 0 : return false;
536 0 : if (!target)
537 0 : return true;
538 0 : if (!target.get("Capturable"))
539 0 : return false;
540 0 : let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
541 0 : return !restrictedClasses || !target.hasClasses(restrictedClasses);
542 : },
543 :
544 0 : "isCapturable": function() { return this.get("Capturable") !== undefined; },
545 :
546 0 : "canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; },
547 :
548 0 : "canGarrison": function() { return "Garrisonable" in this._template; },
549 :
550 0 : "canOccupyTurret": function() { return "Turretable" in this._template; },
551 :
552 0 : "isTreasureCollector": function() { return this.get("TreasureCollector") !== undefined; },
553 : });
554 :
555 :
556 : // defines an entity, with a super Template.
557 : // also redefines several of the template functions where the only change is applying aura and tech modifications.
558 0 : m.Entity = m.Class({
559 : "_super": m.Template,
560 :
561 : "_init": function(sharedAI, entity)
562 : {
563 0 : this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template));
564 :
565 0 : this._entity = entity;
566 0 : this._ai = sharedAI;
567 : // save a reference to the template tech modifications
568 0 : if (!sharedAI._templatesModifications[this._templateName])
569 0 : sharedAI._templatesModifications[this._templateName] = {};
570 0 : this._templateModif = sharedAI._templatesModifications[this._templateName];
571 : // save a reference to the entity tech/aura modifications
572 0 : if (!sharedAI._entitiesModifications.has(entity.id))
573 0 : sharedAI._entitiesModifications.set(entity.id, new Map());
574 0 : this._entityModif = sharedAI._entitiesModifications.get(entity.id);
575 : },
576 :
577 0 : "queryInterface": function(iid) { return SimEngine.QueryInterface(this.id(), iid) },
578 :
579 0 : "toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; },
580 :
581 0 : "id": function() { return this._entity.id; },
582 :
583 : /**
584 : * Returns extra data that the AI scripts have associated with this entity,
585 : * for arbitrary local annotations.
586 : * (This data should not be shared with any other AI scripts.)
587 : */
588 0 : "getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); },
589 :
590 : /**
591 : * Sets extra data to be associated with this entity.
592 : */
593 0 : "setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); },
594 :
595 0 : "deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; },
596 :
597 0 : "deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); },
598 :
599 0 : "position": function() { return this._entity.position; },
600 0 : "angle": function() { return this._entity.angle; },
601 :
602 0 : "isIdle": function() { return this._entity.idle; },
603 :
604 0 : "getStance": function() { return this._entity.stance; },
605 0 : "unitAIState": function() { return this._entity.unitAIState; },
606 0 : "unitAIOrderData": function() { return this._entity.unitAIOrderData; },
607 :
608 0 : "hitpoints": function() { return this._entity.hitpoints; },
609 0 : "isHurt": function() { return this.hitpoints() < this.maxHitpoints(); },
610 0 : "healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); },
611 0 : "needsHeal": function() { return this.isHurt() && this.isHealable(); },
612 0 : "needsRepair": function() { return this.isHurt() && this.isRepairable(); },
613 0 : "decaying": function() { return this._entity.decaying; },
614 0 : "capturePoints": function() {return this._entity.capturePoints; },
615 0 : "isInvulnerable": function() { return this._entity.invulnerability || false; },
616 :
617 0 : "isSharedDropsite": function() { return this._entity.sharedDropsite === true; },
618 :
619 : /**
620 : * Returns the current training queue state, of the form
621 : * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
622 : */
623 : "trainingQueue": function() {
624 0 : return this._entity.trainingQueue;
625 : },
626 :
627 : "trainingQueueTime": function() {
628 0 : let queue = this._entity.trainingQueue;
629 0 : if (!queue)
630 0 : return undefined;
631 0 : let time = 0;
632 0 : for (let item of queue)
633 0 : time += item.timeRemaining;
634 0 : return time / 1000;
635 : },
636 :
637 : "foundationProgress": function() {
638 0 : return this._entity.foundationProgress;
639 : },
640 :
641 : "getBuilders": function() {
642 0 : if (this._entity.foundationProgress === undefined)
643 0 : return undefined;
644 0 : if (this._entity.foundationBuilders === undefined)
645 0 : return [];
646 0 : return this._entity.foundationBuilders;
647 : },
648 :
649 : "getBuildersNb": function() {
650 0 : if (this._entity.foundationProgress === undefined)
651 0 : return undefined;
652 0 : if (this._entity.foundationBuilders === undefined)
653 0 : return 0;
654 0 : return this._entity.foundationBuilders.length;
655 : },
656 :
657 : "owner": function() {
658 0 : return this._entity.owner;
659 : },
660 :
661 : "isOwn": function(player) {
662 0 : if (typeof this._entity.owner === "undefined")
663 0 : return false;
664 0 : return this._entity.owner === player;
665 : },
666 :
667 : "resourceSupplyAmount": function() {
668 0 : return this.queryInterface(Sim.IID_ResourceSupply)?.GetCurrentAmount();
669 : },
670 :
671 : "resourceSupplyNumGatherers": function()
672 : {
673 0 : return this.queryInterface(Sim.IID_ResourceSupply)?.GetNumGatherers();
674 : },
675 :
676 : "isFull": function()
677 : {
678 0 : let numGatherers = this.resourceSupplyNumGatherers();
679 0 : if (numGatherers)
680 0 : return this.maxGatherers() === numGatherers;
681 :
682 0 : return undefined;
683 : },
684 :
685 : "resourceCarrying": function() {
686 0 : return this.queryInterface(Sim.IID_ResourceGatherer)?.GetCarryingStatus();
687 : },
688 :
689 : "currentGatherRate": function() {
690 : // returns the gather rate for the current target if applicable.
691 0 : if (!this.get("ResourceGatherer"))
692 0 : return undefined;
693 :
694 0 : if (this.unitAIOrderData().length &&
695 : this.unitAIState().split(".")[1] == "GATHER")
696 : {
697 : let res;
698 : // this is an abuse of "_ai" but it works.
699 0 : if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined)
700 0 : res = this._ai._entities.get(this.unitAIOrderData()[0].target);
701 0 : else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined)
702 0 : res = this._ai._entities.get(this.unitAIOrderData()[1].target);
703 0 : if (!res)
704 0 : return 0;
705 0 : let type = res.resourceSupplyType();
706 0 : if (!type)
707 0 : return 0;
708 :
709 0 : let tstring = type.generic + "." + type.specific;
710 0 : let rate = +this.get("ResourceGatherer/BaseSpeed");
711 0 : rate *= +this.get("ResourceGatherer/Rates/" +tstring);
712 0 : if (rate)
713 0 : return rate;
714 0 : return 0;
715 : }
716 0 : return undefined;
717 : },
718 :
719 : "garrisonHolderID": function() {
720 0 : return this._entity.garrisonHolderID;
721 : },
722 :
723 0 : "garrisoned": function() { return this._entity.garrisoned; },
724 :
725 : "garrisonedSlots": function() {
726 0 : let count = 0;
727 :
728 0 : if (this._entity.garrisoned)
729 0 : for (let ent of this._entity.garrisoned)
730 0 : count += +this._ai._entities.get(ent).garrisonSize();
731 :
732 0 : return count;
733 : },
734 :
735 : "canGarrisonInside": function()
736 : {
737 0 : return this.garrisonedSlots() < this.garrisonMax();
738 : },
739 :
740 : /**
741 : * returns true if the entity can attack (including capture) the given class.
742 : */
743 : "canAttackClass": function(aClass)
744 : {
745 0 : let attack = this.get("Attack");
746 0 : if (!attack)
747 0 : return false;
748 :
749 0 : for (let type in attack)
750 : {
751 0 : if (type == "Slaughter")
752 0 : continue;
753 0 : let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
754 0 : if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses))
755 0 : return true;
756 : }
757 0 : return false;
758 : },
759 :
760 : /**
761 : * Derived from Attack.js' similary named function.
762 : * @return {boolean} - Whether an entity can attack a given target.
763 : */
764 : "canAttackTarget": function(target, allowCapture)
765 : {
766 0 : let attackTypes = this.get("Attack");
767 0 : if (!attackTypes)
768 0 : return false;
769 :
770 0 : let canCapture = allowCapture && this.canCapture(target);
771 0 : let health = target.get("Health");
772 0 : if (!health)
773 0 : return canCapture;
774 :
775 0 : for (let type in attackTypes)
776 : {
777 0 : if (type == "Capture" ? !canCapture : target.isInvulnerable())
778 0 : continue;
779 :
780 0 : let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
781 0 : if (!restrictedClasses || !target.hasClasses(restrictedClasses))
782 0 : return true;
783 : }
784 :
785 0 : return false;
786 : },
787 :
788 : "move": function(x, z, queued = false, pushFront = false) {
789 0 : Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued, "pushFront": pushFront });
790 0 : return this;
791 : },
792 :
793 : "moveToRange": function(x, z, min, max, queued = false, pushFront = false) {
794 0 : Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront });
795 0 : return this;
796 : },
797 :
798 : "attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) {
799 0 : Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
800 0 : return this;
801 : },
802 :
803 : // violent, aggressive, defensive, passive, standground
804 : "setStance": function(stance) {
805 0 : if (this.getStance() === undefined)
806 0 : return undefined;
807 0 : Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance});
808 0 : return this;
809 : },
810 :
811 : "stopMoving": function() {
812 0 : Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": false });
813 : },
814 :
815 : "unload": function(id) {
816 0 : if (!this.get("GarrisonHolder"))
817 0 : return undefined;
818 0 : Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] });
819 0 : return this;
820 : },
821 :
822 : // Unloads all owned units, don't unload allies
823 : "unloadAll": function() {
824 0 : if (!this.get("GarrisonHolder"))
825 0 : return undefined;
826 0 : Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] });
827 0 : return this;
828 : },
829 :
830 : "garrison": function(target, queued = false, pushFront = false) {
831 0 : Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
832 0 : return this;
833 : },
834 :
835 : "occupy-turret": function(target, queued = false, pushFront = false) {
836 0 : Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
837 0 : return this;
838 : },
839 :
840 : "attack": function(unitId, allowCapture = true, queued = false, pushFront = false) {
841 0 : Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
842 0 : return this;
843 : },
844 :
845 : "collectTreasure": function(target, queued = false, pushFront = false) {
846 0 : Engine.PostCommand(PlayerID, {
847 : "type": "collect-treasure",
848 : "entities": [this.id()],
849 : "target": target.id(),
850 : "queued": queued,
851 : "pushFront": pushFront
852 : });
853 0 : return this;
854 : },
855 :
856 : // moveApart from a point in the opposite direction with a distance dist
857 : "moveApart": function(point, dist) {
858 0 : if (this.position() !== undefined)
859 : {
860 0 : let direction = [this.position()[0] - point[0], this.position()[1] - point[1]];
861 0 : let norm = m.VectorDistance(point, this.position());
862 0 : if (norm === 0)
863 0 : direction = [1, 0];
864 : else
865 : {
866 0 : direction[0] /= norm;
867 0 : direction[1] /= norm;
868 : }
869 0 : Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false, "pushFront": false });
870 : }
871 0 : return this;
872 : },
873 :
874 : // Flees from a unit in the opposite direction.
875 : "flee": function(unitToFleeFrom) {
876 0 : if (this.position() !== undefined && unitToFleeFrom.position() !== undefined)
877 : {
878 0 : let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],
879 : this.position()[1] - unitToFleeFrom.position()[1]];
880 0 : let dist = m.VectorDistance(unitToFleeFrom.position(), this.position());
881 0 : FleeDirection[0] = 40 * FleeDirection[0] / dist;
882 0 : FleeDirection[1] = 40 * FleeDirection[1] / dist;
883 :
884 0 : Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false, "pushFront": false });
885 : }
886 0 : return this;
887 : },
888 :
889 : "gather": function(target, queued = false, pushFront = false) {
890 0 : Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
891 0 : return this;
892 : },
893 :
894 : "repair": function(target, autocontinue = false, queued = false, pushFront = false) {
895 0 : Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued, "pushFront": pushFront });
896 0 : return this;
897 : },
898 :
899 : "returnResources": function(target, queued = false, pushFront = false) {
900 0 : Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
901 0 : return this;
902 : },
903 :
904 : "destroy": function() {
905 0 : Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] });
906 0 : return this;
907 : },
908 :
909 : "barter": function(buyType, sellType, amount) {
910 0 : Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount });
911 0 : return this;
912 : },
913 :
914 : "tradeRoute": function(target, source) {
915 0 : Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false, "pushFront": false });
916 0 : return this;
917 : },
918 :
919 : "setRallyPoint": function(target, command) {
920 0 : let data = { "command": command, "target": target.id() };
921 0 : Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data });
922 0 : return this;
923 : },
924 :
925 : "unsetRallyPoint": function() {
926 0 : Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] });
927 0 : return this;
928 : },
929 :
930 : "train": function(civ, type, count, metadata, pushFront = false)
931 : {
932 0 : let trainable = this.trainableEntities(civ);
933 0 : if (!trainable)
934 : {
935 0 : error("Called train("+type+", "+count+") on non-training entity "+this);
936 0 : return this;
937 : }
938 0 : if (trainable.indexOf(type) == -1)
939 : {
940 0 : error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
941 0 : return this;
942 : }
943 :
944 0 : Engine.PostCommand(PlayerID, {
945 : "type": "train",
946 : "entities": [this.id()],
947 : "template": type,
948 : "count": count,
949 : "metadata": metadata,
950 : "pushFront": pushFront
951 : });
952 0 : return this;
953 : },
954 :
955 : "construct": function(template, x, z, angle, metadata) {
956 : // TODO: verify this unit can construct this, just for internal
957 : // sanity-checking and error reporting
958 :
959 0 : Engine.PostCommand(PlayerID, {
960 : "type": "construct",
961 : "entities": [this.id()],
962 : "template": template,
963 : "x": x,
964 : "z": z,
965 : "angle": angle,
966 : "autorepair": false,
967 : "autocontinue": false,
968 : "queued": false,
969 : "pushFront": false,
970 : "metadata": metadata // can be undefined
971 : });
972 0 : return this;
973 : },
974 :
975 : "research": function(template, pushFront = false) {
976 0 : Engine.PostCommand(PlayerID, {
977 : "type": "research",
978 : "entity": this.id(),
979 : "template": template,
980 : "pushFront": pushFront
981 : });
982 0 : return this;
983 : },
984 :
985 : "stopProduction": function(id) {
986 0 : Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id });
987 0 : return this;
988 : },
989 :
990 : "stopAllProduction": function(percentToStopAt) {
991 0 : let queue = this._entity.trainingQueue;
992 0 : if (!queue)
993 0 : return true; // no queue, so technically we stopped all production.
994 0 : for (let item of queue)
995 0 : if (item.progress < percentToStopAt)
996 0 : Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": item.id });
997 0 : return this;
998 : },
999 :
1000 : "guard": function(target, queued = false, pushFront = false) {
1001 0 : Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
1002 0 : return this;
1003 : },
1004 :
1005 : "removeGuard": function() {
1006 0 : Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] });
1007 0 : return this;
1008 : }
1009 : });
1010 :
1011 0 : return m;
1012 :
1013 : }(API3);
|