Unify Caledonian Meadows and Wild Lake player location duplication from rP19704 ...
[0ad.git] / binaries / data / mods / public / maps / random / danubius_triggers.js
blobaaa6849f2db811a166595c8a1eb4043c55d526f2
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 var shipTemplate = "gaul_ship_trireme";
16 var siegeTemplate = "gaul_mechanical_siege_ram";
18 var heroTemplates = [
19         "gaul_hero_viridomarus",
20         "gaul_hero_vercingetorix",
21         "gaul_hero_brennus"
24 var femaleTemplate = "gaul_support_female_citizen";
25 var healerTemplate = "gaul_support_healer_b";
27 var citizenInfantryTemplates = [
28         "gaul_infantry_javelinist_b",
29         "gaul_infantry_spearman_b",
30         "gaul_infantry_slinger_b"
33 var citizenCavalryTemplates = [
34         "gaul_cavalry_javelinist_b",
35         "gaul_cavalry_swordsman_b"
38 var citizenTemplates = [...citizenInfantryTemplates, ...citizenCavalryTemplates];
40 var championInfantryTemplates = [
41         "gaul_champion_fanatic",
42         "gaul_champion_infantry"
45 var championCavalryTemplates = [
46         "gaul_champion_cavalry"
49 var championTemplates = [...championInfantryTemplates, ...championCavalryTemplates];
51 var ccDefenders = [
52         { "count": 8, "template": "units/" + pickRandom(citizenInfantryTemplates) },
53         { "count": 8, "template": "units/" + pickRandom(championInfantryTemplates) },
54         { "count": 4, "template": "units/" + pickRandom(championCavalryTemplates) },
55         { "count": 4, "template": "units/" + healerTemplate },
56         { "count": 5, "template": "units/" + femaleTemplate },
57         { "count": 10, "template": "gaia/fauna_sheep" }
60 var gallicBuildingGarrison = [
61         {
62                 "buildings": ["House"],
63                 "units": [femaleTemplate, healerTemplate]
64         },
65         {
66                 "buildings": ["CivCentre", "Temple"],
67                 "units": championTemplates
68         },
69         {
70                 "buildings": ["DefenseTower", "Outpost"],
71                 "units": championInfantryTemplates
72         }
75 /**
76  * Notice if gaia becomes too strong, players will just turtle and try to outlast the players on the other side.
77  * However we want interaction and fights between the teams.
78  * This can be accomplished by not wiping out players buildings entirely.
79  */
81 /**
82  * Time in minutes between two consecutive waves spawned from the gaia civic centers, if they still exist.
83  */
84 var ccAttackerInterval = t => randFloat(6, 8);
86 /**
87  * Number of attackers spawned at a civic center at t minutes ingame time.
88  */
89 var ccAttackerCount = t => Math.min(20, Math.max(0, Math.round(t * 1.5)));
91 /**
92  * Time between two consecutive waves.
93  */
94 var shipRespawnTime = () => randFloat(8, 10);
96 /**
97  * Limit of ships on the map when spawning them.
98  * Have at least two ships, so that both sides will be visited.
99  */
100 var shipCount = (t, numPlayers) => Math.max(2, Math.round(Math.min(1.5, t / 10) * numPlayers));
103  * Order all ships to ungarrison at the shoreline.
104  */
105 var shipUngarrisonInterval = () => randFloat(5, 7);
108  * Time between refillings of all ships with new soldiers.
109  */
110 var shipFillInterval = () => randFloat(4, 5);
113  * Total count of gaia attackers per shipload.
114  */
115 var attackersPerShip = t => Math.min(30, Math.round(t * 2));
118  * Likelihood of adding a non-existing hero at t minutes.
119  */
120 var heroProbability = t => Math.max(0, Math.min(1, (t - 25) / 60));
123  * Percent of healers to add per shipload after potentially adding a hero and siege engines.
124  */
125 var healerRatio = t => randFloat(0, 0.1);
128  * Percent of siege engines to add per shipload.
129  */
130 var siegeRatio = t => t < 8 ? 0 : randFloat(0.03, 0.06);
133  * Percent of champions to be added after spawning heroes, healers and siege engines.
134  * Rest will be citizen soldiers.
135  */
136 var championRatio = t => Math.min(1, Math.max(0, (t - 25) / 75));
139  * Ships and land units will queue attack orders for this amount of closest units.
140  */
141 var targetCount = 3;
144  * Number of trigger points to patrol when not having enemies to attack.
145  */
146 var patrolCount = 5;
149  * Which units ships should focus when attacking and patrolling.
150  */
151 var shipTargetClass = "Warship";
154  * Which entities siege engines should focus when attacking and patrolling.
155  */
156 var siegeTargetClass = "Defensive";
159  * Which entities units should focus when attacking and patrolling.
160  */
161 var unitTargetClass = "Unit+!Ship";
164  * Ungarrison ships when being in this range of the target.
165  */
166 var shipUngarrisonDistance = 50;
169  * Currently formations are not working properly and enemies in vision range are often ignored.
170  * So only have a small chance of using formations.
171  */
172 var formationProbability = 0.2;
174 var unitFormations = [
175         "box",
176         "battle_line",
177         "line_closed",
178         "column_closed"
182  * Chance for the units at the meeting place to participate in the ritual.
183  */
184 var ritualProbability = 0.75;
187  * Units celebrating at the meeting place will perform one of these animations
188  * if idle and switch back when becoming idle again.
189  */
190 var ritualAnimations = {
191         "female": ["attack_slaughter"],
192         "male": ["attack_capture", "promotion", "attack_slaughter"],
193         "healer": ["attack_capture", "promotion", "heal"]
196 var triggerPointShipSpawn = "A";
197 var triggerPointShipPatrol = "B";
198 var triggerPointUngarrisonLeft = "C";
199 var triggerPointUngarrisonRight = "D";
200 var triggerPointLandPatrolLeft = "E";
201 var triggerPointLandPatrolRight = "F";
202 var triggerPointCCAttackerPatrolLeft = "G";
203 var triggerPointCCAttackerPatrolRight = "H";
206  * Which playerID to use for the opposing gallic reinforcements.
207  */
208 var gaulPlayer = 0;
210 Trigger.prototype.debugLog = function(txt)
212         if (showDebugLog)
213                 print(
214                         "DEBUG [" +
215                         Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n");
219  * Return a random amount of these templates whose sum is count.
220  */
221 Trigger.prototype.RandomAttackerTemplates = function(templates, count)
223         let ratios = new Array(templates.length).fill(1).map(i => randFloat(0, 1));
224         let ratioSum = ratios.reduce((current, sum) => current + sum, 0);
226         let remainder = count;
227         let templateCounts = {};
229         for (let i in templates)
230         {
231                 let currentCount = +i == templates.length - 1 ? remainder : Math.round(ratios[i] / ratioSum * count);
232                 if (!currentCount)
233                         continue;
235                 templateCounts[templates[i]] = currentCount;
236                 remainder -= currentCount;
237         }
239         if (remainder != 0)
240                 warn("Not as many templates as expected: " + count + " vs " + uneval(templateCounts));
242         return templateCounts;
245 Trigger.prototype.GarrisonAllGallicBuildings = function(gaiaEnts)
247         this.debugLog("Garrisoning all gallic buildings");
249         for (let buildingGarrison of gallicBuildingGarrison)
250                 for (let building of buildingGarrison.buildings)
251                         this.SpawnAndGarrisonBuilding(gaiaEnts, building, buildingGarrison.units);
255  * Garrisons all targetEnts that match the targetClass with newly spawned entities of the given template.
256  */
257 Trigger.prototype.SpawnAndGarrisonBuilding = function(gaiaEnts, targetClass, templates)
259         for (let gaiaEnt of gaiaEnts)
260         {
261                 let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity);
262                 if (!cmpIdentity || !cmpIdentity.HasClass(targetClass))
263                         continue;
265                 let cmpGarrisonHolder = Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder);
266                 if (!cmpGarrisonHolder)
267                         continue;
269                 let unitCounts = this.RandomAttackerTemplates(templates, cmpGarrisonHolder.GetCapacity());
270                 this.debugLog("Garrisoning " + uneval(unitCounts) + " at " + targetClass);
272                 for (let template in unitCounts)
273                         for (let newEnt of TriggerHelper.SpawnUnits(gaiaEnt, "units/" + template, unitCounts[template], gaulPlayer))
274                                 Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder).Garrison(newEnt);
275         }
279  * Spawn units of the template at each gaia Civic Center and set them to defensive.
280  */
281 Trigger.prototype.SpawnInitialCCDefenders = function(gaiaEnts)
283         this.debugLog("To defend CCs, spawning " + uneval(ccDefenders));
285         for (let gaiaEnt of gaiaEnts)
286         {
287                 let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity);
288                 if (!cmpIdentity || !cmpIdentity.HasClass("CivCentre"))
289                         continue;
291                 this.civicCenters.push(gaiaEnt);
293                 for (let ccDefender of ccDefenders)
294                         for (let ent of TriggerHelper.SpawnUnits(gaiaEnt, ccDefender.template, ccDefender.count, gaulPlayer))
295                                 Engine.QueryInterface(ent, IID_UnitAI).SwitchToStance("defensive");
296         }
299 Trigger.prototype.SpawnCCAttackers = function()
301         let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
302         let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize();
304         let [spawnLeft, spawnRight] = this.GetActiveRiversides();
306         for (let gaiaCC of this.civicCenters)
307         {
308                 let isLeft = Engine.QueryInterface(gaiaCC, IID_Position).GetPosition2D().x < mapSize / 2;
309                 if (isLeft && !spawnLeft || !isLeft && !spawnRight)
310                         continue;
312                 let toSpawn = this.GetAttackerComposition(ccAttackerCount(time), false);
313                 this.debugLog("Spawning civic center attackers at " + gaiaCC + ": " + uneval(toSpawn));
315                 let ccAttackers = [];
317                 for (let spawn of toSpawn)
318                 {
319                         let ents = TriggerHelper.SpawnUnits(gaiaCC, "units/" + spawn.template, spawn.count, gaulPlayer);
321                         if (spawn.hero && ents[0])
322                                 this.heroes.push({ "template": spawn.template, "ent": ents[0] });
324                         ccAttackers = ccAttackers.concat(ents);
325                 }
327                 let patrolPointRef = isLeft ?
328                         triggerPointCCAttackerPatrolLeft :
329                         triggerPointCCAttackerPatrolRight;
331                 this.AttackAndPatrol(ccAttackers, unitTargetClass, patrolPointRef, "CCAttackers", false);
332         }
334         if (this.civicCenters.length)
335                 this.DoAfterDelay(ccAttackerInterval() * 60 * 1000, "SpawnCCAttackers", {});
339  * Remember most Humans present at the beginning of the match (before spawning any unit) and
340  * make them defensive.
341  */
342 Trigger.prototype.StartCelticRitual = function(gaiaEnts)
344         for (let ent of gaiaEnts)
345         {
346                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
347                 if (!cmpIdentity || !cmpIdentity.HasClass("Human"))
348                         continue;
350                 if (randBool(ritualProbability))
351                         this.ritualEnts.push(ent);
353                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
354                 if (cmpUnitAI)
355                         cmpUnitAI.SwitchToStance("defensive");
356         }
358         this.DoRepeatedly(5 * 1000, "UpdateCelticRitual", {});
362  * Play one of the given animations for most participants if and only if they are idle.
363  */
364 Trigger.prototype.UpdateCelticRitual = function()
366         for (let ent of this.ritualEnts)
367         {
368                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
369                 if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE")
370                         continue;
372                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
373                 if (!cmpIdentity)
374                         continue;
376                 let animations = ritualAnimations[
377                         cmpIdentity.HasClass("Healer") ? "healer" :
378                         cmpIdentity.HasClass("Female") ? "female" : "male"];
380                 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
381                 if (!cmpVisual)
382                         continue;
384                 if (animations.indexOf(cmpVisual.GetAnimationName()) == -1)
385                         cmpVisual.SelectAnimation(pickRandom(animations), false, 1, "");
386         }
390  * Spawn ships with a unique attacker composition each until
391  * the number of ships is reached that is supposed to exist at the given time.
392  */
393 Trigger.prototype.SpawnShips = function()
395         let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
396         let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
398         let shipSpawnCount = shipCount(time, numPlayers) - this.ships.length;
399         this.debugLog("Spawning " + shipSpawnCount + " ships");
401         while (this.ships.length < shipSpawnCount)
402                 this.ships.push(TriggerHelper.SpawnUnits(pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)), "units/" + shipTemplate, 1, gaulPlayer)[0]);
404         for (let ship of this.ships)
405                 this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships");
407         this.DoAfterDelay(shipRespawnTime(time) * 60 * 1000, "SpawnShips", {});
409         let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
410         cmpTimer.CancelTimer(this.fillShipsTimer);
412         this.FillShips();
415 Trigger.prototype.GetAttackerComposition = function(attackerCount, addSiege)
417         let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
418         let toSpawn = [];
419         let remainder = attackerCount;
421         let siegeCount = addSiege ? Math.round(siegeRatio(time) * remainder) : 0;
422         if (siegeCount)
423                 toSpawn.push({ "template": siegeTemplate, "count": siegeCount });
424         remainder -= siegeCount;
426         let heroTemplate = pickRandom(heroTemplates.filter(hTemp => this.heroes.every(hero => hTemp != hero.template)));
427         if (heroTemplate && remainder && randBool(heroProbability(time)))
428         {
429                 toSpawn.push({ "template": heroTemplate, "count": 1, "hero": true });
430                 --remainder;
431         }
433         let healerCount = Math.round(healerRatio(time) * remainder);
434         if (healerCount)
435                 toSpawn.push({ "template": healerTemplate, "count": healerCount });
436         remainder -= healerCount;
438         let championCount = Math.round(championRatio(time) * remainder);
439         let championTemplateCounts = this.RandomAttackerTemplates(championTemplates, championCount);
440         for (let template in championTemplateCounts)
441         {
442                 let count = championTemplateCounts[template];
443                 toSpawn.push({ "template": template, "count": count });
444                 championCount -= count;
445                 remainder -= count;
446         }
448         let citizenTemplateCounts = this.RandomAttackerTemplates(citizenTemplates, remainder);
449         for (let template in citizenTemplateCounts)
450         {
451                 let count = citizenTemplateCounts[template];
452                 toSpawn.push({ "template": template, "count": count });
453                 remainder -= count;
454         }
456         if (remainder != 0)
457                 warn("Didn't spawn as many attackers as were intended (" + remainder + " remaining)");
459         return toSpawn;
462 Trigger.prototype.FillShips = function()
464         let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
465         for (let ship of this.ships)
466         {
467                 let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
468                 if (!cmpGarrisonHolder)
469                         continue;
471                 let toSpawn = this.GetAttackerComposition(Math.max(0, attackersPerShip(time) - cmpGarrisonHolder.GetEntities().length), true);
472                 this.debugLog("Filling ship " + ship + " with " + uneval(toSpawn));
474                 for (let spawn of toSpawn)
475                 {
476                         // Don't use TriggerHelper.SpawnUnits here because that is too slow,
477                         // needlessly trying all spawn points near the ships footprint which all fail
479                         for (let i = 0; i < spawn.count; ++i)
480                         {
481                                 let ent = Engine.AddEntity("units/" + spawn.template);
482                                 Engine.QueryInterface(ent, IID_Ownership).SetOwner(gaulPlayer);
484                                 if (spawn.hero)
485                                         this.heroes.push({ "template": spawn.template, "ent": ent });
487                                 cmpGarrisonHolder.Garrison(ent);
488                         }
489                 }
490         }
492         this.fillShipsTimer = this.DoAfterDelay(shipFillInterval() * 60 * 1000, "FillShips", {});
496  * Attack the closest enemy ships around, then patrol the sea.
497  */
498 Trigger.prototype.AttackAndPatrol = function(attackers, targetClass, triggerPointRef, debugName, attack = true)
500         if (!attackers.length)
501                 return;
503         let allTargets = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities().filter(ent => {
504                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
505                 return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClass);
506         });
508         let targets = allTargets.sort((ent1, ent2) =>
509                 DistanceBetweenEntities(attackers[0], ent1) - DistanceBetweenEntities(attackers[0], ent2)).slice(0, targetCount);
511         this.debugLog(debugName + " " + uneval(attackers) + " attack " + uneval(targets));
513         if (attack)
514                 for (let target of targets)
515                         ProcessCommand(gaulPlayer, {
516                                 "type": "attack",
517                                 "entities": attackers,
518                                 "target": target,
519                                 "queued": true,
520                                 "allowCapture": false
521                         });
523         let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount);
524         this.debugLog(debugName + " " + uneval(attackers) + " patrol to " + uneval(patrolTargets));
526         for (let patrolTarget of patrolTargets)
527         {
528                 let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D();
529                 ProcessCommand(gaulPlayer, {
530                         "type": "patrol",
531                         "entities": attackers,
532                         "x": pos.x,
533                         "z": pos.y,
534                         "targetClasses": {
535                                 "attack": [targetClass]
536                         },
537                         "queued": true,
538                         "allowCapture": false
539                 });
540         }
544  * To avoid unloading unlimited amounts of units on empty riversides,
545  * only add attackers to riversides where player buildings exist that are
546  * actually targeted.
547  */
548 Trigger.prototype.GetActiveRiversides = function()
550         let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize();
552         let left = false;
553         let right = false;
555         for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities())
556         {
557                 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
558                 if (!cmpIdentity || !cmpIdentity.HasClass(siegeTargetClass))
559                         continue;
561                 if (Engine.QueryInterface(ent, IID_Position).GetPosition2D().x < mapSize / 2)
562                         left = true;
563                 else
564                         right = true;
566                 if (left && right)
567                         break;
568         }
570         return [left, right];
574  * Order all ships to abort naval warfare and move to the shoreline all few minutes.
575  */
576 Trigger.prototype.UngarrisonShipsOrder = function()
578         let [ungarrisonLeft, ungarrisonRight] = this.GetActiveRiversides();
579         if (!ungarrisonLeft && !ungarrisonRight)
580                 return;
582         // Determine which ships should ungarrison on which side of the river
583         let shipsLeft = [];
584         let shipsRight = [];
586         if (ungarrisonLeft && ungarrisonRight)
587         {
588                 shipsLeft = shuffleArray(this.ships).slice(0, Math.round(this.ships.length / 2));
589                 shipsRight = this.ships.filter(ship => shipsLeft.indexOf(ship) == -1);
590         }
591         else if (ungarrisonLeft)
592                 shipsLeft = this.ships;
593         else if (ungarrisonRight)
594                 shipsRight = this.ships;
596         // Determine which ships should ungarrison and patrol at which trigger point names
597         let sides = [];
598         if (shipsLeft.length)
599                 sides.push({
600                         "ships": shipsLeft,
601                         "ungarrisonPointRef": triggerPointUngarrisonLeft,
602                         "landPointRef": triggerPointLandPatrolLeft
603                 });
605         if (shipsRight.length)
606                 sides.push({
607                         "ships": shipsRight,
608                         "ungarrisonPointRef": triggerPointUngarrisonRight,
609                         "landPointRef": triggerPointLandPatrolRight
610                 });
612         // Order those ships to move to a randomly chosen trigger point on the determined
613         // side of the river. Remember that chosen ungarrison point and the name of the
614         // trigger points where the ungarrisoned units should patrol afterwards.
615         for (let side of sides)
616                 for (let ship of side.ships)
617                 {
618                         let ungarrisonPoint = pickRandom(this.GetTriggerPoints(side.ungarrisonPointRef));
619                         let ungarrisonPos = Engine.QueryInterface(ungarrisonPoint, IID_Position).GetPosition2D();
621                         this.debugLog("Ship " + ship + " will ungarrison at " + side.ungarrisonPointRef +
622                                 " (" + ungarrisonPos.x + "," + ungarrisonPos.y + ")");
624                         Engine.QueryInterface(ship, IID_UnitAI).Walk(ungarrisonPos.x, ungarrisonPos.y, false);
625                         this.shipTarget[ship] = { "landPointRef": side.landPointRef, "ungarrisonPoint": ungarrisonPoint };
626                 }
628         this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
632  * Check frequently whether the ships are close enough to unload at the shoreline.
633  */
634 Trigger.prototype.CheckShipRange = function()
636         for (let ship of this.ships)
637         {
638                 if (!this.shipTarget[ship] || DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance)
639                         continue;
641                 let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
642                 if (!cmpGarrisonHolder)
643                         continue;
645                 let attackers = cmpGarrisonHolder.GetEntities();
646                 let siegeEngines = attackers.filter(ent => Engine.QueryInterface(ent, IID_Identity).HasClass("Siege"));
647                 let others = attackers.filter(ent => siegeEngines.indexOf(ent) == -1);
649                 this.debugLog("Ungarrisoning ship " + ship + " at " + uneval(this.shipTarget[ship]));
650                 cmpGarrisonHolder.UnloadAll();
652                 if (randBool(formationProbability))
653                         ProcessCommand(gaulPlayer, {
654                                 "type": "formation",
655                                 "entities": others,
656                                 "name": "special/formations/" + pickRandom(unitFormations)
657                         });
659                 this.AttackAndPatrol(siegeEngines, siegeTargetClass, this.shipTarget[ship].landPointRef, "Siege");
660                 this.AttackAndPatrol(others, unitTargetClass, this.shipTarget[ship].landPointRef, "Units");
661                 delete this.shipTarget[ship];
663                 this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships");
664         }
667 Trigger.prototype.DanubiusOwnershipChange = function(data)
669         if (data.from != 0)
670                 return;
672         let shipIdx = this.ships.indexOf(data.entity);
673         if (shipIdx != -1)
674         {
675                 this.debugLog("Ship " + data.entity + " sunk");
676                 this.ships.splice(shipIdx, 1);
677         }
679         let ritualIdx = this.ritualEnts.indexOf(data.entity);
680         if (ritualIdx != -1)
681                 this.ritualEnts.splice(ritualIdx, 1);
683         let heroIdx = this.heroes.findIndex(hero => hero.ent == data.entity);
684         if (ritualIdx != -1)
685                 this.heroes.splice(heroIdx, 1);
687         let ccIdx = this.civicCenters.indexOf(data.entity);
688         if (ccIdx != -1)
689         {
690                 this.debugLog("Gaia civic center " + data.entity + " destroyed or captured");
691                 this.civicCenters.splice(ccIdx, 1);
692         }
697         let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
699         let gaiaEnts = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0);
701         cmpTrigger.ritualEnts = [];
703         // To prevent spawning more than the limits, track IDs of current entities
704         cmpTrigger.ships = [];
705         cmpTrigger.heroes = [];
707         // Remember gaia CCs to spawn attackers from
708         cmpTrigger.civicCenters = [];
710         // Maps from gaia ship entity ID to ungarrison trigger point entity ID and land patrol triggerpoint name
711         cmpTrigger.shipTarget = {};
712         cmpTrigger.fillShipsTimer = undefined;
714         cmpTrigger.StartCelticRitual(gaiaEnts);
715         cmpTrigger.GarrisonAllGallicBuildings(gaiaEnts);
716         cmpTrigger.SpawnInitialCCDefenders(gaiaEnts);
717         cmpTrigger.SpawnCCAttackers(gaiaEnts);
719         cmpTrigger.SpawnShips();
720         cmpTrigger.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
721         cmpTrigger.DoRepeatedly(5 * 1000, "CheckShipRange", {});
723         cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DanubiusOwnershipChange", { "enabled": true });