2 * If set to true, it will print how many templates would be spawned if the players were not defeated.
7 * If enabled, prints the number of units to the command line output.
9 const debugLog = false;
12 * Get the number of minutes to pass between spawning new treasures.
14 var treasureTime = () => randFloat(3, 5);
17 * Get the time in minutes when the first wave of attackers will be spawned.
19 var firstWaveTime = () => randFloat(4, 6);
22 * Maximum time in minutes between two consecutive waves.
27 * Get the next attacker wave delay.
29 var waveTime = () => randFloat(0.5, 1) * maxWaveTime;
32 * Roughly the number of attackers on the first wave.
34 var initialAttackers = 5;
37 * Increase the number of attackers exponentially, by this percent value per minute.
39 var percentPerMinute = 1.05;
42 * Greatest amount of attackers that can be spawned.
44 var totalAttackerLimit = 200;
47 * Least and greatest amount of siege engines per wave.
49 var siegeFraction = () => randFloat(0.2, 0.5);
52 * Potentially / definitely spawn a gaia hero after this number of minutes.
54 var heroTime = () => randFloat(20, 60);
57 * The following templates can't be built by any player.
59 var disabledTemplates = (civ) => [
60 // Economic structures
61 "structures/" + civ + "/corral",
62 "structures/" + civ + "/farmstead",
63 "structures/" + civ + "/field",
64 "structures/" + civ + "/storehouse",
65 "structures/" + civ + "/rotarymill",
66 "units/maur/support_elephant",
69 "structures/" + civ + "/civil_centre",
70 "structures/" + civ + "/military_colony",
73 "structures/" + civ + "/wallset_stone",
74 "structures/rome/wallset_siege",
75 "structures/wallset_palisade",
78 "structures/" + civ + "/dock",
79 "structures/brit/crannog",
80 "structures/cart/super_dock",
81 "structures/ptol/lighthouse"
85 * Spawn these treasures in regular intervals.
88 "gaia/treasure/food_barrel",
89 "gaia/treasure/food_bin",
90 "gaia/treasure/food_crate",
91 "gaia/treasure/food_jars",
92 "gaia/treasure/metal",
93 "gaia/treasure/stone",
100 * An object that maps from civ [f.e. "spart"] to an object
101 * that has the keys "champions", "siege" and "heroes",
102 * which is an array containing all these templates,
103 * trainable from a building or not.
105 var attackerUnitTemplates = {};
107 Trigger.prototype.InitSurvival = function()
109 this.InitStartingUnits();
110 this.LoadAttackerTemplates();
111 this.SetDisableTemplates();
112 this.PlaceTreasures();
113 this.InitializeEnemyWaves();
116 Trigger.prototype.debugLog = function(txt)
121 print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n");
124 Trigger.prototype.LoadAttackerTemplates = function()
126 for (let civ of ["gaia", ...Object.keys(loadCivFiles(false))])
127 attackerUnitTemplates[civ] = {
128 "heroes": TriggerHelper.GetTemplateNamesByClasses("Hero", civ, undefined, true),
129 "champions": TriggerHelper.GetTemplateNamesByClasses("Champion+!Elephant", civ, undefined, true),
130 "siege": TriggerHelper.GetTemplateNamesByClasses("Siege Champion+Elephant", civ, "packed", undefined)
133 this.debugLog("Attacker templates:");
134 this.debugLog(uneval(attackerUnitTemplates));
137 Trigger.prototype.SetDisableTemplates = function()
139 for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i)
140 QueryPlayerIDInterface(i).SetDisabledTemplates(disabledTemplates(QueryPlayerIDInterface(i, IID_Identity).GetCiv()));
144 * Remember civic centers and make women invincible.
146 Trigger.prototype.InitStartingUnits = function()
148 for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
150 this.playerCivicCenter[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "CivilCentre")[0];
151 this.treasureFemale[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "FemaleCitizen")[0];
152 Engine.QueryInterface(this.treasureFemale[playerID], IID_Resistance).SetInvulnerability(true);
156 Trigger.prototype.InitializeEnemyWaves = function()
158 let time = firstWaveTime() * 60 * 1000;
159 Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({
160 "message": markForTranslation("The first wave will start in %(time)s!"),
161 "translateMessage": true
163 this.DoAfterDelay(time, "StartAnEnemyWave", {});
166 Trigger.prototype.StartAnEnemyWave = function()
168 let currentMin = TriggerHelper.GetMinutes();
169 let nextWaveTime = waveTime();
170 let civ = pickRandom(Object.keys(attackerUnitTemplates));
172 // Determine total attacker count of the current wave.
173 // Exponential increase with time, capped to the limit and fluctuating proportionally with the current wavetime.
174 let totalAttackers = Math.ceil(Math.min(totalAttackerLimit,
175 initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / maxWaveTime));
177 let siegeRatio = siegeFraction();
179 this.debugLog("Spawning " + totalAttackers + " attackers, siege ratio " + siegeRatio.toFixed(2));
181 let attackerCount = TriggerHelper.BalancedTemplateComposition(
184 "templates": attackerUnitTemplates[civ].heroes,
185 "count": currentMin > heroTime() && attackerUnitTemplates[civ].heroes.length ? 1 : 0
188 "templates": attackerUnitTemplates[civ].siege,
189 "frequency": siegeRatio
192 "templates": attackerUnitTemplates[civ].champions,
193 "frequency": 1 - siegeRatio
198 this.debugLog("Templates: " + uneval(attackerCount));
200 // Spawn the templates
202 for (let point of this.GetTriggerPoints("A"))
210 // Don't spawn attackers for defeated players and players that lost their cc after win
211 let cmpPlayer = QueryOwnerInterface(point, IID_Player);
215 let playerID = cmpPlayer.GetPlayerID();
216 let civicCentre = this.playerCivicCenter[playerID];
220 // Check if the cc is garrisoned in another building
221 let targetPos = TriggerHelper.GetEntityPosition2D(civicCentre);
225 for (let templateName in attackerCount)
227 let isHero = attackerUnitTemplates[civ].heroes.indexOf(templateName) != -1;
229 // Don't spawn gaia hero if the previous one is still alive
230 if (this.gaiaHeroes[playerID] && isHero)
232 let cmpHealth = Engine.QueryInterface(this.gaiaHeroes[playerID], IID_Health);
233 if (cmpHealth && cmpHealth.GetHitpoints() != 0)
235 this.debugLog("Not spawning hero for player " + playerID + " as the previous one is still alive");
243 let entities = TriggerHelper.SpawnUnits(point, templateName, attackerCount[templateName], 0);
246 "type": "attack-walk",
247 "entities": entities,
250 "targetClasses": undefined,
251 "allowCapture": false,
256 this.gaiaHeroes[playerID] = entities[0];
264 Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
265 "message": markForTranslation("An enemy wave is attacking!"),
266 "translateMessage": true
269 this.DoAfterDelay(nextWaveTime * 60 * 1000, "StartAnEnemyWave", {});
272 Trigger.prototype.PlaceTreasures = function()
274 let triggerPoints = this.GetTriggerPoints(pickRandom(["B", "C", "D"]));
275 for (let point of triggerPoints)
276 TriggerHelper.SpawnUnits(point, pickRandom(treasures), 1, 0);
278 this.DoAfterDelay(treasureTime() * 60 * 1000, "PlaceTreasures", {});
281 Trigger.prototype.OnOwnershipChanged = function(data)
283 if (data.entity == this.playerCivicCenter[data.from])
285 this.playerCivicCenter[data.from] = undefined;
287 TriggerHelper.DefeatPlayer(
289 markForTranslation("%(player)s has been defeated (lost civic center)."));
291 else if (data.entity == this.treasureFemale[data.from])
293 this.treasureFemale[data.from] = undefined;
294 Engine.DestroyEntity(data.entity);
300 let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
302 cmpTrigger.treasureFemale = [];
303 cmpTrigger.playerCivicCenter = [];
304 cmpTrigger.gaiaHeroes = [];
306 cmpTrigger.RegisterTrigger("OnInitGame", "InitSurvival", { "enabled": true });
307 cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true });