Map description for Jebel Barkal, based on description by Sundiata, refs #5040.
[0ad.git] / binaries / data / mods / public / maps / random / jebel_barkal_triggers.js
blob9f73b8722c98b147d4d7cf77593bd4e63db7bf62
1 /**
2  * The city is patroled along its paths by infantry champions that respawn reoccuringly.
3  * There are increasingly great gaia attacks started from the different buildings.
4  * The players can destroy gaia buildings to reduce the number of attackers for the future.
5  */
7 /**
8  * If set to true, it will print how many templates would be spawned if the players were not defeated.
9  */
10 const dryRun = false;
12 /**
13  * If enabled, prints the number of units to the command line output.
14  */
15 const showDebugLog = false;
17 // TODO: harass attackers
19 // TODO: use Object.assign to remove redundancy
21 var jebelBarkal_rank = "Advanced";
23 /**
24  * These are the templates spawned at the gamestart and during the game.
25  */
26 var jebelBarkal_templates = deepfreeze({
27         "heroes": TriggerHelper.GetTemplateNamesByClasses("Hero", "kush", undefined, jebelBarkal_rank, true),
28         "champions": TriggerHelper.GetTemplateNamesByClasses("Champion+!Elephant", "kush", undefined, jebelBarkal_rank, true),
29         "elephants": TriggerHelper.GetTemplateNamesByClasses("Champion+Elephant", "kush", undefined, jebelBarkal_rank, true),
30         "champion_infantry": TriggerHelper.GetTemplateNamesByClasses("Champion+Infantry", "kush", undefined, jebelBarkal_rank, true),
31         "champion_infantry_melee": TriggerHelper.GetTemplateNamesByClasses("Champion+Infantry+Infantry", "kush", undefined, jebelBarkal_rank, true),
32         "champion_infantry_ranged": TriggerHelper.GetTemplateNamesByClasses("Champion+Infantry+Ranged", "kush", undefined, jebelBarkal_rank, true),
33         "champion_cavalry": TriggerHelper.GetTemplateNamesByClasses("Champion+Cavalry", "kush", undefined, jebelBarkal_rank, true),
34         "citizenSoldiers": TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier+Infantry", "kush", undefined, jebelBarkal_rank, true),
35         "citizenSoldier_infantry": TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier+Infantry", "kush", undefined, jebelBarkal_rank, true),
36         "citizenSoldier_infantry_melee": TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier+Infantry", "kush", undefined, jebelBarkal_rank, true),
37         "citizenSoldier_infantry_ranged": TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier+Infantry", "kush", undefined, jebelBarkal_rank, true),
38         "citizenSoldier_cavalry": TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier+Cavalry", "kush", undefined, jebelBarkal_rank, true),
39         "healers": TriggerHelper.GetTemplateNamesByClasses("Healer","kush", undefined, jebelBarkal_rank, true),
40         "females": TriggerHelper.GetTemplateNamesByClasses("FemaleCitizen","kush", undefined, jebelBarkal_rank),
41         "siege_towers": TriggerHelper.GetTemplateNamesByClasses("SiegeTower", "kush", undefined, jebelBarkal_rank)
42 });
44 /**
45  * These are the formations patroling and attacking units can use.
46  * TODO: use?
47  * "special/formations/line_open",
48  * "special/formations/wedge",
49  * "special/formations/syntagma"
51 var jebelBarkal_formations = [
52         "special/formations/line_closed",
53         "special/formations/column_closed"
56 /**
57  *  Balancing helper function.
58  *
59  *  @returns min0 value at the beginning of the game, min60 after an hour of gametime or longer and
60  *  a proportionate number between these two values before the first hour is reached.
61  */
62 var scaleByTime = (minCurrent, min0, min60) => min0 + (min60 - min0) * Math.min(1, minCurrent / 60);
64 /**
65  *  @returns min0 value at the beginning of the game, min60 after an hour of gametime or longer and
66  *  a proportionate number between these two values before the first hour is reached.
67  */
68 var scaleByMapSize = (min, max) => min + (max - min) * (TriggerHelper.GetMapSizeTiles() - 128) / (512 - 128);
70 /**
71  * Defensive Infantry units patrol along the paths of the city.
72  */
73 var jebelBarkal_cityPatrolGroup_count = time => scaleByTime(time, 3, scaleByMapSize(3, 10));
74 var jebelBarkal_cityPatrolGroup_interval = time => scaleByTime(time, 5, 3);
75 var jebelBarkal_cityPatrolGroup_balancing = {
76         "buildingClasses": ["Wonder", "Temple", "CivCentre", "Fortress", "Barracks", "Embassy"],
77         "unitCount": time => Math.min(20, scaleByTime(time, 10, 45)),
78         "unitComposition": (time, heroes) => [
79                 {
80                         "templates": jebelBarkal_templates.champion_infantry_melee,
81                         "frequency": scaleByTime(time, 0, 2)
82                 },
83                 {
84                         "templates": jebelBarkal_templates.champion_infantry_ranged,
85                         "frequency": scaleByTime(time, 0, 3)
86                 },
87                 {
88                         "templates": jebelBarkal_templates.citizenSoldier_infantry_melee,
89                         "frequency": scaleByTime(time, 2, 0)
90                 },
91                 {
92                         "templates": jebelBarkal_templates.citizenSoldier_infantry_ranged,
93                         "frequency": scaleByTime(time, 3, 0)
94                 }
95         ]
98 /**
99  * Frequently the buildings spawn different units that attack the players groupwise.
100  */
101 var jebelBarkal_attackInterval = time => randFloat(5, 7);
104  * Frequently cavalry is spawned at few buildings to harass the enemy trade.
105  */
106 var jebelBarkal_harassInterval = time => randFloat(6, 8);
109  * Assume gaia to be the native kushite player.
110  */
111 var jebelBarkal_playerID = 0;
114  * Soldiers will patrol along the city grid.
115  */
116 var jebelBarkal_triggerPointPath = "A";
119  * Attacker groups approach these player buildings and attack enemies of the given classes encountered.
120  */
121 var jebelBarkal_pathTargetClasses = "CivCentre Wonder";
124  * Soldiers and siege towers prefer these targets when attacking or patroling.
125  */
126 var jebelBarkal_soldierTargetClasses = "Unit+!Ship";
129  * Elephants focus these units when attacking.
130  */
131 var jebelBarkal_elephantTargetClasses = "Defensive SiegeEngine";
134  * This defines which units are spawned and garrisoned at the gamestart per building.
135  */
136 var jebelBarkal_buildingGarrison = [
137         {
138                 "buildingClasses": ["Wonder", "Temple", "CivCentre", "Fortress"],
139                 "unitTemplates": jebelBarkal_templates.champions
140         },
141         {
142                 "buildingClasses": ["Barracks", "Embassy"],
143                 "unitTemplates": [...jebelBarkal_templates.citizenSoldiers, ...jebelBarkal_templates.champions]
144         },
145         {
146                 "buildingClasses": ["DefenseTower"],
147                 "unitTemplates": jebelBarkal_templates.champion_infantry
148         },
149         {
150                 "buildingClasses": ["ElephantStables"],
151                 "unitTemplates": jebelBarkal_templates.elephants
152         },
153         {
154                 "buildingClasses": ["Stable"],
155                 "unitTemplates": jebelBarkal_templates.champion_cavalry
156         },
157         {
158                 "buildingClasses": ["Pyramid"],
159                 "unitTemplates": [...jebelBarkal_templates.citizenSoldiers, ...jebelBarkal_templates.healers]
160         },
161         {
162                 "buildingClasses": ["House"],
163                 "unitTemplates": [...jebelBarkal_templates.females, ...jebelBarkal_templates.healers]
164         }
168  * This defines which units are spawned at the different buildings at the given time.
169  * The buildings are ordered by strength.
170  * Notice that there are always 2 groups of these count spawned, one for each side!
171  * The units should do a walk-attack to random player CCs
172  */
173 var jebelBarkal_attackerGroup_balancing = [
174         {
175                 "buildingClasses": ["Wonder"],
176                 "unitCount": time => scaleByTime(time, 0, 85),
177                 "unitComposition": (time, heroes) => [
178                         {
179                                 "templates": jebelBarkal_templates.heroes,
180                                 "count": randBool(scaleByTime(time, -0.5, 2)) ? 1 : 0,
181                                 "unique_entities": heroes
182                         },
183                         {
184                                 "templates": jebelBarkal_templates.healers,
185                                 "frequency": randFloat(0, 0.1)
186                         },
187                         {
188                                 "templates": jebelBarkal_templates.champions,
189                                 "frequency": scaleByTime(time, 0, 0.6)
190                         },
191                         {
192                                 "templates": jebelBarkal_templates.champion_infantry_ranged,
193                                 "frequency": scaleByTime(time, 0, 0.4)
194                         },
195                         {
196                                 "templates": jebelBarkal_templates.citizenSoldiers,
197                                 "frequency": scaleByTime(time, 1, 0)
198                         },
199                         {
200                                 "templates": jebelBarkal_templates.citizenSoldier_infantry_ranged,
201                                 "frequency": scaleByTime(time, 1, 0)
202                         }
203                 ]
204         },
205         {
206                 "buildingClasses": ["Fortress"],
207                 "unitCount": time => scaleByTime(time, 0, 45),
208                 "unitComposition": (time, heroes) => [
209                         {
210                                 "templates": jebelBarkal_templates.heroes,
211                                 "count": randBool(scaleByTime(time, -0.5, 1.5)) ? 1 : 0,
212                                 "unique_entities": heroes
213                         },
214                         {
215                                 "templates": jebelBarkal_templates.champions,
216                                 "frequency": scaleByTime(time, 0, 1)
217                         }
218                 ]
219         },
220         {
221                 "buildingClasses": ["Temple"],
222                 "unitCount": time => Math.min(45, scaleByTime(time, 0, 90)),
223                 "unitComposition": (time, heroes) => [
224                         {
225                                 "templates": jebelBarkal_templates.heroes,
226                                 "count": randBool(scaleByTime(time, -0.5, 1)) ? 1 : 0,
227                                 "unique_entities": heroes
228                         },
229                         {
230                                 "templates": jebelBarkal_templates.champion_infantry_melee,
231                                 "frequency": 0.5
232                         },
233                         {
234                                 "templates": jebelBarkal_templates.champion_infantry_ranged,
235                                 "frequency": 0.5
236                         },
237                         {
238                                 "templates": jebelBarkal_templates.healers,
239                                 "frequency": randFloat(0.05, 0.2)
240                         }
241                 ]
242         },
243         {
244                 "buildingClasses": ["CivCentre"],
245                 "unitCount": time => Math.min(40, scaleByTime(time, 0, 80)),
246                 "unitComposition": (time, heroes) => [
247                         {
248                                 "templates": jebelBarkal_templates.heroes,
249                                 "count": randBool(scaleByTime(time, -0.5, 0.5)) ? 1 : 0,
250                                 "unique_entities": heroes
251                         },
252                         {
253                                 "templates": jebelBarkal_templates.champion_infantry,
254                                 "frequency": scaleByTime(time, 0, 1)
255                         },
256                         {
257                                 "templates": jebelBarkal_templates.citizenSoldiers,
258                                 "frequency": scaleByTime(time, 1, 0)
259                         }
260                 ]
261         },
262         {
263                 "buildingClasses": ["Stable"],
264                 "unitCount": time => Math.min(30, scaleByTime(time, 0, 80)),
265                 "harasserSize": time => Math.min(20, scaleByTime(time, 0, 50)),
266                 "unitComposition": (time, heroes) => [
267                         {
268                                 "templates": jebelBarkal_templates.citizenSoldier_cavalry,
269                                 "frequency": scaleByTime(time, 2, 0)
270                         },
271                         {
272                                 "templates": jebelBarkal_templates.champion_cavalry,
273                                 "frequency": scaleByTime(time, 0, 1)
274                         }
275                 ]
276         },
277         {
278                 "buildingClasses": ["Barracks", "Embassy"],
279                 "unitCount": time => Math.min(35, scaleByTime(time, 0, 70)),
280                 "unitComposition": (time, heroes) => [
281                         {
282                                 "templates": jebelBarkal_templates.citizenSoldier_infantry,
283                                 "frequency": 1
284                         }
285                 ]
286         },
287         {
288                 "buildingClasses": ["ElephantStables"],
289                 "unitCount": time => scaleByTime(time, 0, 10),
290                 "unitComposition": (time, heroes) => [
291                         {
292                                 "templates": jebelBarkal_templates.elephants,
293                                 "frequency": 1
294                         }
295                 ]
296         }
299 Trigger.prototype.debugLog = function(txt)
301         if (showDebugLog)
302                 print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n");
305 Trigger.prototype.JebelBarkal_Init = function()
307         this.JebelBarkal_TrackUnits();
308         this.JebelBarkal_SetDefenderStance();
309         this.JebelBarkal_GarrisonBuildings();
310         this.JebelBarkal_SpawnCityPatrolGroups();
311         this.JebelBarkal_SpawnAttackerGroups();
314 Trigger.prototype.JebelBarkal_TrackUnits = function()
316         // Each item is an entity ID
317         this.jebelBarkal_heroes = [];
319         // Each item is an array of entity IDs
320         this.jebelBarkal_patrolingUnits = [];
322         // Array of entityIDs where patrol groups can spawn
323         this.jebelBarkal_patrolGroupSpawnPoints = TriggerHelper.GetPlayerEntitiesByClass(
324                 jebelBarkal_playerID,
325                 jebelBarkal_cityPatrolGroup_balancing.buildingClasses);
327         this.debugLog("Patrol spawn points: " + uneval(this.jebelBarkal_patrolGroupSpawnPoints));
329         // Array of entityIDs where attacker groups can spawn
330         this.jebelBarkal_attackerGroupSpawnPoints = TriggerHelper.GetPlayerEntitiesByClass(
331                 jebelBarkal_playerID,
332                 jebelBarkal_attackerGroup_balancing.reduce((classes, attackerSpawning) => classes.concat(attackerSpawning.buildingClasses), []));
334         this.debugLog("Attacker spawn points: " + uneval(this.jebelBarkal_attackerGroupSpawnPoints));
337 Trigger.prototype.JebelBarkal_SetDefenderStance = function()
339         for (let ent of TriggerHelper.GetPlayerEntitiesByClass(jebelBarkal_playerID, "Soldier"))
340                 TriggerHelper.SetUnitStance(ent, "defensive");
343 Trigger.prototype.JebelBarkal_GarrisonBuildings = function()
345         for (let buildingGarrison of jebelBarkal_buildingGarrison)
346                 TriggerHelper.SpawnAndGarrisonAtClasses(jebelBarkal_playerID, buildingGarrison.buildingClasses, buildingGarrison.unitTemplates, 1);
350  * Spawn new groups if old ones were wiped out.
351  */
352 Trigger.prototype.JebelBarkal_SpawnCityPatrolGroups = function()
354         if (!this.jebelBarkal_patrolGroupSpawnPoints.length)
355                 return;
357         // TODO: the patrol group size should be increased
358         let time = TriggerHelper.GetMinutes();
359         let groupCount = Math.floor(Math.max(0, jebelBarkal_cityPatrolGroup_count(time)) - this.jebelBarkal_patrolingUnits.length);
361         for (let i = 0; i < groupCount; ++i)
362         {
363                 let spawnEnt = pickRandom(this.jebelBarkal_patrolGroupSpawnPoints);
365                 let templateCounts = TriggerHelper.BalancedTemplateComposition(
366                         jebelBarkal_cityPatrolGroup_balancing.unitComposition(time, this.jebelBarkal_heroes),
367                         jebelBarkal_cityPatrolGroup_balancing.unitCount(time));
369                 this.debugLog("Spawning " + groupCount + " city patrol groups, " +
370                         this.jebelBarkal_patrolingUnits.length + " exist, templates:\n" + uneval(templateCounts));
372                 let groupEntities = this.JebelBarkal_SpawnTemplates(spawnEnt, templateCounts);
374                 this.jebelBarkal_patrolingUnits.push(groupEntities);
376                 for (let ent of groupEntities)
377                         TriggerHelper.SetUnitStance(ent, "defensive");
379                 TriggerHelper.SetUnitFormation(jebelBarkal_playerID, groupEntities, pickRandom(jebelBarkal_formations));
381                 // TODO: order waypoints
382                 for (let entTriggerPoint of this.GetTriggerPoints(jebelBarkal_triggerPointPath))
383                 {
384                         let pos = TriggerHelper.GetEntityPosition2D(entTriggerPoint);
385                         ProcessCommand(jebelBarkal_playerID, {
386                                 "type": "patrol",
387                                 "entities": groupEntities,
388                                 "x": pos.x,
389                                 "z": pos.y,
390                                 "targetClasses": {
391                                         "attack": jebelBarkal_soldierTargetClasses
392                                 },
393                                 "queued": true,
394                                 "allowCapture": false
395                         });
396                 }
397         }
399         this.DoAfterDelay(jebelBarkal_cityPatrolGroup_interval(time) * 60 * 1000, "JebelBarkal_SpawnCityPatrolGroups", {});
402 Trigger.prototype.JebelBarkal_SpawnTemplates = function(spawnEnt, templateCounts)
404         let groupEntities = [];
406         for (let templateName in templateCounts)
407         {
408                 let ents = TriggerHelper.SpawnUnits(spawnEnt, templateName, templateCounts[templateName], jebelBarkal_playerID);
410                 groupEntities = groupEntities.concat(ents);
412                 if (jebelBarkal_templates.heroes.indexOf(templateName) != -1 && ents[0])
413                         this.jebelBarkal_heroes.push(ents[0]);
414         }
416         return groupEntities;
420  * Spawn a group of attackers at every remaining building.
421  */
422 Trigger.prototype.JebelBarkal_SpawnAttackerGroups = function()
424         let time = TriggerHelper.GetMinutes();
425         this.debugLog("Attacker wave");
427         let targets = TriggerHelper.GetAllPlayersEntitiesByClass(jebelBarkal_pathTargetClasses);
428         if (!targets.length)
429                 return;
431         let spawnedAnything = false;
432         for (let spawnEnt of this.jebelBarkal_attackerGroupSpawnPoints)
433         {
434                 let spawnPointBalancing = jebelBarkal_attackerGroup_balancing.find(balancing =>
435                         TriggerHelper.EntityMatchesClassList(spawnEnt, balancing.buildingClasses));
437                 let unitCount = Math.round(spawnPointBalancing.unitCount(time));
439                 if (!unitCount)
440                         continue;
442                 let templateCounts = TriggerHelper.BalancedTemplateComposition(spawnPointBalancing.unitComposition(time, this.jebelBarkal_heroes), unitCount);
444                 this.debugLog("Spawning " + unitCount + " attackers at " + uneval(spawnPointBalancing.buildingClasses) + " " + spawnEnt + ":\n" + uneval(templateCounts));
446                 if (dryRun)
447                         continue;
449                 let groupEntities = this.JebelBarkal_SpawnTemplates(spawnEnt, templateCounts);
450                 spawnedAnything = true;
452                 // TODO Move to some place, 30 seconds later attack
454                 let isElephant = TriggerHelper.EntityMatchesClassList(groupEntities[0], "Elephant");
456                 for (let ent of groupEntities)
457                         TriggerHelper.SetUnitStance(ent, isElephant ? "aggressive" : "violent");
459                 if (!isElephant)
460                         TriggerHelper.SetUnitFormation(jebelBarkal_playerID, groupEntities, pickRandom(jebelBarkal_formations));
462                 let targetPos = TriggerHelper.GetEntityPosition2D(pickRandom(targets));
463                 if (!targetPos)
464                         continue;
466                 ProcessCommand(jebelBarkal_playerID, {
467                         "type": "attack-walk",
468                         "entities": groupEntities,
469                         "x": targetPos.x,
470                         "z": targetPos.y,
471                         "targetClasses": isElephant ? jebelBarkal_elephantTargetClasses : jebelBarkal_soldierTargetClasses,
472                         "allowCapture": false,
473                         "queued": false
474                 });
475         }
477         if (spawnedAnything)
478                 Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
479                         "message": markForTranslation("Napata is attacking!"),
480                         "translateMessage": true
481                 });
483         this.DoAfterDelay(jebelBarkal_attackInterval(TriggerHelper.GetMinutes()) * 60 * 1000, "JebelBarkal_SpawnAttackerGroups", {});
487  * Keep track of heroes, so that each of them remains unique.
488  * Keep track of spawn points, as only there units should be spawned.
489  * 
490  */
491 Trigger.prototype.JebelBarkal_OwnershipChange = function(data)
493         if (data.from != 0)
494                 return;
496         for (let array of [this.jebelBarkal_heroes, this.jebelBarkal_patrolGroupSpawnPoints, this.jebelBarkal_attackerGroupSpawnPoints, ...this.jebelBarkal_patrolingUnits])
497         {
498                 let idx = array.indexOf(data.entity);
499                 if (idx != -1)
500                         array.splice(idx, 1);
501         }
503         this.jebelBarkal_patrolingUnits = this.jebelBarkal_patrolingUnits.filter(entities => entities.length);
508         let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
509         cmpTrigger.RegisterTrigger("OnInitGame", "JebelBarkal_Init", { "enabled": true });
510         cmpTrigger.RegisterTrigger("OnOwnershipChanged", "JebelBarkal_OwnershipChange", { "enabled": true });