On Danubius, send soldier attack commands last, so that observers follow these units...
[0ad.git] / binaries / data / mods / public / maps / random / danubius_triggers.js
blobab392373c84949f53516e674b26233cea2ccff5a
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         "special/formations/box",
155         "special/formations/battle_line",
156         "special/formations/line_closed",
157         "special/formations/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", true);
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(entities, targetClass, triggerPointRef, debugName, attack)
396         if (!entities.length)
397                 return;
399         let healers = TriggerHelper.MatchEntitiesByClass(entities, "Healer");
400         if (healers.length)
401         {
402                 let healerTargets = TriggerHelper.MatchEntitiesByClass(entities, "Hero Champion");
403                 if (!healerTargets.length)
404                         healerTargets = TriggerHelper.MatchEntitiesByClass(entities, "Soldier");
406                 ProcessCommand(gaulPlayer, {
407                         "type": "guard",
408                         "entities": healers,
409                         "target": pickRandom(healerTargets),
410                         "queued": false
411                 });
412         }
414         let attackers = TriggerHelper.MatchEntitiesByClass(entities, "!Healer");
416         let targets = TriggerHelper.MatchEntitiesByClass(TriggerHelper.GetAllPlayersEntities(), targetClass).sort(
417                 (ent1, ent2) => DistanceBetweenEntities(attackers[0], ent1) - DistanceBetweenEntities(attackers[0], ent2)).slice(0, targetCount);
419         this.debugLog(debugName + " " + uneval(attackers) + " attack " + uneval(targets));
421         if (attack)
422                 for (let target of targets)
423                         ProcessCommand(gaulPlayer, {
424                                 "type": "attack",
425                                 "entities": attackers,
426                                 "target": target,
427                                 "queued": true,
428                                 "allowCapture": false
429                         });
431         let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount);
432         this.debugLog(debugName + " " + uneval(attackers) + " patrol to " + uneval(patrolTargets));
434         for (let patrolTarget of patrolTargets)
435         {
436                 let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D();
437                 ProcessCommand(gaulPlayer, {
438                         "type": "patrol",
439                         "entities": attackers,
440                         "x": pos.x,
441                         "z": pos.y,
442                         "targetClasses": {
443                                 "attack": [targetClass]
444                         },
445                         "queued": true,
446                         "allowCapture": false
447                 });
448         }
452  * To avoid unloading unlimited amounts of units on empty riversides,
453  * only add attackers to riversides where player buildings exist that are
454  * actually targeted.
455  */
456 Trigger.prototype.GetActiveRiversides = function()
458         let left = false;
459         let right = false;
461         for (let ent of TriggerHelper.GetAllPlayersEntitiesByClass(siegeTargetClass))
462         {
463                 if (this.IsLeftRiverside(ent))
464                         left = true;
465                 else
466                         right = true;
468                 if (left && right)
469                         break;
470         }
472         return [left, right];
475 Trigger.prototype.IsLeftRiverside = function(ent)
477         return this.riverDirection.cross(Vector2D.sub(Engine.QueryInterface(ent, IID_Position).GetPosition2D(), this.mapCenter)) > 0;
481  * Order all ships to abort naval warfare and move to the shoreline all few minutes.
482  */
483 Trigger.prototype.UngarrisonShipsOrder = function()
485         let [ungarrisonLeft, ungarrisonRight] = this.GetActiveRiversides();
486         if (!ungarrisonLeft && !ungarrisonRight)
487                 return;
489         // Determine which ships should ungarrison on which side of the river
490         let ships = Array.from(this.ships);
491         let shipsLeft = [];
492         let shipsRight = [];
494         if (ungarrisonLeft && ungarrisonRight)
495         {
496                 shipsLeft = shuffleArray(ships).slice(0, Math.round(ships.length / 2));
497                 shipsRight = ships.filter(ship => shipsLeft.indexOf(ship) == -1);
498         }
499         else if (ungarrisonLeft)
500                 shipsLeft = ships;
501         else if (ungarrisonRight)
502                 shipsRight = ships;
504         // Determine which ships should ungarrison and patrol at which trigger point names
505         let sides = [];
506         if (shipsLeft.length)
507                 sides.push({
508                         "ships": shipsLeft,
509                         "ungarrisonPointRef": triggerPointUngarrisonLeft,
510                         "landPointRef": triggerPointLandPatrolLeft
511                 });
513         if (shipsRight.length)
514                 sides.push({
515                         "ships": shipsRight,
516                         "ungarrisonPointRef": triggerPointUngarrisonRight,
517                         "landPointRef": triggerPointLandPatrolRight
518                 });
520         // Order those ships to move to a randomly chosen trigger point on the determined
521         // side of the river. Remember that chosen ungarrison point and the name of the
522         // trigger points where the ungarrisoned units should patrol afterwards.
523         for (let side of sides)
524                 for (let ship of side.ships)
525                 {
526                         let ungarrisonPoint = pickRandom(this.GetTriggerPoints(side.ungarrisonPointRef));
527                         let ungarrisonPos = Engine.QueryInterface(ungarrisonPoint, IID_Position).GetPosition2D();
529                         this.debugLog("Ship " + ship + " will ungarrison at " + side.ungarrisonPointRef +
530                                 " (" + ungarrisonPos.x + "," + ungarrisonPos.y + ")");
532                         Engine.QueryInterface(ship, IID_UnitAI).Walk(ungarrisonPos.x, ungarrisonPos.y, false);
533                         this.shipTarget[ship] = { "landPointRef": side.landPointRef, "ungarrisonPoint": ungarrisonPoint };
534                 }
536         this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
540  * Check frequently whether the ships are close enough to unload at the shoreline.
541  */
542 Trigger.prototype.CheckShipRange = function()
544         for (let ship of this.ships)
545         {
546                 if (!this.shipTarget[ship] || DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance)
547                         continue;
549                 let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
550                 if (!cmpGarrisonHolder)
551                         continue;
553                 let humans = TriggerHelper.MatchEntitiesByClass(cmpGarrisonHolder.GetEntities(), "Human");
554                 let siegeEngines = TriggerHelper.MatchEntitiesByClass(cmpGarrisonHolder.GetEntities(), "Siege");
556                 this.debugLog("Ungarrisoning ship " + ship + " at " + uneval(this.shipTarget[ship]));
557                 cmpGarrisonHolder.UnloadAll();
558                 this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships", true);
560                 if (randBool(formationProbability))
561                         ProcessCommand(gaulPlayer, {
562                                 "type": "formation",
563                                 "entities": humans,
564                                 "name": pickRandom(unitFormations)
565                         });
567                 this.AttackAndPatrol(siegeEngines, siegeTargetClass, this.shipTarget[ship].landPointRef, "Siege Engines", true);
569                 // Order soldiers at last, so the follow-player observer feature focuses the soldiers
570                 this.AttackAndPatrol(humans, unitTargetClass, this.shipTarget[ship].landPointRef, "Units", true);
572                 delete this.shipTarget[ship];
573         }
576 Trigger.prototype.DanubiusOwnershipChange = function(data)
578         if (data.from != 0)
579                 return;
581         if (this.heroes.delete(data.entity))
582                 this.debugLog("Hero " + data.entity + " died");
584         if (this.ships.delete(data.entity))
585                 this.debugLog("Ship " + data.entity + " sunk");
587         if (this.civicCenters.delete(data.entity))
588                 this.debugLog("Gaia civic center " + data.entity + " destroyed or captured");
590         this.ritualEnts.delete(data.entity);
593 Trigger.prototype.InitDanubius = function()
595         // Set a custom animation of idle ritual units frequently
596         this.ritualEnts = new Set();
598         // To prevent spawning more than the limits, track IDs of current entities
599         this.ships = new Set();
600         this.heroes = new Set();
602         // Remember gaia CCs to spawn attackers from
603         this.civicCenters = new Set(TriggerHelper.GetPlayerEntitiesByClass(gaulPlayer, "CivCentre"))
605         // Maps from gaia ship entity ID to ungarrison trigger point entity ID and land patrol triggerpoint name
606         this.shipTarget = {};
607         this.fillShipsTimer = undefined;
609         // Be able to distinguish between the left and right riverside
610         let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize();
611         this.mapCenter = new Vector2D(mapSize / 2, mapSize / 2);
612         this.riverDirection = Vector2D.sub(
613                 Engine.QueryInterface(this.GetTriggerPoints(triggerPointRiverDirection)[0], IID_Position).GetPosition2D(),
614                 this.mapCenter);
616         this.StartCelticRitual();
617         this.GarrisonAllGallicBuildings();
618         this.SpawnInitialCCDefenders();
619         this.SpawnCCAttackers();
621         this.SpawnShips();
622         this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
623         this.DoRepeatedly(5 * 1000, "CheckShipRange", {});
627         let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
628         cmpTrigger.RegisterTrigger("OnInitGame", "InitDanubius", { "enabled": true });
629         cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DanubiusOwnershipChange", { "enabled": true });