TriggerHelper functions to retrieve entities by player and by class.
[0ad.git] / binaries / data / mods / public / maps / scripts / TriggerHelper.js
blob735e58c44f12d33ab684bc4f0a260b72a719fa74
1 // Contains standardized functions suitable for using in trigger scripts.
2 // Do not use them in any other simulation script.
4 var TriggerHelper = {};
6 TriggerHelper.GetPlayerIDFromEntity = function(ent)
8         let cmpPlayer = Engine.QueryInterface(ent, IID_Player);
9         if (cmpPlayer)
10                 return cmpPlayer.GetPlayerID();
12         return -1;
15 TriggerHelper.GetOwner = function(ent)
17         let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
18         if (cmpOwnership)
19                 return cmpOwnership.GetOwner();
21         return -1;
24 TriggerHelper.GetEntitiesByPlayer = function(playerID)
26         return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(playerID);
29 TriggerHelper.GetAllPlayersEntities = function()
31         return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
34 TriggerHelper.SetUnitStance = function(ent, stance)
36         let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
37         if (cmpUnitAI)
38                 cmpUnitAI.SwitchToStance(stance);
41 /**
42  * Can be used to "force" a building/unit to spawn a group of entities.
43  *
44  * @param source Entity id of the point where they will be spawned from
45  * @param template Name of the template
46  * @param count Number of units to spawn
47  * @param owner Player id of the owner of the new units. By default, the owner
48  * of the source entity.
49  */
50 TriggerHelper.SpawnUnits = function(source, template, count, owner)
52         let entities = [];
53         let cmpFootprint = Engine.QueryInterface(source, IID_Footprint);
54         let cmpPosition = Engine.QueryInterface(source, IID_Position);
56         if (!cmpPosition || !cmpPosition.IsInWorld())
57         {
58                 error("tried to create entity from a source without position");
59                 return entities;
60         }
62         if (owner == null)
63                 owner = TriggerHelper.GetOwner(source);
65         for (let i = 0; i < count; ++i)
66         {
67                 let ent = Engine.AddEntity(template);
68                 let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
69                 if (!cmpEntPosition)
70                 {
71                         Engine.DestroyEntity(ent);
72                         error("tried to create entity without position");
73                         continue;
74                 }
76                 let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
77                 if (cmpEntOwnership)
78                         cmpEntOwnership.SetOwner(owner);
80                 entities.push(ent);
82                 let pos;
83                 if (cmpFootprint)
84                         pos = cmpFootprint.PickSpawnPoint(ent);
86                 // TODO this can happen if the player build on the place
87                 // where our trigger point is
88                 // We should probably warn the trigger maker in some way,
89                 // but not interrupt the game for the player
90                 if (!pos || pos.y < 0)
91                         pos = cmpPosition.GetPosition();
93                 cmpEntPosition.JumpTo(pos.x, pos.z);
94         }
96         return entities;
99 /**
100  * Can be used to spawn garrisoned units inside a building/ship.
102  * @param entity Entity id of the garrison holder in which units will be garrisoned
103  * @param template Name of the template
104  * @param count Number of units to spawn
105  * @param owner Player id of the owner of the new units. By default, the owner
106  * of the garrisonholder entity.
107  */
108 TriggerHelper.SpawnGarrisonedUnits = function(entity, template, count, owner)
110         let entities = [];
112         let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder);
113         if (!cmpGarrisonHolder)
114         {
115                 error("tried to create garrisoned entities inside a non-garrisonholder");
116                 return entities;
117         }
119         if (owner == null)
120                 owner = TriggerHelper.GetOwner(entity);
122         for (let i = 0; i < count; ++i)
123         {
124                 let ent = Engine.AddEntity(template);
126                 let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
127                 if (cmpOwnership)
128                         cmpOwnership.SetOwner(owner);
130                 if (cmpGarrisonHolder.PerformGarrison(ent))
131                 {
132                         let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
133                         if (cmpUnitAI)
134                                 cmpUnitAI.Autogarrison(entity);
136                         entities.push(ent);
137                 }
138                 else
139                         error("failed to garrison entity " + ent + " (" + template + ") inside " + entity);
140         }
142         return entities;
146  * Spawn units from all trigger points with this reference
147  * If player is defined, only spaw units from the trigger points
148  * that belong to that player
149  * @param ref Trigger point reference name to spawn units from
150  * @param template Template name
151  * @param count Number of spawned entities per Trigger point
152  * @param owner Owner of the spawned units. Default: the owner of the origins
153  * @return A list of new entities per origin like
154  * {originId1: [entId1, entId2], originId2: [entId3, entId4], ...}
155  */
156 TriggerHelper.SpawnUnitsFromTriggerPoints = function(ref, template, count, owner = null)
158         let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
159         let triggerPoints = cmpTrigger.GetTriggerPoints(ref);
161         let entities = {};
162         for (let point of triggerPoints)
163                 entities[point] = TriggerHelper.SpawnUnits(point, template, count, owner);
165         return entities;
169  * Returns the resource type that can be gathered from an entity
170  */
171 TriggerHelper.GetResourceType = function(entity)
173         let cmpResourceSupply = Engine.QueryInterface(entity, IID_ResourceSupply);
174         if (!cmpResourceSupply)
175                 return undefined;
177         return cmpResourceSupply.GetType();
181  * The given player will win the game.
182  * If it's not a last man standing game, then allies will win too and others will be defeated.
184  * @param {number} playerID - The player who will win.
185  * @param {function} victoryReason - Function that maps from number to plural string, for example
186  *     n => markForPluralTranslation(
187  *         "%(lastPlayer)s has won (game mode).",
188  *         "%(players)s and %(lastPlayer)s have won (game mode).",
189  *         n));
190  * It's a function since we don't know in advance how many players will have won.
191  */
192 TriggerHelper.SetPlayerWon = function(playerID, victoryReason, defeatReason)
194         let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
195         cmpEndGameManager.MarkPlayerAndAlliesAsWon(playerID, victoryReason, defeatReason);
199  * Defeats a single player.
201  * @param {number} - ID of that player.
202  * @param {string} - String to be shown in chat, for example
203  *                   markForTranslation("%(player)s has been defeated (objective).")
204  */
205 TriggerHelper.DefeatPlayer = function(playerID, defeatReason)
207         let cmpPlayer = QueryPlayerIDInterface(playerID);
208         if (cmpPlayer)
209                 cmpPlayer.SetState("defeated", defeatReason);
213  * Returns the number of current players
214  */
215 TriggerHelper.GetNumberOfPlayers = function()
217         return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
221  * A function to determine if an entity matches specific classes.
222  * See globalscripts/Templates.js for details of MatchesClassList.
224  * @param entity - ID of the entity that we want to check for classes.
225  * @param classes - List of the classes we are checking if the entity matches.
226  */
227 TriggerHelper.EntityMatchesClassList = function(entity, classes)
229         let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
230         return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes);
233 TriggerHelper.MatchEntitiesByClass = function(entities, classes)
235         return entities.filter(ent => TriggerHelper.EntityMatchesClassList(ent, classes));
238 TriggerHelper.GetPlayerEntitiesByClass = function(playerID, classes)
240         return TriggerHelper.MatchEntitiesByClass(TriggerHelper.GetEntitiesByPlayer(playerID), classes);
243 TriggerHelper.GetAllPlayersEntitiesByClass = function(playerID, classes)
245         return TriggerHelper.MatchEntitiesByClass(TriggerHelper.GetAllPlayersEntities(), classes);
249  * Return valid gaia-owned spawn points on land in neutral territory.
250  * If there are none, use those available in player-owned territory.
251  */
252 TriggerHelper.GetLandSpawnPoints = function()
254         let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
255         let cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
256         let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager);
257         let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
259         let neutralSpawnPoints = [];
260         let nonNeutralSpawnPoints = [];
262         for (let ent of cmpRangeManager.GetEntitiesByPlayer(0))
263         {
264                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
265                 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
266                 if (!cmpIdentity || !cmpPosition || !cmpPosition.IsInWorld())
267                         continue;
269                 let templateName = cmpTemplateManager.GetCurrentTemplateName(ent);
270                 if (!templateName)
271                         continue;
273                 let template = cmpTemplateManager.GetTemplate(templateName);
274                 if (!template || template.UnitMotionFlying)
275                         continue;
277                 let pos = cmpPosition.GetPosition();
278                 if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z))
279                         continue;
281                 if (cmpTerritoryManager.GetOwner(pos.x, pos.z) == 0)
282                         neutralSpawnPoints.push(ent);
283                 else
284                         nonNeutralSpawnPoints.push(ent);
285         }
287         return neutralSpawnPoints.length ? neutralSpawnPoints : nonNeutralSpawnPoints;
290 TriggerHelper.HasDealtWithTech = function(playerID, techName)
292         let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
293         let playerEnt = cmpPlayerManager.GetPlayerByID(playerID);
294         let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
295         return cmpTechnologyManager && (cmpTechnologyManager.IsTechnologyQueued(techName) ||
296                                         cmpTechnologyManager.IsTechnologyStarted(techName) ||
297                                         cmpTechnologyManager.IsTechnologyResearched(techName));
301  * Returns all names of templates that match the given identity classes, constrainted to an optional civ.
303  * @param {String} classes - See MatchesClassList for the accepted formats, for example "Class1 Class2+!Class3".
304  * @param [String] civ - Optionally only retrieve templates of the given civ. Can be left undefined.
305  * @param [String] packedState - When retrieving siege engines filter for the "packed" or "unpacked" state
306  * @param [String] rank - If given, only return templates that have no or the given rank. For example "Elite".
307  * @param [Boolean] excludeBarracksVariants - Optionally exclude templates whose name ends with "_barracks"
308  */
309 TriggerHelper.GetTemplateNamesByClasses = function(classes, civ, packedState, rank, excludeBarracksVariants)
311         let templateNames = [];
312         let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
313         for (let templateName of cmpTemplateManager.FindAllTemplates(false))
314         {
315                 if (templateName.startsWith("campaigns/army_"))
316                         continue;
318                 if (excludeBarracksVariants && templateName.endsWith("_barracks"))
319                         continue;
321                 let template = cmpTemplateManager.GetTemplate(templateName);
323                 if (civ && (!template.Identity || template.Identity.Civ != civ))
324                         continue;
326                 if (!MatchesClassList(GetIdentityClasses(template.Identity), classes))
327                         continue;
329                 if (rank && template.Identity.Rank && template.Identity.Rank != rank)
330                         continue;
332                 if (packedState && template.Pack && packedState != template.Pack.State)
333                         continue;
335                 templateNames.push(templateName);
336         }
338         return templateNames;
341  * Composes a random set of the given templates of the given total size.
343  * @param {String[]} templateNames - for example ["brit_infantry_javelinist_b", "brit_cavalry_swordsman_e"]
344  * @param {Number} totalCount - total amount of templates, in this  example 12
345  * @returns an object where the keys are template names and values are amounts,
346  *          for example { "brit_infantry_javelinist_b": 4, "brit_cavalry_swordsman_e": 8 }
347  */
348 TriggerHelper.RandomTemplateComposition = function(templateNames, totalCount)
350         let frequencies = templateNames.map(() => randFloat(0, 1));
351         let frequencySum = frequencies.reduce((sum, frequency) => sum + frequency, 0);
353         let remainder = totalCount;
354         let templateCounts = {};
356         for (let i = 0; i < templateNames.length; ++i)
357         {
358                 let count = i == templateNames.length - 1 ? remainder : Math.min(remainder, Math.round(frequencies[i] / frequencySum * totalCount));
359                 if (!count)
360                         continue;
362                 templateCounts[templateNames[i]] = count;
363                 remainder -= count;
364         }
366         return templateCounts;
370  * Composes a random set of the given templates so that the sum of templates matches totalCount.
371  * For each template array that has a count item, it choses exactly that number of templates at random.
372  * The remaining template arrays are chosen depending on the given frequency.
373  * If a unique_entities array is given, it will only select the template if none of the given entityIDs
374  * already have that entity (useful to let heroes remain unique).
376  * @param {Object[]} templateBalancing - for example
377  *     [
378  *        { "templates": ["template1", "template2"], "frequency": 2 },
379  *        { "templates": ["template3"], "frequency": 1 },
380  *        { "templates": ["hero1", "hero2"], "unique_entities": [380, 495], "count": 1 }
381  *     ]
382  * @param {Number} totalCount - total amount of templates, for example 5.
384  * @returns an object where the keys are template names and values are amounts,
385  *    for example { "template1": 1, "template2": 3, "template3": 2, "hero1": 1 }
386  */
387 TriggerHelper.BalancedTemplateComposition = function(templateBalancing, totalCount)
389         // Remove all unavailable unique templates (heroes) and empty template arrays
390         let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
391         let templateBalancingFiltered = [];
392         for (let templateBalance of templateBalancing)
393         {
394                 let templateBalanceNew = clone(templateBalance);
396                 if (templateBalanceNew.unique_entities)
397                         templateBalanceNew.templates = templateBalanceNew.templates.filter(templateName =>
398                                 templateBalanceNew.unique_entities.every(ent => templateName != cmpTemplateManager.GetCurrentTemplateName(ent)));
400                 if (templateBalanceNew.templates.length)
401                         templateBalancingFiltered.push(templateBalanceNew);
402         }
404         // Helper function to add randomized templates to the result
405         let remainder = totalCount;
406         let results = {};
407         let addTemplates = (templateNames, count) => {
408                 let templateCounts = TriggerHelper.RandomTemplateComposition(templateNames, count);
409                 for (let templateName in templateCounts)
410                 {
411                         if (!results[templateName])
412                                 results[templateName] = 0;
414                         results[templateName] += templateCounts[templateName];
415                         remainder -= templateCounts[templateName];
416                 }
417         };
419         // Add template groups with fixed counts
420         for (let templateBalance of templateBalancingFiltered)
421                 if (templateBalance.count)
422                         addTemplates(templateBalance.templates, Math.min(remainder, templateBalance.count));
424         // Add template groups with frequency weights
425         let templateBalancingFrequencies = templateBalancingFiltered.filter(templateBalance => !!templateBalance.frequency);
426         let templateBalancingFrequencySum = templateBalancingFrequencies.reduce((sum, templateBalance) => sum + templateBalance.frequency, 0);
427         for (let i = 0; i < templateBalancingFrequencies.length; ++i)
428                 addTemplates(
429                         templateBalancingFrequencies[i].templates,
430                         i == templateBalancingFrequencies.length - 1 ?
431                                 remainder :
432                                 Math.min(remainder, Math.round(templateBalancingFrequencies[i].frequency / templateBalancingFrequencySum * totalCount)));
434         if (remainder != 0)
435                 warn("Could not chose as many templates as intended, remaining " + remainder + ", chosen: " + uneval(results));
437         return results;
441  * This will spawn random compositions of entities of the given templates at all garrisonholders of the given targetClass of the given player.
442  * The garrisonholder will be filled to capacityPercent.
443  * Returns an object where keys are entityIDs of the affected garrisonholders and the properties are template compositions, see RandomTemplateComposition.
444  */
445 TriggerHelper.SpawnAndGarrisonAtClasses = function(playerID, classes, templates, capacityPercent)
447         let results = {};
449         for (let entGarrisonHolder of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(playerID))
450         {
451                 let cmpIdentity = Engine.QueryInterface(entGarrisonHolder, IID_Identity);
452                 if (!cmpIdentity || !MatchesClassList(cmpIdentity.GetClassesList(), classes))
453                         continue;
455                 let cmpGarrisonHolder = Engine.QueryInterface(entGarrisonHolder, IID_GarrisonHolder);
456                 if (!cmpGarrisonHolder)
457                         continue;
459                 // TODO: account for already garrisoned entities
460                 results[entGarrisonHolder] = this.RandomTemplateComposition(templates, Math.floor(cmpGarrisonHolder.GetCapacity() * capacityPercent));
462                 for (let template in results[entGarrisonHolder])
463                         TriggerHelper.SpawnGarrisonedUnits(entGarrisonHolder, template, results[entGarrisonHolder][template], playerID);
464         }
466         return results;
469 Engine.RegisterGlobal("TriggerHelper", TriggerHelper);