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 "other/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(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + 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)
141 let cmpPlayer = QueryPlayerIDInterface(i);
142 cmpPlayer.SetDisabledTemplates(disabledTemplates(cmpPlayer.GetCiv()));
147 * Remember civic centers and make women invincible.
149 Trigger.prototype.InitStartingUnits = function()
151 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
152 for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i)
154 let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i);
156 for (let entity of playerEntities)
158 if (TriggerHelper.EntityMatchesClassList(entity, "CivilCentre"))
159 this.playerCivicCenter[i] = entity;
160 else if (TriggerHelper.EntityMatchesClassList(entity, "FemaleCitizen"))
162 this.treasureFemale[i] = entity;
164 let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver);
165 cmpDamageReceiver.SetInvulnerability(true);
171 Trigger.prototype.InitializeEnemyWaves = function()
173 let time = firstWaveTime() * 60 * 1000;
174 Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({
175 "message": markForTranslation("The first wave will start in %(time)s!"),
176 "translateMessage": true
178 this.DoAfterDelay(time, "StartAnEnemyWave", {});
181 Trigger.prototype.StartAnEnemyWave = function()
183 let currentMin = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
184 let nextWaveTime = waveTime();
185 let civ = pickRandom(Object.keys(attackerUnitTemplates));
187 // Determine total attacker count of the current wave.
188 // Exponential increase with time, capped to the limit and fluctuating proportionally with the current wavetime.
189 let totalAttackers = Math.ceil(Math.min(totalAttackerLimit,
190 initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / maxWaveTime));
192 let siegeRatio = siegeFraction();
194 this.debugLog("Spawning " + totalAttackers + " attackers, siege ratio " + siegeRatio.toFixed(2));
196 let attackerCount = TriggerHelper.BalancedTemplateComposition(
199 "templates": attackerUnitTemplates[civ].heroes,
200 "count": currentMin > heroTime() && attackerUnitTemplates[civ].heroes.length ? 1 : 0
203 "templates": attackerUnitTemplates[civ].siege,
204 "frequency": siegeRatio
207 "templates": attackerUnitTemplates[civ].champions,
208 "frequency": 1 - siegeRatio
213 this.debugLog("Templates: " + uneval(attackerCount));
215 // Spawn the templates
216 let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
218 for (let point of this.GetTriggerPoints("A"))
226 // Don't spawn attackers for defeated players and players that lost their cc after win
227 let playerID = QueryOwnerInterface(point, IID_Player).GetPlayerID();
228 let civicCentre = this.playerCivicCenter[playerID];
232 // Check if the cc is garrisoned in another building
233 let cmpPosition = Engine.QueryInterface(civicCentre, IID_Position);
234 if (!cmpPosition || !cmpPosition.IsInWorld())
237 let targetPos = cmpPosition.GetPosition2D();
239 for (let templateName in attackerCount)
241 let isHero = attackerUnitTemplates[civ].heroes.indexOf(templateName) != -1;
243 // Don't spawn gaia hero if the previous one is still alive
244 if (this.gaiaHeroes[playerID] && isHero)
246 let cmpHealth = Engine.QueryInterface(this.gaiaHeroes[playerID], IID_Health);
247 if (cmpHealth && cmpHealth.GetHitpoints() != 0)
249 this.debugLog("Not spawning hero for player " + playerID + " as the previous one is still alive");
257 let entities = TriggerHelper.SpawnUnits(point, templateName, attackerCount[templateName], 0);
260 "type": "attack-walk",
261 "entities": entities,
264 "targetClasses": undefined,
265 "allowCapture": false,
270 this.gaiaHeroes[playerID] = entities[0];
278 Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
279 "message": markForTranslation("An enemy wave is attacking!"),
280 "translateMessage": true
283 this.DoAfterDelay(nextWaveTime * 60 * 1000, "StartAnEnemyWave", {});
286 Trigger.prototype.PlaceTreasures = function()
288 let point = pickRandom(["B", "C", "D"]);
289 let triggerPoints = this.GetTriggerPoints(point);
290 for (let point of triggerPoints)
291 TriggerHelper.SpawnUnits(point, pickRandom(treasures), 1, 0);
293 this.DoAfterDelay(treasureTime() * 60 * 1000, "PlaceTreasures", {});
296 Trigger.prototype.OnOwnershipChanged = function(data)
298 if (data.entity == this.playerCivicCenter[data.from])
300 this.playerCivicCenter[data.from] = undefined;
302 TriggerHelper.DefeatPlayer(
304 markForTranslation("%(player)s has been defeated (lost civic center)."));
306 else if (data.entity == this.treasureFemale[data.from])
308 this.treasureFemale[data.from] = undefined;
309 Engine.DestroyEntity(data.entity);
315 let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
317 cmpTrigger.treasureFemale = [];
318 cmpTrigger.playerCivicCenter = [];
319 cmpTrigger.gaiaHeroes = [];
321 cmpTrigger.RegisterTrigger("OnInitGame", "InitSurvival", { "enabled": true });
322 cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true });