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";
19 "gaul_hero_viridomarus",
20 "gaul_hero_vercingetorix",
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];
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 = [
62 "buildings": ["House"],
63 "units": [femaleTemplate, healerTemplate]
66 "buildings": ["CivCentre", "Temple"],
67 "units": championTemplates
70 "buildings": ["DefenseTower", "Outpost"],
71 "units": championInfantryTemplates
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.
82 * Time in minutes between two consecutive waves spawned from the gaia civic centers, if they still exist.
84 var ccAttackerInterval = t => randFloat(6, 8);
87 * Number of attackers spawned at a civic center at t minutes ingame time.
89 var ccAttackerCount = t => Math.min(20, Math.max(0, Math.round(t * 1.5)));
92 * Time between two consecutive waves.
94 var shipRespawnTime = () => randFloat(8, 10);
97 * Limit of ships on the map when spawning them.
98 * Have at least two ships, so that both sides will be visited.
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.
105 var shipUngarrisonInterval = () => randFloat(5, 7);
108 * Time between refillings of all ships with new soldiers.
110 var shipFillInterval = () => randFloat(4, 5);
113 * Total count of gaia attackers per shipload.
115 var attackersPerShip = t => Math.min(30, Math.round(t * 2));
118 * Likelihood of adding a non-existing hero at t minutes.
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.
125 var healerRatio = t => randFloat(0, 0.1);
128 * Percent of siege engines to add per shipload.
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.
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.
144 * Number of trigger points to patrol when not having enemies to attack.
149 * Which units ships should focus when attacking and patrolling.
151 var shipTargetClass = "Warship";
154 * Which entities siege engines should focus when attacking and patrolling.
156 var siegeTargetClass = "Defensive";
159 * Which entities units should focus when attacking and patrolling.
161 var unitTargetClass = "Unit+!Ship";
164 * Ungarrison ships when being in this range of the target.
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.
172 var formationProbability = 0.2;
174 var unitFormations = [
182 * Chance for the units at the meeting place to participate in the ritual.
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.
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.
210 Trigger.prototype.debugLog = function(txt)
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.
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)
231 let currentCount = +i == templates.length - 1 ? remainder : Math.round(ratios[i] / ratioSum * count);
235 templateCounts[templates[i]] = currentCount;
236 remainder -= currentCount;
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.
257 Trigger.prototype.SpawnAndGarrisonBuilding = function(gaiaEnts, targetClass, templates)
259 for (let gaiaEnt of gaiaEnts)
261 let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity);
262 if (!cmpIdentity || !cmpIdentity.HasClass(targetClass))
265 let cmpGarrisonHolder = Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder);
266 if (!cmpGarrisonHolder)
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);
279 * Spawn units of the template at each gaia Civic Center and set them to defensive.
281 Trigger.prototype.SpawnInitialCCDefenders = function(gaiaEnts)
283 this.debugLog("To defend CCs, spawning " + uneval(ccDefenders));
285 for (let gaiaEnt of gaiaEnts)
287 let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity);
288 if (!cmpIdentity || !cmpIdentity.HasClass("CivCentre"))
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");
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)
308 let isLeft = Engine.QueryInterface(gaiaCC, IID_Position).GetPosition2D().x < mapSize / 2;
309 if (isLeft && !spawnLeft || !isLeft && !spawnRight)
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)
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);
327 let patrolPointRef = isLeft ?
328 triggerPointCCAttackerPatrolLeft :
329 triggerPointCCAttackerPatrolRight;
331 this.AttackAndPatrol(ccAttackers, unitTargetClass, patrolPointRef, "CCAttackers", false);
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.
342 Trigger.prototype.StartCelticRitual = function(gaiaEnts)
344 for (let ent of gaiaEnts)
346 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
347 if (!cmpIdentity || !cmpIdentity.HasClass("Human"))
350 if (randBool(ritualProbability))
351 this.ritualEnts.push(ent);
353 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
355 cmpUnitAI.SwitchToStance("defensive");
358 this.DoRepeatedly(5 * 1000, "UpdateCelticRitual", {});
362 * Play one of the given animations for most participants if and only if they are idle.
364 Trigger.prototype.UpdateCelticRitual = function()
366 for (let ent of this.ritualEnts)
368 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
369 if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE")
372 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
376 let animations = ritualAnimations[
377 cmpIdentity.HasClass("Healer") ? "healer" :
378 cmpIdentity.HasClass("Female") ? "female" : "male"];
380 let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
384 if (animations.indexOf(cmpVisual.GetAnimationName()) == -1)
385 cmpVisual.SelectAnimation(pickRandom(animations), false, 1, "");
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.
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);
415 Trigger.prototype.GetAttackerComposition = function(attackerCount, addSiege)
417 let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
419 let remainder = attackerCount;
421 let siegeCount = addSiege ? Math.round(siegeRatio(time) * remainder) : 0;
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)))
429 toSpawn.push({ "template": heroTemplate, "count": 1, "hero": true });
433 let healerCount = Math.round(healerRatio(time) * remainder);
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)
442 let count = championTemplateCounts[template];
443 toSpawn.push({ "template": template, "count": count });
444 championCount -= count;
448 let citizenTemplateCounts = this.RandomAttackerTemplates(citizenTemplates, remainder);
449 for (let template in citizenTemplateCounts)
451 let count = citizenTemplateCounts[template];
452 toSpawn.push({ "template": template, "count": count });
457 warn("Didn't spawn as many attackers as were intended (" + remainder + " remaining)");
462 Trigger.prototype.FillShips = function()
464 let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
465 for (let ship of this.ships)
467 let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
468 if (!cmpGarrisonHolder)
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)
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)
481 let ent = Engine.AddEntity("units/" + spawn.template);
482 Engine.QueryInterface(ent, IID_Ownership).SetOwner(gaulPlayer);
485 this.heroes.push({ "template": spawn.template, "ent": ent });
487 cmpGarrisonHolder.Garrison(ent);
492 this.fillShipsTimer = this.DoAfterDelay(shipFillInterval() * 60 * 1000, "FillShips", {});
496 * Attack the closest enemy ships around, then patrol the sea.
498 Trigger.prototype.AttackAndPatrol = function(attackers, targetClass, triggerPointRef, debugName, attack = true)
500 if (!attackers.length)
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);
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));
514 for (let target of targets)
515 ProcessCommand(gaulPlayer, {
517 "entities": attackers,
520 "allowCapture": false
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)
528 let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D();
529 ProcessCommand(gaulPlayer, {
531 "entities": attackers,
535 "attack": [targetClass]
538 "allowCapture": false
544 * To avoid unloading unlimited amounts of units on empty riversides,
545 * only add attackers to riversides where player buildings exist that are
548 Trigger.prototype.GetActiveRiversides = function()
550 let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize();
555 for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities())
557 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
558 if (!cmpIdentity || !cmpIdentity.HasClass(siegeTargetClass))
561 if (Engine.QueryInterface(ent, IID_Position).GetPosition2D().x < mapSize / 2)
570 return [left, right];
574 * Order all ships to abort naval warfare and move to the shoreline all few minutes.
576 Trigger.prototype.UngarrisonShipsOrder = function()
578 let [ungarrisonLeft, ungarrisonRight] = this.GetActiveRiversides();
579 if (!ungarrisonLeft && !ungarrisonRight)
582 // Determine which ships should ungarrison on which side of the river
586 if (ungarrisonLeft && ungarrisonRight)
588 shipsLeft = shuffleArray(this.ships).slice(0, Math.round(this.ships.length / 2));
589 shipsRight = this.ships.filter(ship => shipsLeft.indexOf(ship) == -1);
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
598 if (shipsLeft.length)
601 "ungarrisonPointRef": triggerPointUngarrisonLeft,
602 "landPointRef": triggerPointLandPatrolLeft
605 if (shipsRight.length)
608 "ungarrisonPointRef": triggerPointUngarrisonRight,
609 "landPointRef": triggerPointLandPatrolRight
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)
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 };
628 this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
632 * Check frequently whether the ships are close enough to unload at the shoreline.
634 Trigger.prototype.CheckShipRange = function()
636 for (let ship of this.ships)
638 if (!this.shipTarget[ship] || DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance)
641 let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder);
642 if (!cmpGarrisonHolder)
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, {
656 "name": "special/formations/" + pickRandom(unitFormations)
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");
667 Trigger.prototype.DanubiusOwnershipChange = function(data)
672 let shipIdx = this.ships.indexOf(data.entity);
675 this.debugLog("Ship " + data.entity + " sunk");
676 this.ships.splice(shipIdx, 1);
679 let ritualIdx = this.ritualEnts.indexOf(data.entity);
681 this.ritualEnts.splice(ritualIdx, 1);
683 let heroIdx = this.heroes.findIndex(hero => hero.ent == data.entity);
685 this.heroes.splice(heroIdx, 1);
687 let ccIdx = this.civicCenters.indexOf(data.entity);
690 this.debugLog("Gaia civic center " + data.entity + " destroyed or captured");
691 this.civicCenters.splice(ccIdx, 1);
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 });