/**
* @class
* The city is patroled along its paths by infantry champions that respawn reoccuringly.
* There are increasingly great gaia attacks started from the different buildings.
* The players can destroy gaia buildings to reduce the number of attackers for the future.
*/
/**
* If set to true, it will print how many templates would be spawned if the players were not defeated.
*/
const dryRun = false;
/**
* If enabled, prints the number of units to the command line output.
*/
const showDebugLog = false;
/**
* Since Gaia doesn't have a TechnologyManager, Advanced and Elite soldiers have the same statistics as Basic.
*/
var jebelBarkal_rank = "Basic";
/**
* Limit the total amount of gaia units spawned for performance reasons.
*/
var jebelBarkal_maxPopulation = 8 * 150;
/**
* These are the templates spawned at the gamestart and during the game.
*/
var jebelBarkal_templateClasses = deepfreeze({
"heroes": "Hero",
"champions": "Champion+!Elephant",
"elephants": "Champion+Elephant",
"champion_infantry": "Champion+Infantry",
"champion_infantry_melee": "Champion+Infantry+Melee",
"champion_infantry_ranged": "Champion+Infantry+Ranged",
"champion_cavalry": "Champion+Cavalry",
"champion_cavalry_melee": "Champion+Cavalry+Melee",
"citizenSoldiers": "CitizenSoldier",
"citizenSoldier_infantry": "CitizenSoldier+Infantry",
"citizenSoldier_infantry_melee": "CitizenSoldier+Infantry+Melee",
"citizenSoldier_infantry_ranged": "CitizenSoldier+Infantry+Ranged",
"citizenSoldier_cavalry": "CitizenSoldier+Cavalry",
"citizenSoldier_cavalry_melee": "CitizenSoldier+Cavalry+Melee",
"healers": "Healer",
"females": "FemaleCitizen"
});
var jebelBarkal_templates = deepfreeze(Object.keys(jebelBarkal_templateClasses).reduce((templates, name) => {
templates[name] = TriggerHelper.GetTemplateNamesByClasses(jebelBarkal_templateClasses[name], "kush", undefined, jebelBarkal_rank, true);
return templates;
}, {}));
/**
* These are the formations patroling and attacking units can use.
*/
var jebelBarkal_formations = [
"special/formations/line_closed",
"special/formations/box"
];
/**
* Balancing helper function.
*
* @returns min0 value at the beginning of the game, min60 after an hour of gametime or longer and
* a proportionate number between these two values before the first hour is reached.
*/
var scaleByTime = (minCurrent, min0, min60) => min0 + (min60 - min0) * Math.min(1, minCurrent / 60);
/**
* @returns min0 value at the beginning of the game, min60 after an hour of gametime or longer and
* a proportionate number between these two values before the first hour is reached.
*/
var scaleByMapSize = (min, max) => min + (max - min) * (TriggerHelper.GetMapSizeTiles() - 128) / (512 - 128);
/**
* Defensive Infantry units patrol along the paths of the city.
*/
var jebelBarkal_cityPatrolGroup_count = time => TriggerHelper.GetMapSizeTiles() > 192 ? scaleByTime(time, 3, scaleByMapSize(3, 10)) : 0;
var jebelBarkal_cityPatrolGroup_interval = time => scaleByTime(time, 5, 3);
var jebelBarkal_cityPatrolGroup_balancing = {
"buildingClasses": ["Wonder", "Temple", "CivCentre", "Fortress", "Barracks", "Embassy"],
"unitCount": time => Math.min(20, scaleByTime(time, 10, 45)),
"unitComposition": (time, heroes) => [
{
"templates": jebelBarkal_templates.champion_infantry_melee,
"frequency": scaleByTime(time, 0, 2)
},
{
"templates": jebelBarkal_templates.champion_infantry_ranged,
"frequency": scaleByTime(time, 0, 3)
},
{
"templates": jebelBarkal_templates.citizenSoldier_infantry_melee,
"frequency": scaleByTime(time, 2, 0)
},
{
"templates": jebelBarkal_templates.citizenSoldier_infantry_ranged,
"frequency": scaleByTime(time, 3, 0)
}
],
"targetClasses": () => "Unit+!Ship"
};
/**
* Frequently the buildings spawn different units that attack the players groupwise.
* Leave more time between the attacks in later stages of the game since the attackers become much stronger over time.
*/
var jebelBarkal_attackInterval = (time, difficulty) => randFloat(5, 7) + time / difficulty / 10;
/**
* Prevent city patrols chasing the starting units in nomad mode.
*/
var jebelBarkal_firstCityPatrolTime = (difficulty, isNomad) =>
(isNomad ? 7 - difficulty : 0);
/**
* Delay the first attack in nomad mode.
*/
var jebelBarkal_firstAttackTime = (difficulty, isNomad) =>
jebelBarkal_attackInterval(0, difficulty) +
2 * Math.max(0, 3 - difficulty) +
(isNomad ? 9 - difficulty : 0);
/**
* Account for varying mapsizes and number of players when spawning attackers.
*/
var jebelBarkal_attackerGroup_sizeFactor = (numPlayers, numInitialSpawnPoints, difficulty) =>
numPlayers / numInitialSpawnPoints * difficulty * 0.85;
/**
* Assume gaia to be the native kushite player.
*/
var jebelBarkal_playerID = 0;
/**
* City patrols soldiers will patrol along these triggerpoints on the crossings of the city paths.
*/
var jebelBarkal_cityPatrolGroup_triggerPointPath = "A";
/**
* Attackers will patrol these points after having finished the attack-walk order.
*/
var jebelBarkal_attackerGroup_triggerPointPatrol = "B";
/**
* Number of points the attackers patrol.
*/
var jebelBarkal_patrolPointCount = 6;
/**
* Healers near the wonder run these animations when idle.
*/
var jebelBarkal_ritualAnimations = ["attack_capture", "promotion", "heal"];
/**
* This defines which units are spawned and garrisoned at the gamestart per building.
*/
var jebelBarkal_buildingGarrison = difficulty => [
{
"buildingClasses": ["Wonder", "Temple", "CivCentre", "Fortress"],
"unitTemplates": jebelBarkal_templates.champions,
"capacityRatio": 1
},
{
"buildingClasses": ["Barracks", "Embassy"],
"unitTemplates": [...jebelBarkal_templates.citizenSoldiers, ...jebelBarkal_templates.champions],
"capacityRatio": 1
},
{
"buildingClasses": ["Tower"],
"unitTemplates": jebelBarkal_templates.champion_infantry,
"capacityRatio": 1
},
{
"buildingClasses": ["ElephantStable"],
"unitTemplates": jebelBarkal_templates.elephants,
"capacityRatio": 1
},
{
"buildingClasses": ["Stable"],
"unitTemplates": jebelBarkal_templates.champion_cavalry,
"capacityRatio": 1
},
{
"buildingClasses": ["House"],
"unitTemplates": [...jebelBarkal_templates.females, ...jebelBarkal_templates.healers],
"capacityRatio": 0.5
},
{
"buildingClasses": ["StoneWall+Tower"],
"unitTemplates": jebelBarkal_templates.champion_infantry_ranged,
"capacityRatio": difficulty > 3 ? 1 : 0
},
{
"buildingClasses": ["StoneWall+!Tower"],
"unitTemplates": difficulty > 3 ? jebelBarkal_templates.champion_infantry_ranged : jebelBarkal_templates.citizenSoldier_infantry_ranged,
"capacityRatio": (difficulty - 2) / 3
}
];
/**
* This defines which units are spawned at the different buildings at the given time.
* The buildings are ordered by strength.
* Notice that there are always 2 groups of these count spawned, one for each side!
* The units should do a walk-attack to random player CCs
*/
var jebelBarkal_attackerGroup_balancing = [
{
// This should be the most influential building
"buildingClasses": ["Wonder"],
"unitCount": time => scaleByTime(time, 0, 85),
"unitComposition": (time, heroes) => [
{
"templates": jebelBarkal_templates.heroes,
"count": randBool(scaleByTime(time, -0.5, 2)) ? 1 : 0,
"unique_entities": heroes
},
{
"templates": jebelBarkal_templates.healers,
"frequency": randFloat(0, 0.1)
},
{
"templates": jebelBarkal_templates.champions,
"frequency": scaleByTime(time, 0, 0.6)
},
{
"templates": jebelBarkal_templates.champion_infantry_ranged,
"frequency": scaleByTime(time, 0, 0.4)
},
{
"templates": jebelBarkal_templates.citizenSoldiers,
"frequency": scaleByTime(time, 1, 0)
},
{
"templates": jebelBarkal_templates.citizenSoldier_infantry_ranged,
"frequency": scaleByTime(time, 1, 0)
}
],
"formations": jebelBarkal_formations,
"targetClasses": () => "Unit+!Ship"
},
{
"buildingClasses": ["Fortress"],
"unitCount": time => scaleByTime(time, 0, 45),
"unitComposition": (time, heroes) => [
{
"templates": jebelBarkal_templates.heroes,
"count": randBool(scaleByTime(time, -0.5, 1.5)) ? 1 : 0,
"unique_entities": heroes
},
{
"templates": jebelBarkal_templates.champions,
"frequency": scaleByTime(time, 0, 1)
},
{
"templates": jebelBarkal_templates.citizenSoldiers,
"frequency": scaleByTime(time, 1, 0)
}
],
"formations": jebelBarkal_formations,
"targetClasses": () => "Unit+!Ship"
},
{
// These should only train the strongest units
"buildingClasses": ["Temple"],
"unitCount": time => Math.min(45, scaleByTime(time, -30, 90)),
"unitComposition": (time, heroes) => [
{
"templates": jebelBarkal_templates.heroes,
"count": randBool(scaleByTime(time, -0.5, 1)) ? 1 : 0,
"unique_entities": heroes
},
{
"templates": jebelBarkal_templates.champion_infantry_melee,
"frequency": 0.5
},
{
"templates": jebelBarkal_templates.champion_infantry_ranged,
"frequency": 0.5
},
{
"templates": jebelBarkal_templates.healers,
"frequency": randFloat(0.05, 0.2)
}
],
"formations": jebelBarkal_formations,
"targetClasses": () => "Unit+!Ship"
},
{
"buildingClasses": ["CivCentre"],
"unitCount": time => Math.min(40, scaleByTime(time, 0, 80)),
"unitComposition": (time, heroes) => [
{
"templates": jebelBarkal_templates.heroes,
"count": randBool(scaleByTime(time, -0.5, 0.5)) ? 1 : 0,
"unique_entities": heroes
},
{
"templates": jebelBarkal_templates.champion_infantry,
"frequency": scaleByTime(time, 0, 1)
},
{
"templates": jebelBarkal_templates.citizenSoldiers,
"frequency": scaleByTime(time, 1, 0)
}
],
"formations": jebelBarkal_formations,
"targetClasses": () => "Unit+!Ship"
},
{
"buildingClasses": ["Stable"],
"unitCount": time => Math.min(30, scaleByTime(time, 0, 80)),
"unitComposition": (time, heroes) => [
{
"templates": jebelBarkal_templates.citizenSoldier_cavalry_melee,
"frequency": scaleByTime(time, 2, 0)
},
{
"templates": jebelBarkal_templates.champion_cavalry_melee,
"frequency": scaleByTime(time, 0, 1)
}
],
"formations": jebelBarkal_formations,
"targetClasses": () => "Unit+!Ship"
},
{
"buildingClasses": ["Barracks", "Embassy"],
"unitCount": time => Math.min(35, scaleByTime(time, 0, 70)),
"unitComposition": (time, heroes) => [
{
"templates": jebelBarkal_templates.citizenSoldier_infantry,
"frequency": 1
}
],
"formations": jebelBarkal_formations,
"targetClasses": () => "Unit+!Ship"
},
{
"buildingClasses": ["ElephantStable", "Wonder"],
"unitCount": time => scaleByTime(time, 1, 14),
"unitComposition": (time, heroes) => [
{
"templates": jebelBarkal_templates.elephants,
"frequency": 1
}
],
"formations": [],
"targetClasses": () => pickRandom(["Defensive SiegeEngine Monument Wonder", "Structure"])
}
];
Trigger.prototype.debugLog = function(txt)
{
if (showDebugLog)
print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n");
};
Trigger.prototype.JebelBarkal_Init = function()
{
let isNomad = !TriggerHelper.GetAllPlayersEntitiesByClass("CivCentre").length;
this.JebelBarkal_TrackUnits();
this.RegisterTrigger("OnOwnershipChanged", "JebelBarkal_OwnershipChange", { "enabled": true });
this.JebelBarkal_SetDefenderStance();
this.JebelBarkal_StartRitualAnimations();
this.JebelBarkal_GarrisonBuildings();
this.DoAfterDelay(jebelBarkal_firstCityPatrolTime(this.GetDifficulty(), isNomad) * 60 * 1000, "JebelBarkal_SpawnCityPatrolGroups", {});
this.JebelBarkal_StartAttackTimer(jebelBarkal_firstAttackTime(this.GetDifficulty(), isNomad));
};
Trigger.prototype.JebelBarkal_TrackUnits = function()
{
// Each item is an entity ID
this.jebelBarkal_heroes = [];
this.jebelBarkal_ritualHealers = TriggerHelper.GetPlayerEntitiesByClass(jebelBarkal_playerID, "Healer");
// Each item is an array of entity IDs
this.jebelBarkal_patrolingUnits = [];
// Keep track of population limit for attackers
this.jebelBarkal_attackerUnits = [];
// Array of entityIDs where patrol groups can spawn
this.jebelBarkal_patrolGroupSpawnPoints = TriggerHelper.GetPlayerEntitiesByClass(
jebelBarkal_playerID,
jebelBarkal_cityPatrolGroup_balancing.buildingClasses);
this.debugLog("Patrol spawn points: " + uneval(this.jebelBarkal_patrolGroupSpawnPoints));
// Array of entityIDs where attacker groups can spawn
this.jebelBarkal_attackerGroupSpawnPoints = TriggerHelper.GetPlayerEntitiesByClass(
jebelBarkal_playerID,
jebelBarkal_attackerGroup_balancing.reduce((classes, attackerSpawning) => classes.concat(attackerSpawning.buildingClasses), []));
this.numInitialSpawnPoints = this.jebelBarkal_attackerGroupSpawnPoints.length;
this.debugLog("Attacker spawn points: " + uneval(this.jebelBarkal_attackerGroupSpawnPoints));
};
Trigger.prototype.JebelBarkal_SetDefenderStance = function()
{
for (let ent of TriggerHelper.GetPlayerEntitiesByClass(jebelBarkal_playerID, "Human"))
TriggerHelper.SetUnitStance(ent, "defensive");
};
Trigger.prototype.JebelBarkal_StartRitualAnimations = function()
{
this.DoRepeatedly(5 * 1000, "JebelBarkal_UpdateRitualAnimations", {});
};
Trigger.prototype.JebelBarkal_UpdateRitualAnimations = function()
{
for (let ent of this.jebelBarkal_ritualHealers)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE")
continue;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual && jebelBarkal_ritualAnimations.indexOf(cmpVisual.GetAnimationName()) == -1)
cmpVisual.SelectAnimation(pickRandom(jebelBarkal_ritualAnimations), false, 1, "");
}
};
Trigger.prototype.JebelBarkal_GarrisonBuildings = function()
{
for (let buildingGarrison of jebelBarkal_buildingGarrison(this.GetDifficulty()))
TriggerHelper.SpawnAndGarrisonAtClasses(jebelBarkal_playerID, buildingGarrison.buildingClasses, buildingGarrison.unitTemplates, buildingGarrison.capacityRatio);
};
/**
* Spawn new groups if old ones were wiped out.
*/
Trigger.prototype.JebelBarkal_SpawnCityPatrolGroups = function()
{
if (!this.jebelBarkal_patrolGroupSpawnPoints.length)
return;
let time = TriggerHelper.GetMinutes();
let groupCount = Math.floor(Math.max(0, jebelBarkal_cityPatrolGroup_count(time)) - this.jebelBarkal_patrolingUnits.length);
this.debugLog("Spawning " + groupCount + " city patrol groups, " + this.jebelBarkal_patrolingUnits.length + " exist");
for (let i = 0; i < groupCount; ++i)
{
let spawnEnt = pickRandom(this.jebelBarkal_patrolGroupSpawnPoints);
let templateCounts = TriggerHelper.BalancedTemplateComposition(
jebelBarkal_cityPatrolGroup_balancing.unitComposition(time, this.jebelBarkal_heroes),
jebelBarkal_cityPatrolGroup_balancing.unitCount(time));
this.debugLog(uneval(templateCounts));
let groupEntities = this.JebelBarkal_SpawnTemplates(spawnEnt, templateCounts);
this.jebelBarkal_patrolingUnits.push(groupEntities);
for (let ent of groupEntities)
TriggerHelper.SetUnitStance(ent, "defensive");
TriggerHelper.SetUnitFormation(jebelBarkal_playerID, groupEntities, pickRandom(jebelBarkal_formations));
for (let patrolTarget of shuffleArray(this.GetTriggerPoints(jebelBarkal_cityPatrolGroup_triggerPointPath)))
{
let pos = TriggerHelper.GetEntityPosition2D(patrolTarget);
ProcessCommand(jebelBarkal_playerID, {
"type": "patrol",
"entities": groupEntities,
"x": pos.x,
"z": pos.y,
"targetClasses": {
"attack": jebelBarkal_cityPatrolGroup_balancing.targetClasses()
},
"queued": true,
"allowCapture": false
});
}
}
this.DoAfterDelay(jebelBarkal_cityPatrolGroup_interval(time) * 60 * 1000, "JebelBarkal_SpawnCityPatrolGroups", {});
};
Trigger.prototype.JebelBarkal_SpawnTemplates = function(spawnEnt, templateCounts)
{
let groupEntities = [];
for (let templateName in templateCounts)
{
let ents = TriggerHelper.SpawnUnits(spawnEnt, templateName, templateCounts[templateName], jebelBarkal_playerID);
groupEntities = groupEntities.concat(ents);
if (jebelBarkal_templates.heroes.indexOf(templateName) != -1 && ents[0])
this.jebelBarkal_heroes.push(ents[0]);
}
return groupEntities;
};
/**
* Spawn a group of attackers at every remaining building.
*/
Trigger.prototype.JebelBarkal_SpawnAttackerGroups = function()
{
if (!this.jebelBarkal_attackerGroupSpawnPoints)
return;
let time = TriggerHelper.GetMinutes();
this.JebelBarkal_StartAttackTimer(jebelBarkal_attackInterval(time, this.GetDifficulty()));
this.debugLog("Attacker wave (at most " + (jebelBarkal_maxPopulation - this.jebelBarkal_attackerUnits.length) + " attackers)");
let activePlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers();
let playerEntities = activePlayers.map(playerID =>
TriggerHelper.GetEntitiesByPlayer(playerID).filter(TriggerHelper.IsInWorld));
let patrolPoints = this.GetTriggerPoints(jebelBarkal_attackerGroup_triggerPointPatrol);
let groupSizeFactor = jebelBarkal_attackerGroup_sizeFactor(
activePlayers.length,
this.numInitialSpawnPoints,
this.GetDifficulty());
let totalSpawnCount = 0;
for (let spawnPointBalancing of jebelBarkal_attackerGroup_balancing)
{
let targets = playerEntities.reduce((allTargets, playerEnts) =>
allTargets.concat(shuffleArray(TriggerHelper.MatchEntitiesByClass(playerEnts, spawnPointBalancing.targetClasses())).slice(0, 10)), []);
if (!targets.length)
continue;
for (let spawnEnt of TriggerHelper.MatchEntitiesByClass(this.jebelBarkal_attackerGroupSpawnPoints, spawnPointBalancing.buildingClasses))
{
let unitCount = Math.min(
jebelBarkal_maxPopulation - this.jebelBarkal_attackerUnits.length,
groupSizeFactor * spawnPointBalancing.unitCount(time));
// Spawn between 0 and 1 elephants per stable in a 1v1 on a normal mapsize at the beginning
unitCount = Math.floor(unitCount) + (randBool(unitCount % 1) ? 1 : 0);
if (unitCount <= 0)
continue;
let templateCounts = TriggerHelper.BalancedTemplateComposition(spawnPointBalancing.unitComposition(time, this.jebelBarkal_heroes), unitCount);
totalSpawnCount += unitCount;
this.debugLog("Spawning " + unitCount + " attackers at " + uneval(spawnPointBalancing.buildingClasses) + " " +
spawnEnt + ":\n" + uneval(templateCounts));
if (dryRun)
continue;
let spawnedEntities = this.JebelBarkal_SpawnTemplates(spawnEnt, templateCounts);
this.jebelBarkal_attackerUnits = this.jebelBarkal_attackerUnits.concat(spawnedEntities);
let formation = pickRandom(spawnPointBalancing.formations);
if (formation)
TriggerHelper.SetUnitFormation(jebelBarkal_playerID, spawnedEntities, formation);
let entityGroups = formation ? [spawnedEntities] : spawnedEntities.reduce((entityGroup, ent) => entityGroup.concat([[ent]]), []);
for (let i = 0; i < jebelBarkal_patrolPointCount; ++i)
for (let entities of entityGroups)
{
let pos = TriggerHelper.GetEntityPosition2D(pickRandom(i == 0 ? targets : patrolPoints));
ProcessCommand(jebelBarkal_playerID, {
"type": "patrol",
"entities": entities,
"x": pos.x,
"z": pos.y,
"targetClasses": {
"attack": spawnPointBalancing.targetClasses()
},
"queued": true,
"allowCapture": false
});
}
}
}
this.debugLog("Total attackers: " + totalSpawnCount);
if (totalSpawnCount)
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"message": markForTranslation("Napata is attacking!"),
"translateMessage": true
});
};
Trigger.prototype.JebelBarkal_StartAttackTimer = function(delay)
{
let nextAttack = delay * 60 * 1000;
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({
"message": markForTranslation("Napata will attack in %(time)s!"),
"players": [-1, 0],
"translateMessage": true
}, nextAttack);
this.DoAfterDelay(nextAttack, "JebelBarkal_SpawnAttackerGroups", {});
};
/**
* Keep track of heroes, so that each of them remains unique.
* Keep track of spawn points, as only there units should be spawned.
*/
Trigger.prototype.JebelBarkal_OwnershipChange = function(data)
{
if (data.from != 0)
return;
let trackedEntityArrays = [
this.jebelBarkal_heroes,
this.jebelBarkal_ritualHealers,
this.jebelBarkal_patrolGroupSpawnPoints,
this.jebelBarkal_attackerGroupSpawnPoints,
this.jebelBarkal_attackerUnits,
...this.jebelBarkal_patrolingUnits,
];
for (let array of trackedEntityArrays)
{
let idx = array.indexOf(data.entity);
if (idx != -1)
array.splice(idx, 1);
}
this.jebelBarkal_patrolingUnits = this.jebelBarkal_patrolingUnits.filter(entities => entities.length);
};
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).RegisterTrigger("OnInitGame", "JebelBarkal_Init", { "enabled": true });
}