TriggerHelper functions to retrieve entities by player and by class.
[0ad.git] / binaries / data / mods / public / maps / random / danubius_triggers.js
blobe78c1b1a51766c8188e96679c6ee5fdc7c9458a2
1 // Ships respawn every few minutes, attack the closest warships, then patrol the sea.
2 // To prevent unlimited spawning of ships, no more than the amount of ships intended at a given time are spawned.
4 // Ships are filled or refilled with new units.
5 // The number of ships, number of units per ship, as well as ratio of siege engines, champion and heroes
6 // increases with time, while keeping an individual and randomized composition for each ship.
7 // Each hero exists at most once per map.
9 // Every few minutes, equal amount of ships unload units at the sides of the river unless
10 // one side of the river was wiped from players.
11 // Siege engines attack defensive structures, units attack units then patrol that side of the river.
13 const showDebugLog = false;
15 const danubiusAttackerTemplates = deepfreeze({
16         "ships": TriggerHelper.GetTemplateNamesByClasses("Warship", "gaul", undefined, undefined, true),
17         "siege": TriggerHelper.GetTemplateNamesByClasses("Siege","gaul", undefined, undefined, true),
18         "females": TriggerHelper.GetTemplateNamesByClasses("FemaleCitizen","gaul", undefined, undefined, true),
19         "healers": TriggerHelper.GetTemplateNamesByClasses("Healer","gaul", undefined, undefined, true),
20         "champions": TriggerHelper.GetTemplateNamesByClasses("Champion", "gaul", undefined, undefined, true),
21         "champion_infantry": TriggerHelper.GetTemplateNamesByClasses("Champion+Infantry", "gaul", undefined, undefined, true),
22         "citizen_soldiers": TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier", "gaul", undefined, "Basic", true),
23         "heroes": [
24                 // Excludes the Vercingetorix variant
25                 "units/gaul_hero_viridomarus",
26                 "units/gaul_hero_vercingetorix",
27                 "units/gaul_hero_brennus"
28         ]
29 });
31 var ccDefenders = [
32         { "count": 8, "templates": danubiusAttackerTemplates.citizen_soldiers },
33         { "count": 13, "templates": danubiusAttackerTemplates.champions },
34         { "count": 4, "templates": danubiusAttackerTemplates.healers },
35         { "count": 5, "templates": danubiusAttackerTemplates.females },
36         { "count": 10, "templates": ["gaia/fauna_sheep"] }
39 var gallicBuildingGarrison = [
40         {
41                 "buildingClasses": ["House"],
42                 "unitTemplates": danubiusAttackerTemplates.females.concat(danubiusAttackerTemplates.healers)
43         },
44         {
45                 "buildingClasses": ["CivCentre", "Temple"],
46                 "unitTemplates": danubiusAttackerTemplates.champions,
47         },
48         {
49                 "buildingClasses": ["DefenseTower", "Outpost"],
50                 "unitTemplates": danubiusAttackerTemplates.champion_infantry
51         }
54 /**
55  * Notice if gaia becomes too strong, players will just turtle and try to outlast the players on the other side.
56  * However we want interaction and fights between the teams.
57  * This can be accomplished by not wiping out players buildings entirely.
58  */
60 /**
61  * Time in minutes between two consecutive waves spawned from the gaia civic centers, if they still exist.
62  */
63 var ccAttackerInterval = t => randFloat(6, 8);
65 /**
66  * Number of attackers spawned at a civic center at t minutes ingame time.
67  */
68 var ccAttackerCount = t => Math.min(20, Math.max(0, Math.round(t * 1.5)));
70 /**
71  * Time between two consecutive waves.
72  */
73 var shipRespawnTime = () => randFloat(8, 10);
75 /**
76  * Limit of ships on the map when spawning them.
77  * Have at least two ships, so that both sides will be visited.
78  */
79 var shipCount = (t, numPlayers) => Math.max(2, Math.round(Math.min(1.5, t / 10) * numPlayers));
81 /**
82  * Order all ships to ungarrison at the shoreline.
83  */
84 var shipUngarrisonInterval = () => randFloat(5, 7);
86 /**
87  * Time between refillings of all ships with new soldiers.
88  */
89 var shipFillInterval = () => randFloat(4, 5);
91 /**
92  * Total count of gaia attackers per shipload.
93  */
94 var attackersPerShip = t => Math.min(30, Math.round(t * 2));
96 /**
97  * Likelihood of adding a non-existing hero at t minutes.
98  */
99 var heroProbability = t => Math.max(0, Math.min(1, (t - 25) / 60));
102  * Percent of healers to add per shipload after potentially adding a hero and siege engines.
103  */
104 var healerRatio = t => randFloat(0, 0.1);
107  * Number of siege engines to add per shipload.
108  */
109 var siegeCount = t => 1 + Math.min(2, Math.floor(t / 30));
112  * Percent of champions to be added after spawning heroes, healers and siege engines.
113  * Rest will be citizen soldiers.
114  */
115 var championRatio = t => Math.min(1, Math.max(0, (t - 25) / 75));
118  * Ships and land units will queue attack orders for this amount of closest units.
119  */
120 var targetCount = 3;
123  * Number of trigger points to patrol when not having enemies to attack.
124  */
125 var patrolCount = 5;
128  * Which units ships should focus when attacking and patrolling.
129  */
130 var shipTargetClass = "Warship";
133  * Which entities siege engines should focus when attacking and patrolling.
134  */
135 var siegeTargetClass = "Defensive";
138  * Which entities units should focus when attacking and patrolling.
139  */
140 var unitTargetClass = "Unit+!Ship";
143  * Ungarrison ships when being in this range of the target.
144  */
145 var shipUngarrisonDistance = 50;
148  * Currently formations are not working properly and enemies in vision range are often ignored.
149  * So only have a small chance of using formations.
150  */
151 var formationProbability = 0.2;
153 var unitFormations = [
154         "box",
155         "battle_line",
156         "line_closed",
157         "column_closed"
161  * Chance for the units at the meeting place to participate in the ritual.
162  */
163 var ritualProbability = 0.75;
166  * Units celebrating at the meeting place will perform one of these animations
167  * if idle and switch back when becoming idle again.
168  */
169 var ritualAnimations = {
170         "female": ["attack_slaughter"],
171         "male": ["attack_capture", "promotion", "attack_slaughter"],
172         "healer": ["attack_capture", "promotion", "heal"]
175 var triggerPointShipSpawn = "A";
176 var triggerPointShipPatrol = "B";
177 var triggerPointUngarrisonLeft = "C";
178 var triggerPointUngarrisonRight = "D";
179 var triggerPointLandPatrolLeft = "E";
180 var triggerPointLandPatrolRight = "F";
181 var triggerPointCCAttackerPatrolLeft = "G";
182 var triggerPointCCAttackerPatrolRight = "H";
183 var triggerPointRiverDirection = "I";
186  * Which playerID to use for the opposing gallic reinforcements.
187  */
188 var gaulPlayer = 0;
190 Trigger.prototype.debugLog = function(txt)
192         if (showDebugLog)
193                 print(
194                         "DEBUG [" +
195                         Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n");
198 Trigger.prototype.GarrisonAllGallicBuildings = function()
200         this.debugLog("Garrisoning all gallic buildings");
202         for (let buildingGarrison of gallicBuildingGarrison)
203                 for (let buildingClass of buildingGarrison.buildingClasses)
204                 {
205                         let unitCounts = TriggerHelper.SpawnAndGarrisonAtClasses(gaulPlayer, buildingClass, buildingGarrison.unitTemplates, 1);
206                         this.debugLog("Garrisoning at " + buildingClass + ": " + uneval(unitCounts));
207                 }
211  * Spawn units of the template at each gaia Civic Center and set them to defensive.
212  */
213 Trigger.prototype.SpawnInitialCCDefenders = function()
215         this.debugLog("To defend CCs, spawning " + uneval(ccDefenders));
217         for (let ent of this.civicCenters)
218                 for (let ccDefender of ccDefenders)
219                         for (let ent of TriggerHelper.SpawnUnits(ent, pickRandom(ccDefender.templates), ccDefender.count, gaulPlayer))
220                                 TriggerHelper.SetUnitStance(ent, "defensive");
223 Trigger.prototype.SpawnCCAttackers = function()
225         let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
227         let [spawnLeft, spawnRight] = this.GetActiveRiversides();
229         for (let gaiaCC of this.civicCenters)
230         {
231                 let isLeft = this.IsLeftRiverside(gaiaCC)
232                 if (isLeft && !spawnLeft || !isLeft && !spawnRight)
233                         continue;
235                 let templateCounts = TriggerHelper.BalancedTemplateComposition(this.GetAttackerComposition(time, false), ccAttackerCount(time));
236                 this.debugLog("Spawning civic center attackers at " + gaiaCC + ": " + uneval(templateCounts));
238                 let ccAttackers = [];
240                 for (let templateName in templateCounts)
241                 {
242                         let ents = TriggerHelper.SpawnUnits(gaiaCC, templateName, templateCounts[templateName], gaulPlayer);
244                         if (danubiusAttackerTemplates.heroes.indexOf(templateName) != -1 && ents[0])
245                                 this.heroes.add(ents[0]);
247                         ccAttackers = ccAttackers.concat(ents);
248                 }
250                 let patrolPointRef = isLeft ?
251                         triggerPointCCAttackerPatrolLeft :
252                         triggerPointCCAttackerPatrolRight;
254                 this.AttackAndPatrol(ccAttackers, unitTargetClass, patrolPointRef, "CCAttackers", false);
255         }
257         if (this.civicCenters.size)
258                 this.DoAfterDelay(ccAttackerInterval() * 60 * 1000, "SpawnCCAttackers", {});
262  * Remember most Humans present at the beginning of the match (before spawning any unit) and
263  * make them defensive.
264  */
265 Trigger.prototype.StartCelticRitual = function()
267         for (let ent of TriggerHelper.GetPlayerEntitiesByClass(gaulPlayer, "Human"))
268         {
269                 if (randBool(ritualProbability))
270                         this.ritualEnts.add(ent);
272                 TriggerHelper.SetUnitStance(ent, "defensive");
273         }
275         this.DoRepeatedly(5 * 1000, "UpdateCelticRitual", {});
279  * Play one of the given animations for most participants if and only if they are idle.
280  */
281 Trigger.prototype.UpdateCelticRitual = function()
283         for (let ent of this.ritualEnts)
284         {
285                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
286                 if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE")
287                         continue;
289                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
290                 if (!cmpIdentity)
291                         continue;
293                 let animations = ritualAnimations[
294                         cmpIdentity.HasClass("Healer") ? "healer" :
295                         cmpIdentity.HasClass("Female") ? "female" : "male"];
297                 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
298                 if (!cmpVisual)
299                         continue;
301                 if (animations.indexOf(cmpVisual.GetAnimationName()) == -1)
302                         cmpVisual.SelectAnimation(pickRandom(animations), false, 1, "");
303         }
307  * Spawn ships with a unique attacker composition each until
308  * the number of ships is reached that is supposed to exist at the given time.
309  */
310 Trigger.prototype.SpawnShips = function()
312         let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
313         let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
315         let shipSpawnCount = shipCount(time, numPlayers) - this.ships.size;
316         this.debugLog("Spawning " + shipSpawnCount + " ships");
318         while (this.ships.size < shipSpawnCount)
319                 this.ships.add(
320                         TriggerHelper.SpawnUnits(
321                                 pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)),
322                                 pickRandom(danubiusAttackerTemplates.ships),
323                                 1,
324                                 gaulPlayer)[0]);
326         for (let ship of this.ships)
327                 this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ship");
329         this.DoAfterDelay(shipRespawnTime(time) * 60 * 1000, "SpawnShips", {});
331         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
332         cmpTimer.CancelTimer(this.fillShipsTimer);
334         this.FillShips();
337 Trigger.prototype.GetAttackerComposition = function(time, siegeEngines)
339         let champRatio = championRatio(time);
340         return [
341                 {
342                         "templates": danubiusAttackerTemplates.heroes,
343                         "count": randBool(heroProbability(time)) ? 1 : 0,
344                         "unique_entities": Array.from(this.heroes)
345                 },
346                 {
347                         "templates": danubiusAttackerTemplates.siege,
348                         "count": siegeEngines ? siegeCount(time) : 0
349                 },
350                 {
351                         "templates": danubiusAttackerTemplates.healers,
352                         "frequency": healerRatio(time)
353                 },
354                 {
355                         "templates": danubiusAttackerTemplates.champions,
356                         "frequency": champRatio
357                 },
358                 {
359                         "templates": danubiusAttackerTemplates.citizen_soldiers,
360                         "frequency": 1 - champRatio
361                 }
362         ];
365 Trigger.prototype.FillShips = function()
367         let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
368         for (let ship of this.ships)
369         {
370                 let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
371                 if (!cmpGarrisonHolder)
372                         continue;
374                 let templateCounts = TriggerHelper.BalancedTemplateComposition(
375                         this.GetAttackerComposition(time, true),
376                         Math.max(0, attackersPerShip(time) - cmpGarrisonHolder.GetEntities().length));
378                 this.debugLog("Filling ship " + ship + " with " + uneval(templateCounts));
380                 for (let templateName in templateCounts)
381                 {
382                         let ents = TriggerHelper.SpawnGarrisonedUnits(ship, templateName, templateCounts[templateName], gaulPlayer);
383                         if (danubiusAttackerTemplates.heroes.indexOf(templateName) != -1 && ents[0])
384                                 this.heroes.add(ents[0]);
385                 }
386         }
388         this.fillShipsTimer = this.DoAfterDelay(shipFillInterval() * 60 * 1000, "FillShips", {});
392  * Attack the closest enemy ships around, then patrol the sea.
393  */
394 Trigger.prototype.AttackAndPatrol = function(attackers, targetClass, triggerPointRef, debugName, attack = true)
396         if (!attackers.length)
397                 return;
399         let targets = TriggerHelper.MatchEntitiesByClass(TriggerHelper.GetAllPlayersEntities(), targetClass).sort(
400                 (ent1, ent2) => DistanceBetweenEntities(attackers[0], ent1) - DistanceBetweenEntities(attackers[0], ent2)).slice(0, targetCount);
402         this.debugLog(debugName + " " + uneval(attackers) + " attack " + uneval(targets));
404         if (attack)
405                 for (let target of targets)
406                         ProcessCommand(gaulPlayer, {
407                                 "type": "attack",
408                                 "entities": attackers,
409                                 "target": target,
410                                 "queued": true,
411                                 "allowCapture": false
412                         });
414         let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount);
415         this.debugLog(debugName + " " + uneval(attackers) + " patrol to " + uneval(patrolTargets));
417         for (let patrolTarget of patrolTargets)
418         {
419                 let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D();
420                 ProcessCommand(gaulPlayer, {
421                         "type": "patrol",
422                         "entities": attackers,
423                         "x": pos.x,
424                         "z": pos.y,
425                         "targetClasses": {
426                                 "attack": [targetClass]
427                         },
428                         "queued": true,
429                         "allowCapture": false
430                 });
431         }
435  * To avoid unloading unlimited amounts of units on empty riversides,
436  * only add attackers to riversides where player buildings exist that are
437  * actually targeted.
438  */
439 Trigger.prototype.GetActiveRiversides = function()
441         let left = false;
442         let right = false;
444         for (let ent of TriggerHelper.GetAllPlayersEntitiesByClass(siegeTargetClass))
445         {
446                 if (this.IsLeftRiverside(ent))
447                         left = true;
448                 else
449                         right = true;
451                 if (left && right)
452                         break;
453         }
455         return [left, right];
458 Trigger.prototype.IsLeftRiverside = function(ent)
460         return this.riverDirection.cross(Vector2D.sub(Engine.QueryInterface(ent, IID_Position).GetPosition2D(), this.mapCenter)) > 0;
464  * Order all ships to abort naval warfare and move to the shoreline all few minutes.
465  */
466 Trigger.prototype.UngarrisonShipsOrder = function()
468         let [ungarrisonLeft, ungarrisonRight] = this.GetActiveRiversides();
469         if (!ungarrisonLeft && !ungarrisonRight)
470                 return;
472         // Determine which ships should ungarrison on which side of the river
473         let ships = Array.from(this.ships);
474         let shipsLeft = [];
475         let shipsRight = [];
477         if (ungarrisonLeft && ungarrisonRight)
478         {
479                 shipsLeft = shuffleArray(ships).slice(0, Math.round(ships.length / 2));
480                 shipsRight = ships.filter(ship => shipsLeft.indexOf(ship) == -1);
481         }
482         else if (ungarrisonLeft)
483                 shipsLeft = ships;
484         else if (ungarrisonRight)
485                 shipsRight = ships;
487         // Determine which ships should ungarrison and patrol at which trigger point names
488         let sides = [];
489         if (shipsLeft.length)
490                 sides.push({
491                         "ships": shipsLeft,
492                         "ungarrisonPointRef": triggerPointUngarrisonLeft,
493                         "landPointRef": triggerPointLandPatrolLeft
494                 });
496         if (shipsRight.length)
497                 sides.push({
498                         "ships": shipsRight,
499                         "ungarrisonPointRef": triggerPointUngarrisonRight,
500                         "landPointRef": triggerPointLandPatrolRight
501                 });
503         // Order those ships to move to a randomly chosen trigger point on the determined
504         // side of the river. Remember that chosen ungarrison point and the name of the
505         // trigger points where the ungarrisoned units should patrol afterwards.
506         for (let side of sides)
507                 for (let ship of side.ships)
508                 {
509                         let ungarrisonPoint = pickRandom(this.GetTriggerPoints(side.ungarrisonPointRef));
510                         let ungarrisonPos = Engine.QueryInterface(ungarrisonPoint, IID_Position).GetPosition2D();
512                         this.debugLog("Ship " + ship + " will ungarrison at " + side.ungarrisonPointRef +
513                                 " (" + ungarrisonPos.x + "," + ungarrisonPos.y + ")");
515                         Engine.QueryInterface(ship, IID_UnitAI).Walk(ungarrisonPos.x, ungarrisonPos.y, false);
516                         this.shipTarget[ship] = { "landPointRef": side.landPointRef, "ungarrisonPoint": ungarrisonPoint };
517                 }
519         this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
523  * Check frequently whether the ships are close enough to unload at the shoreline.
524  */
525 Trigger.prototype.CheckShipRange = function()
527         for (let ship of this.ships)
528         {
529                 if (!this.shipTarget[ship] || DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance)
530                         continue;
532                 let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
533                 if (!cmpGarrisonHolder)
534                         continue;
536                 let attackers = cmpGarrisonHolder.GetEntities();
537                 let siegeEngines = attackers.filter(ent => Engine.QueryInterface(ent, IID_Identity).HasClass("Siege"));
538                 let others = attackers.filter(ent => siegeEngines.indexOf(ent) == -1);
540                 this.debugLog("Ungarrisoning ship " + ship + " at " + uneval(this.shipTarget[ship]));
541                 cmpGarrisonHolder.UnloadAll();
543                 if (randBool(formationProbability))
544                         ProcessCommand(gaulPlayer, {
545                                 "type": "formation",
546                                 "entities": others,
547                                 "name": "special/formations/" + pickRandom(unitFormations)
548                         });
550                 this.AttackAndPatrol(siegeEngines, siegeTargetClass, this.shipTarget[ship].landPointRef, "Siege");
551                 this.AttackAndPatrol(others, unitTargetClass, this.shipTarget[ship].landPointRef, "Units");
552                 delete this.shipTarget[ship];
554                 this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships");
555         }
558 Trigger.prototype.DanubiusOwnershipChange = function(data)
560         if (data.from != 0)
561                 return;
563         if (this.heroes.delete(data.entity))
564                 this.debugLog("Hero " + data.entity + " died");
566         if (this.ships.delete(data.entity))
567                 this.debugLog("Ship " + data.entity + " sunk");
569         if (this.civicCenters.delete(data.entity))
570                 this.debugLog("Gaia civic center " + data.entity + " destroyed or captured");
572         this.ritualEnts.delete(data.entity);
575 Trigger.prototype.InitDanubius = function()
577         // Set a custom animation of idle ritual units frequently
578         this.ritualEnts = new Set();
580         // To prevent spawning more than the limits, track IDs of current entities
581         this.ships = new Set();
582         this.heroes = new Set();
584         // Remember gaia CCs to spawn attackers from
585         this.civicCenters = new Set(TriggerHelper.GetPlayerEntitiesByClass(gaulPlayer, "CivCentre"))
587         // Maps from gaia ship entity ID to ungarrison trigger point entity ID and land patrol triggerpoint name
588         this.shipTarget = {};
589         this.fillShipsTimer = undefined;
591         // Be able to distinguish between the left and right riverside
592         let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize();
593         this.mapCenter = new Vector2D(mapSize / 2, mapSize / 2);
594         this.riverDirection = Vector2D.sub(
595                 Engine.QueryInterface(this.GetTriggerPoints(triggerPointRiverDirection)[0], IID_Position).GetPosition2D(),
596                 this.mapCenter);
598         this.StartCelticRitual();
599         this.GarrisonAllGallicBuildings();
600         this.SpawnInitialCCDefenders();
601         this.SpawnCCAttackers();
603         this.SpawnShips();
604         this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
605         this.DoRepeatedly(5 * 1000, "CheckShipRange", {});
609         let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
610         cmpTrigger.RegisterTrigger("OnInitGame", "InitDanubius", { "enabled": true });
611         cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DanubiusOwnershipChange", { "enabled": true });