2 * The city is patroled along its paths by infantry champions that respawn reoccuringly.
3 * There are increasingly great gaia attacks started from the different buildings.
4 * The players can destroy gaia buildings to reduce the number of attackers for the future.
8 * If set to true, it will print how many templates would be spawned if the players were not defeated.
13 * If enabled, prints the number of units to the command line output.
15 const showDebugLog = false;
17 // TODO: harass attackers
19 var jebelBarkal_rank = "Advanced";
22 * These are the templates spawned at the gamestart and during the game.
24 var jebelBarkal_templateClasses = deepfreeze({
26 "champions": "Champion+!Elephant",
27 "elephants": "Champion+Elephant",
28 "champion_infantry": "Champion+Infantry",
29 "champion_infantry_melee": "Champion+Infantry+Melee",
30 "champion_infantry_ranged": "Champion+Infantry+Ranged",
31 "champion_cavalry": "Champion+Cavalry",
32 "citizenSoldiers": "CitizenSoldier",
33 "citizenSoldier_infantry": "CitizenSoldier+Infantry",
34 "citizenSoldier_infantry_melee": "CitizenSoldier+Infantry+Melee",
35 "citizenSoldier_infantry_ranged": "CitizenSoldier+Infantry+Ranged",
36 "citizenSoldier_cavalry": "CitizenSoldier+Cavalry",
38 "females": "FemaleCitizen"
41 var jebelBarkal_templates = deepfreeze(Object.keys(jebelBarkal_templateClasses).reduce((templates, name) => {
42 templates[name] = TriggerHelper.GetTemplateNamesByClasses(jebelBarkal_templateClasses[name], "kush", undefined, jebelBarkal_rank, true);
47 * These are the formations patroling and attacking units can use.
49 var jebelBarkal_formations = [
50 "special/formations/line_closed",
51 "special/formations/column_closed"
55 * Balancing helper function.
57 * @returns min0 value at the beginning of the game, min60 after an hour of gametime or longer and
58 * a proportionate number between these two values before the first hour is reached.
60 var scaleByTime = (minCurrent, min0, min60) => min0 + (min60 - min0) * Math.min(1, minCurrent / 60);
63 * @returns min0 value at the beginning of the game, min60 after an hour of gametime or longer and
64 * a proportionate number between these two values before the first hour is reached.
66 var scaleByMapSize = (min, max) => min + (max - min) * (TriggerHelper.GetMapSizeTiles() - 128) / (512 - 128);
69 * Defensive Infantry units patrol along the paths of the city.
71 var jebelBarkal_cityPatrolGroup_count = time => scaleByTime(time, 3, scaleByMapSize(3, 10));
72 var jebelBarkal_cityPatrolGroup_interval = time => scaleByTime(time, 5, 3);
73 var jebelBarkal_cityPatrolGroup_balancing = {
74 "buildingClasses": ["Wonder", "Temple", "CivCentre", "Fortress", "Barracks", "Embassy"],
75 "unitCount": time => Math.min(20, scaleByTime(time, 10, 45)),
76 "unitComposition": (time, heroes) => [
78 "templates": jebelBarkal_templates.champion_infantry_melee,
79 "frequency": scaleByTime(time, 0, 2)
82 "templates": jebelBarkal_templates.champion_infantry_ranged,
83 "frequency": scaleByTime(time, 0, 3)
86 "templates": jebelBarkal_templates.citizenSoldier_infantry_melee,
87 "frequency": scaleByTime(time, 2, 0)
90 "templates": jebelBarkal_templates.citizenSoldier_infantry_ranged,
91 "frequency": scaleByTime(time, 3, 0)
97 * Frequently the buildings spawn different units that attack the players groupwise.
99 var jebelBarkal_attackInterval = time => randFloat(5, 7);
102 * Frequently cavalry is spawned at few buildings to harass the enemy trade.
104 var jebelBarkal_harassInterval = time => randFloat(6, 8);
107 * Assume gaia to be the native kushite player.
109 var jebelBarkal_playerID = 0;
112 * Soldiers will patrol along the city grid.
114 var jebelBarkal_triggerPointPath = "A";
117 * Attacker groups approach these player buildings and attack enemies of the given classes encountered.
119 var jebelBarkal_pathTargetClasses = "CivCentre Wonder";
122 * Soldiers and siege towers prefer these targets when attacking or patroling.
124 var jebelBarkal_soldierTargetClasses = "Unit+!Ship";
127 * Elephants focus these units when attacking.
129 var jebelBarkal_elephantTargetClasses = "Defensive SiegeEngine";
132 * This defines which units are spawned and garrisoned at the gamestart per building.
134 var jebelBarkal_buildingGarrison = [
136 "buildingClasses": ["Wonder", "Temple", "CivCentre", "Fortress"],
137 "unitTemplates": jebelBarkal_templates.champions
140 "buildingClasses": ["Barracks", "Embassy"],
141 "unitTemplates": [...jebelBarkal_templates.citizenSoldiers, ...jebelBarkal_templates.champions]
144 "buildingClasses": ["DefenseTower"],
145 "unitTemplates": jebelBarkal_templates.champion_infantry
148 "buildingClasses": ["ElephantStables"],
149 "unitTemplates": jebelBarkal_templates.elephants
152 "buildingClasses": ["Stable"],
153 "unitTemplates": jebelBarkal_templates.champion_cavalry
156 "buildingClasses": ["Pyramid"],
157 "unitTemplates": [...jebelBarkal_templates.citizenSoldiers, ...jebelBarkal_templates.healers]
160 "buildingClasses": ["House"],
161 "unitTemplates": [...jebelBarkal_templates.females, ...jebelBarkal_templates.healers]
166 * This defines which units are spawned at the different buildings at the given time.
167 * The buildings are ordered by strength.
168 * Notice that there are always 2 groups of these count spawned, one for each side!
169 * The units should do a walk-attack to random player CCs
171 var jebelBarkal_attackerGroup_balancing = [
173 "buildingClasses": ["Wonder"],
174 "unitCount": time => scaleByTime(time, 0, 85),
175 "unitComposition": (time, heroes) => [
177 "templates": jebelBarkal_templates.heroes,
178 "count": randBool(scaleByTime(time, -0.5, 2)) ? 1 : 0,
179 "unique_entities": heroes
182 "templates": jebelBarkal_templates.healers,
183 "frequency": randFloat(0, 0.1)
186 "templates": jebelBarkal_templates.champions,
187 "frequency": scaleByTime(time, 0, 0.6)
190 "templates": jebelBarkal_templates.champion_infantry_ranged,
191 "frequency": scaleByTime(time, 0, 0.4)
194 "templates": jebelBarkal_templates.citizenSoldiers,
195 "frequency": scaleByTime(time, 1, 0)
198 "templates": jebelBarkal_templates.citizenSoldier_infantry_ranged,
199 "frequency": scaleByTime(time, 1, 0)
204 "buildingClasses": ["Fortress"],
205 "unitCount": time => scaleByTime(time, 0, 45),
206 "unitComposition": (time, heroes) => [
208 "templates": jebelBarkal_templates.heroes,
209 "count": randBool(scaleByTime(time, -0.5, 1.5)) ? 1 : 0,
210 "unique_entities": heroes
213 "templates": jebelBarkal_templates.champions,
214 "frequency": scaleByTime(time, 0, 1)
219 "buildingClasses": ["Temple"],
220 "unitCount": time => Math.min(45, scaleByTime(time, 0, 90)),
221 "unitComposition": (time, heroes) => [
223 "templates": jebelBarkal_templates.heroes,
224 "count": randBool(scaleByTime(time, -0.5, 1)) ? 1 : 0,
225 "unique_entities": heroes
228 "templates": jebelBarkal_templates.champion_infantry_melee,
232 "templates": jebelBarkal_templates.champion_infantry_ranged,
236 "templates": jebelBarkal_templates.healers,
237 "frequency": randFloat(0.05, 0.2)
242 "buildingClasses": ["CivCentre"],
243 "unitCount": time => Math.min(40, scaleByTime(time, 0, 80)),
244 "unitComposition": (time, heroes) => [
246 "templates": jebelBarkal_templates.heroes,
247 "count": randBool(scaleByTime(time, -0.5, 0.5)) ? 1 : 0,
248 "unique_entities": heroes
251 "templates": jebelBarkal_templates.champion_infantry,
252 "frequency": scaleByTime(time, 0, 1)
255 "templates": jebelBarkal_templates.citizenSoldiers,
256 "frequency": scaleByTime(time, 1, 0)
261 "buildingClasses": ["Stable"],
262 "unitCount": time => Math.min(30, scaleByTime(time, 0, 80)),
263 "harasserSize": time => Math.min(20, scaleByTime(time, 0, 50)),
264 "unitComposition": (time, heroes) => [
266 "templates": jebelBarkal_templates.citizenSoldier_cavalry,
267 "frequency": scaleByTime(time, 2, 0)
270 "templates": jebelBarkal_templates.champion_cavalry,
271 "frequency": scaleByTime(time, 0, 1)
276 "buildingClasses": ["Barracks", "Embassy"],
277 "unitCount": time => Math.min(35, scaleByTime(time, 0, 70)),
278 "unitComposition": (time, heroes) => [
280 "templates": jebelBarkal_templates.citizenSoldier_infantry,
286 "buildingClasses": ["ElephantStables"],
287 "unitCount": time => scaleByTime(time, 0, 10),
288 "unitComposition": (time, heroes) => [
290 "templates": jebelBarkal_templates.elephants,
297 Trigger.prototype.debugLog = function(txt)
300 print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n");
303 Trigger.prototype.JebelBarkal_Init = function()
305 this.JebelBarkal_TrackUnits();
306 this.JebelBarkal_SetDefenderStance();
307 this.JebelBarkal_GarrisonBuildings();
308 this.JebelBarkal_SpawnCityPatrolGroups();
309 this.JebelBarkal_SpawnAttackerGroups();
312 Trigger.prototype.JebelBarkal_TrackUnits = function()
314 // Each item is an entity ID
315 this.jebelBarkal_heroes = [];
317 // Each item is an array of entity IDs
318 this.jebelBarkal_patrolingUnits = [];
320 // Array of entityIDs where patrol groups can spawn
321 this.jebelBarkal_patrolGroupSpawnPoints = TriggerHelper.GetPlayerEntitiesByClass(
322 jebelBarkal_playerID,
323 jebelBarkal_cityPatrolGroup_balancing.buildingClasses);
325 this.debugLog("Patrol spawn points: " + uneval(this.jebelBarkal_patrolGroupSpawnPoints));
327 // Array of entityIDs where attacker groups can spawn
328 this.jebelBarkal_attackerGroupSpawnPoints = TriggerHelper.GetPlayerEntitiesByClass(
329 jebelBarkal_playerID,
330 jebelBarkal_attackerGroup_balancing.reduce((classes, attackerSpawning) => classes.concat(attackerSpawning.buildingClasses), []));
332 this.debugLog("Attacker spawn points: " + uneval(this.jebelBarkal_attackerGroupSpawnPoints));
335 Trigger.prototype.JebelBarkal_SetDefenderStance = function()
337 for (let ent of TriggerHelper.GetPlayerEntitiesByClass(jebelBarkal_playerID, "Soldier"))
338 TriggerHelper.SetUnitStance(ent, "defensive");
341 Trigger.prototype.JebelBarkal_GarrisonBuildings = function()
343 for (let buildingGarrison of jebelBarkal_buildingGarrison)
344 TriggerHelper.SpawnAndGarrisonAtClasses(jebelBarkal_playerID, buildingGarrison.buildingClasses, buildingGarrison.unitTemplates, 1);
348 * Spawn new groups if old ones were wiped out.
350 Trigger.prototype.JebelBarkal_SpawnCityPatrolGroups = function()
352 if (!this.jebelBarkal_patrolGroupSpawnPoints.length)
355 let time = TriggerHelper.GetMinutes();
356 let groupCount = Math.floor(Math.max(0, jebelBarkal_cityPatrolGroup_count(time)) - this.jebelBarkal_patrolingUnits.length);
358 for (let i = 0; i < groupCount; ++i)
360 let spawnEnt = pickRandom(this.jebelBarkal_patrolGroupSpawnPoints);
362 let templateCounts = TriggerHelper.BalancedTemplateComposition(
363 jebelBarkal_cityPatrolGroup_balancing.unitComposition(time, this.jebelBarkal_heroes),
364 jebelBarkal_cityPatrolGroup_balancing.unitCount(time));
366 this.debugLog("Spawning " + groupCount + " city patrol groups, " +
367 this.jebelBarkal_patrolingUnits.length + " exist, templates:\n" + uneval(templateCounts));
369 let groupEntities = this.JebelBarkal_SpawnTemplates(spawnEnt, templateCounts);
371 this.jebelBarkal_patrolingUnits.push(groupEntities);
373 for (let ent of groupEntities)
374 TriggerHelper.SetUnitStance(ent, "defensive");
376 TriggerHelper.SetUnitFormation(jebelBarkal_playerID, groupEntities, pickRandom(jebelBarkal_formations));
378 for (let entTriggerPoint of this.GetTriggerPoints(jebelBarkal_triggerPointPath))
380 let pos = TriggerHelper.GetEntityPosition2D(entTriggerPoint);
381 ProcessCommand(jebelBarkal_playerID, {
383 "entities": groupEntities,
387 "attack": jebelBarkal_soldierTargetClasses
390 "allowCapture": false
395 this.DoAfterDelay(jebelBarkal_cityPatrolGroup_interval(time) * 60 * 1000, "JebelBarkal_SpawnCityPatrolGroups", {});
398 Trigger.prototype.JebelBarkal_SpawnTemplates = function(spawnEnt, templateCounts)
400 let groupEntities = [];
402 for (let templateName in templateCounts)
404 let ents = TriggerHelper.SpawnUnits(spawnEnt, templateName, templateCounts[templateName], jebelBarkal_playerID);
406 groupEntities = groupEntities.concat(ents);
408 if (jebelBarkal_templates.heroes.indexOf(templateName) != -1 && ents[0])
409 this.jebelBarkal_heroes.push(ents[0]);
412 return groupEntities;
416 * Spawn a group of attackers at every remaining building.
418 Trigger.prototype.JebelBarkal_SpawnAttackerGroups = function()
420 let time = TriggerHelper.GetMinutes();
421 this.debugLog("Attacker wave");
423 let targets = TriggerHelper.GetAllPlayersEntitiesByClass(jebelBarkal_pathTargetClasses);
427 let spawnedAnything = false;
428 for (let spawnEnt of this.jebelBarkal_attackerGroupSpawnPoints)
430 let spawnPointBalancing = jebelBarkal_attackerGroup_balancing.find(balancing =>
431 TriggerHelper.EntityMatchesClassList(spawnEnt, balancing.buildingClasses));
433 let unitCount = Math.round(spawnPointBalancing.unitCount(time));
438 let templateCounts = TriggerHelper.BalancedTemplateComposition(spawnPointBalancing.unitComposition(time, this.jebelBarkal_heroes), unitCount);
440 this.debugLog("Spawning " + unitCount + " attackers at " + uneval(spawnPointBalancing.buildingClasses) + " " + spawnEnt + ":\n" + uneval(templateCounts));
445 let groupEntities = this.JebelBarkal_SpawnTemplates(spawnEnt, templateCounts);
446 spawnedAnything = true;
448 let isElephant = TriggerHelper.EntityMatchesClassList(groupEntities[0], "Elephant");
450 for (let ent of groupEntities)
451 TriggerHelper.SetUnitStance(ent, isElephant ? "aggressive" : "violent");
454 TriggerHelper.SetUnitFormation(jebelBarkal_playerID, groupEntities, pickRandom(jebelBarkal_formations));
456 let targetPos = TriggerHelper.GetEntityPosition2D(pickRandom(targets));
460 ProcessCommand(jebelBarkal_playerID, {
461 "type": "attack-walk",
462 "entities": groupEntities,
465 "targetClasses": isElephant ? jebelBarkal_elephantTargetClasses : jebelBarkal_soldierTargetClasses,
466 "allowCapture": false,
472 Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
473 "message": markForTranslation("Napata is attacking!"),
474 "translateMessage": true
477 this.DoAfterDelay(jebelBarkal_attackInterval(TriggerHelper.GetMinutes()) * 60 * 1000, "JebelBarkal_SpawnAttackerGroups", {});
481 * Keep track of heroes, so that each of them remains unique.
482 * Keep track of spawn points, as only there units should be spawned.
485 Trigger.prototype.JebelBarkal_OwnershipChange = function(data)
490 for (let array of [this.jebelBarkal_heroes, this.jebelBarkal_patrolGroupSpawnPoints, this.jebelBarkal_attackerGroupSpawnPoints, ...this.jebelBarkal_patrolingUnits])
492 let idx = array.indexOf(data.entity);
494 array.splice(idx, 1);
497 this.jebelBarkal_patrolingUnits = this.jebelBarkal_patrolingUnits.filter(entities => entities.length);
502 let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
503 cmpTrigger.RegisterTrigger("OnInitGame", "JebelBarkal_Init", { "enabled": true });
504 cmpTrigger.RegisterTrigger("OnOwnershipChanged", "JebelBarkal_OwnershipChange", { "enabled": true });