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),
24 // Excludes the Vercingetorix variant
25 "units/gaul_hero_viridomarus",
26 "units/gaul_hero_vercingetorix",
27 "units/gaul_hero_brennus"
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 = [
41 "buildingClasses": ["House"],
42 "unitTemplates": danubiusAttackerTemplates.females.concat(danubiusAttackerTemplates.healers)
45 "buildingClasses": ["CivCentre", "Temple"],
46 "unitTemplates": danubiusAttackerTemplates.champions,
49 "buildingClasses": ["DefenseTower", "Outpost"],
50 "unitTemplates": danubiusAttackerTemplates.champion_infantry
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.
61 * Time in minutes between two consecutive waves spawned from the gaia civic centers, if they still exist.
63 var ccAttackerInterval = t => randFloat(6, 8);
66 * Number of attackers spawned at a civic center at t minutes ingame time.
68 var ccAttackerCount = t => Math.min(20, Math.max(0, Math.round(t * 1.5)));
71 * Time between two consecutive waves.
73 var shipRespawnTime = () => randFloat(8, 10);
76 * Limit of ships on the map when spawning them.
77 * Have at least two ships, so that both sides will be visited.
79 var shipCount = (t, numPlayers) => Math.max(2, Math.round(Math.min(1.5, t / 10) * numPlayers));
82 * Order all ships to ungarrison at the shoreline.
84 var shipUngarrisonInterval = () => randFloat(5, 7);
87 * Time between refillings of all ships with new soldiers.
89 var shipFillInterval = () => randFloat(4, 5);
92 * Total count of gaia attackers per shipload.
94 var attackersPerShip = t => Math.min(30, Math.round(t * 2));
97 * Likelihood of adding a non-existing hero at t minutes.
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.
104 var healerRatio = t => randFloat(0, 0.1);
107 * Number of siege engines to add per shipload.
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.
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.
123 * Number of trigger points to patrol when not having enemies to attack.
128 * Which units ships should focus when attacking and patrolling.
130 var shipTargetClass = "Warship";
133 * Which entities siege engines should focus when attacking and patrolling.
135 var siegeTargetClass = "Defensive";
138 * Which entities units should focus when attacking and patrolling.
140 var unitTargetClass = "Unit+!Ship";
143 * Ungarrison ships when being in this range of the target.
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.
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.
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.
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.
190 Trigger.prototype.debugLog = function(txt)
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)
205 let unitCounts = TriggerHelper.SpawnAndGarrisonAtClasses(gaulPlayer, buildingClass, buildingGarrison.unitTemplates, 1);
206 this.debugLog("Garrisoning at " + buildingClass + ": " + uneval(unitCounts));
211 * Spawn units of the template at each gaia Civic Center and set them to defensive.
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)
231 let isLeft = this.IsLeftRiverside(gaiaCC)
232 if (isLeft && !spawnLeft || !isLeft && !spawnRight)
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)
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);
250 let patrolPointRef = isLeft ?
251 triggerPointCCAttackerPatrolLeft :
252 triggerPointCCAttackerPatrolRight;
254 this.AttackAndPatrol(ccAttackers, unitTargetClass, patrolPointRef, "CCAttackers", false);
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.
265 Trigger.prototype.StartCelticRitual = function()
267 for (let ent of TriggerHelper.GetPlayerEntitiesByClass(gaulPlayer, "Human"))
269 if (randBool(ritualProbability))
270 this.ritualEnts.add(ent);
272 TriggerHelper.SetUnitStance(ent, "defensive");
275 this.DoRepeatedly(5 * 1000, "UpdateCelticRitual", {});
279 * Play one of the given animations for most participants if and only if they are idle.
281 Trigger.prototype.UpdateCelticRitual = function()
283 for (let ent of this.ritualEnts)
285 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
286 if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE")
289 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
293 let animations = ritualAnimations[
294 cmpIdentity.HasClass("Healer") ? "healer" :
295 cmpIdentity.HasClass("Female") ? "female" : "male"];
297 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
301 if (animations.indexOf(cmpVisual.GetAnimationName()) == -1)
302 cmpVisual.SelectAnimation(pickRandom(animations), false, 1, "");
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.
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)
320 TriggerHelper.SpawnUnits(
321 pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)),
322 pickRandom(danubiusAttackerTemplates.ships),
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);
337 Trigger.prototype.GetAttackerComposition = function(time, siegeEngines)
339 let champRatio = championRatio(time);
342 "templates": danubiusAttackerTemplates.heroes,
343 "count": randBool(heroProbability(time)) ? 1 : 0,
344 "unique_entities": Array.from(this.heroes)
347 "templates": danubiusAttackerTemplates.siege,
348 "count": siegeEngines ? siegeCount(time) : 0
351 "templates": danubiusAttackerTemplates.healers,
352 "frequency": healerRatio(time)
355 "templates": danubiusAttackerTemplates.champions,
356 "frequency": champRatio
359 "templates": danubiusAttackerTemplates.citizen_soldiers,
360 "frequency": 1 - champRatio
365 Trigger.prototype.FillShips = function()
367 let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
368 for (let ship of this.ships)
370 let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
371 if (!cmpGarrisonHolder)
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)
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]);
388 this.fillShipsTimer = this.DoAfterDelay(shipFillInterval() * 60 * 1000, "FillShips", {});
392 * Attack the closest enemy ships around, then patrol the sea.
394 Trigger.prototype.AttackAndPatrol = function(entities, targetClass, triggerPointRef, debugName, attack)
396 if (!entities.length)
399 let healers = TriggerHelper.MatchEntitiesByClass(entities, "Healer");
402 let healerTargets = TriggerHelper.MatchEntitiesByClass(entities, "Hero Champion");
403 if (!healerTargets.length)
404 healerTargets = TriggerHelper.MatchEntitiesByClass(entities, "Soldier");
406 ProcessCommand(gaulPlayer, {
409 "target": pickRandom(healerTargets),
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));
422 for (let target of targets)
423 ProcessCommand(gaulPlayer, {
425 "entities": attackers,
428 "allowCapture": false
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)
436 let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D();
437 ProcessCommand(gaulPlayer, {
439 "entities": attackers,
443 "attack": [targetClass]
446 "allowCapture": false
452 * To avoid unloading unlimited amounts of units on empty riversides,
453 * only add attackers to riversides where player buildings exist that are
456 Trigger.prototype.GetActiveRiversides = function()
461 for (let ent of TriggerHelper.GetAllPlayersEntitiesByClass(siegeTargetClass))
463 if (this.IsLeftRiverside(ent))
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.
483 Trigger.prototype.UngarrisonShipsOrder = function()
485 let [ungarrisonLeft, ungarrisonRight] = this.GetActiveRiversides();
486 if (!ungarrisonLeft && !ungarrisonRight)
489 // Determine which ships should ungarrison on which side of the river
490 let ships = Array.from(this.ships);
494 if (ungarrisonLeft && ungarrisonRight)
496 shipsLeft = shuffleArray(ships).slice(0, Math.round(ships.length / 2));
497 shipsRight = ships.filter(ship => shipsLeft.indexOf(ship) == -1);
499 else if (ungarrisonLeft)
501 else if (ungarrisonRight)
504 // Determine which ships should ungarrison and patrol at which trigger point names
506 if (shipsLeft.length)
509 "ungarrisonPointRef": triggerPointUngarrisonLeft,
510 "landPointRef": triggerPointLandPatrolLeft
513 if (shipsRight.length)
516 "ungarrisonPointRef": triggerPointUngarrisonRight,
517 "landPointRef": triggerPointLandPatrolRight
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)
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 };
536 this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
540 * Check frequently whether the ships are close enough to unload at the shoreline.
542 Trigger.prototype.CheckShipRange = function()
544 for (let ship of this.ships)
546 if (!this.shipTarget[ship] || DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance)
549 let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
550 if (!cmpGarrisonHolder)
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, {
564 "name": pickRandom(unitFormations)
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];
576 Trigger.prototype.DanubiusOwnershipChange = function(data)
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(),
616 this.StartCelticRitual();
617 this.GarrisonAllGallicBuildings();
618 this.SpawnInitialCCDefenders();
619 this.SpawnCCAttackers();
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 });