1
0
forked from mirrors/0ad

Refactor and move random template composition triggerscript code used for gaia attacker waves from Danubius (026dcf10eb / D204) and Survival Of The Fittest (18e7d8a518 / D145) to the TriggerHelper.

Eases implementation of new maps with diverse scripted attackers, refs
#5040, D11 and  map difficulties, refs #4963, D1189.
Replaces hardcoded templatenames with calls to a new TriggerHelper
function to query template names, given Classes, Civ, Rank or Packed
state.
Removes the duplicated template counting logic, that was intertwined
with map specific unit classes balancing logic, refs #4805.

Use mimos garrison function from e29dfb7000 / D1146 to support
doubleclicking on garrisoned gaia heroes on Danubius, fixing the bug
described in comment:10 of #4291.
Fix wrong (Trigger) prototype reference in ce65af8dcb.

This was SVN commit r21445.
This commit is contained in:
elexis
2018-03-06 13:31:34 +00:00
parent d8b5439956
commit 6113edd746
4 changed files with 293 additions and 272 deletions
@@ -12,63 +12,42 @@
const showDebugLog = false;
var shipTemplate = "units/gaul_ship_trireme";
var siegeTemplate = "units/gaul_mechanical_siege_ram";
var heroTemplates = [
"units/gaul_hero_viridomarus",
"units/gaul_hero_vercingetorix",
"units/gaul_hero_brennus"
];
var femaleTemplate = "units/gaul_support_female_citizen";
var healerTemplate = "units/gaul_support_healer_b";
var citizenInfantryTemplates = [
"units/gaul_infantry_javelinist_b",
"units/gaul_infantry_spearman_b",
"units/gaul_infantry_slinger_b"
];
var citizenCavalryTemplates = [
"units/gaul_cavalry_javelinist_b",
"units/gaul_cavalry_swordsman_b"
];
var citizenTemplates = [...citizenInfantryTemplates, ...citizenCavalryTemplates];
var championInfantryTemplates = [
"units/gaul_champion_fanatic",
"units/gaul_champion_infantry"
];
var championCavalryTemplates = [
"units/gaul_champion_cavalry"
];
var championTemplates = [...championInfantryTemplates, ...championCavalryTemplates];
const danubiusAttackerTemplates = deepfreeze({
"ships": TriggerHelper.GetTemplateNamesByClasses("Warship", "gaul", undefined, undefined, true),
"siege": TriggerHelper.GetTemplateNamesByClasses("Siege","gaul", undefined, undefined, true),
"females": TriggerHelper.GetTemplateNamesByClasses("FemaleCitizen","gaul", undefined, undefined, true),
"healers": TriggerHelper.GetTemplateNamesByClasses("Healer","gaul", undefined, undefined, true),
"champions": TriggerHelper.GetTemplateNamesByClasses("Champion", "gaul", undefined, undefined, true),
"champion_infantry": TriggerHelper.GetTemplateNamesByClasses("Champion+Infantry", "gaul", undefined, undefined, true),
"citizen_soldiers": TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier", "gaul", undefined, "Basic", true),
"heroes": [
// Excludes the Vercingetorix variant
"units/gaul_hero_viridomarus",
"units/gaul_hero_vercingetorix",
"units/gaul_hero_brennus"
]
});
var ccDefenders = [
{ "count": 8, "template": pickRandom(citizenInfantryTemplates) },
{ "count": 8, "template": pickRandom(championInfantryTemplates) },
{ "count": 4, "template": pickRandom(championCavalryTemplates) },
{ "count": 4, "template": healerTemplate },
{ "count": 5, "template": femaleTemplate },
{ "count": 10, "template": "gaia/fauna_sheep" }
{ "count": 8, "templates": danubiusAttackerTemplates.citizen_soldiers },
{ "count": 13, "templates": danubiusAttackerTemplates.champions },
{ "count": 4, "templates": danubiusAttackerTemplates.healers },
{ "count": 5, "templates": danubiusAttackerTemplates.females },
{ "count": 10, "templates": ["gaia/fauna_sheep"] }
];
var gallicBuildingGarrison = [
{
"buildingClasses": ["House"],
"unitTemplates": [femaleTemplate, healerTemplate]
"unitTemplates": danubiusAttackerTemplates.females.concat(danubiusAttackerTemplates.healers)
},
{
"buildingClasses": ["CivCentre", "Temple"],
"unitTemplates": championTemplates
"unitTemplates": danubiusAttackerTemplates.champions,
},
{
"buildingClasses": ["DefenseTower", "Outpost"],
"unitTemplates": championInfantryTemplates
"unitTemplates": danubiusAttackerTemplates.champion_infantry
}
];
@@ -125,9 +104,9 @@ var heroProbability = t => Math.max(0, Math.min(1, (t - 25) / 60));
var healerRatio = t => randFloat(0, 0.1);
/**
* Percent of siege engines to add per shipload.
* Number of siege engines to add per shipload.
*/
var siegeRatio = t => t < 8 ? 0 : randFloat(0.03, 0.06);
var siegeCount = t => 1 + Math.min(2, Math.floor(t / 30));
/**
* Percent of champions to be added after spawning heroes, healers and siege engines.
@@ -223,7 +202,7 @@ Trigger.prototype.GarrisonAllGallicBuildings = function(gaiaEnts)
for (let buildingGarrison of gallicBuildingGarrison)
for (let buildingClass of buildingGarrison.buildingClasses)
{
let unitCounts = this.SpawnAndGarrison(gaulPlayer, buildingClass, buildingGarrison.unitTemplates, 1);
let unitCounts = TriggerHelper.SpawnAndGarrisonAtClasses(gaulPlayer, buildingClass, buildingGarrison.unitTemplates, 1);
this.debugLog("Garrisoning at " + buildingClass + ": " + uneval(unitCounts));
}
};
@@ -244,7 +223,7 @@ Trigger.prototype.SpawnInitialCCDefenders = function(gaiaEnts)
this.civicCenters.push(gaiaEnt);
for (let ccDefender of ccDefenders)
for (let ent of TriggerHelper.SpawnUnits(gaiaEnt, ccDefender.template, ccDefender.count, gaulPlayer))
for (let ent of TriggerHelper.SpawnUnits(gaiaEnt, pickRandom(ccDefender.templates), ccDefender.count, gaulPlayer))
Engine.QueryInterface(ent, IID_UnitAI).SwitchToStance("defensive");
}
};
@@ -261,17 +240,17 @@ Trigger.prototype.SpawnCCAttackers = function()
if (isLeft && !spawnLeft || !isLeft && !spawnRight)
continue;
let toSpawn = this.GetAttackerComposition(ccAttackerCount(time), false);
this.debugLog("Spawning civic center attackers at " + gaiaCC + ": " + uneval(toSpawn));
let templateCounts = TriggerHelper.BalancedTemplateComposition(this.GetAttackerComposition(time, false), ccAttackerCount(time));
this.debugLog("Spawning civic center attackers at " + gaiaCC + ": " + uneval(templateCounts));
let ccAttackers = [];
for (let spawn of toSpawn)
for (let templateName in templateCounts)
{
let ents = TriggerHelper.SpawnUnits(gaiaCC, spawn.template, spawn.count, gaulPlayer);
let ents = TriggerHelper.SpawnUnits(gaiaCC, templateName, templateCounts[templateName], gaulPlayer);
if (spawn.hero && ents[0])
this.heroes.push({ "template": spawn.template, "ent": ents[0] });
if (danubiusAttackerTemplates.heroes.indexOf(templateName) != -1 && ents[0])
this.heroes.push(ents[0]);
ccAttackers = ccAttackers.concat(ents);
}
@@ -351,7 +330,7 @@ Trigger.prototype.SpawnShips = function()
this.debugLog("Spawning " + shipSpawnCount + " ships");
while (this.ships.length < shipSpawnCount)
this.ships.push(TriggerHelper.SpawnUnits(pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)), shipTemplate, 1, gaulPlayer)[0]);
this.ships.push(TriggerHelper.SpawnUnits(pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)), pickRandom(danubiusAttackerTemplates.ships), 1, gaulPlayer)[0]);
for (let ship of this.ships)
this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships");
@@ -364,51 +343,32 @@ Trigger.prototype.SpawnShips = function()
this.FillShips();
};
Trigger.prototype.GetAttackerComposition = function(attackerCount, addSiege)
Trigger.prototype.GetAttackerComposition = function(time, siegeEngines)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
let toSpawn = [];
let remainder = attackerCount;
let siegeCount = addSiege ? Math.round(siegeRatio(time) * remainder) : 0;
if (siegeCount)
toSpawn.push({ "template": siegeTemplate, "count": siegeCount });
remainder -= siegeCount;
let heroTemplate = pickRandom(heroTemplates.filter(hTemp => this.heroes.every(hero => hTemp != hero.template)));
if (heroTemplate && remainder && randBool(heroProbability(time)))
{
toSpawn.push({ "template": heroTemplate, "count": 1, "hero": true });
--remainder;
}
let healerCount = Math.round(healerRatio(time) * remainder);
if (healerCount)
toSpawn.push({ "template": healerTemplate, "count": healerCount });
remainder -= healerCount;
let championCount = Math.round(championRatio(time) * remainder);
let championTemplateCounts = this.RandomTemplateComposition(championTemplates, championCount);
for (let template in championTemplateCounts)
{
let count = championTemplateCounts[template];
toSpawn.push({ "template": template, "count": count });
championCount -= count;
remainder -= count;
}
let citizenTemplateCounts = this.RandomTemplateComposition(citizenTemplates, remainder);
for (let template in citizenTemplateCounts)
{
let count = citizenTemplateCounts[template];
toSpawn.push({ "template": template, "count": count });
remainder -= count;
}
if (remainder != 0)
warn("Didn't spawn as many attackers as were intended (" + remainder + " remaining)");
return toSpawn;
let champRatio = championRatio(time);
return [
{
"templates": danubiusAttackerTemplates.heroes,
"count": randBool(heroProbability(time)) ? 1 : 0,
"unique_entities": this.heroes
},
{
"templates": danubiusAttackerTemplates.siege,
"count": siegeEngines ? siegeCount(time) : 0
},
{
"templates": danubiusAttackerTemplates.healers,
"frequency": healerRatio(time)
},
{
"templates": danubiusAttackerTemplates.champions,
"frequency": champRatio
},
{
"templates": danubiusAttackerTemplates.citizen_soldiers,
"frequency": 1 - champRatio
}
];
};
Trigger.prototype.FillShips = function()
@@ -420,24 +380,17 @@ Trigger.prototype.FillShips = function()
if (!cmpGarrisonHolder)
continue;
let toSpawn = this.GetAttackerComposition(Math.max(0, attackersPerShip(time) - cmpGarrisonHolder.GetEntities().length), true);
this.debugLog("Filling ship " + ship + " with " + uneval(toSpawn));
let templateCounts = TriggerHelper.BalancedTemplateComposition(
this.GetAttackerComposition(time, true),
Math.max(0, attackersPerShip(time) - cmpGarrisonHolder.GetEntities().length));
for (let spawn of toSpawn)
this.debugLog("Filling ship " + ship + " with " + uneval(templateCounts));
for (let templateName in templateCounts)
{
// Don't use TriggerHelper.SpawnUnits here because that is too slow,
// needlessly trying all spawn points near the ships footprint which all fail
for (let i = 0; i < spawn.count; ++i)
{
let ent = Engine.AddEntity(spawn.template);
Engine.QueryInterface(ent, IID_Ownership).SetOwner(gaulPlayer);
if (spawn.hero)
this.heroes.push({ "template": spawn.template, "ent": ent });
cmpGarrisonHolder.Garrison(ent);
}
let ents = TriggerHelper.SpawnGarrisonedUnits(ship, templateName, templateCounts[templateName], gaulPlayer);
if (danubiusAttackerTemplates.heroes.indexOf(templateName) != -1 && ents[0])
this.heroes.push(ents[0]);
}
}
@@ -635,7 +588,7 @@ Trigger.prototype.DanubiusOwnershipChange = function(data)
if (ritualIdx != -1)
this.ritualEnts.splice(ritualIdx, 1);
let heroIdx = this.heroes.findIndex(hero => hero.ent == data.entity);
let heroIdx = this.heroes.findIndex(ent => ent == data.entity);
if (ritualIdx != -1)
this.heroes.splice(heroIdx, 1);
@@ -647,40 +600,41 @@ Trigger.prototype.DanubiusOwnershipChange = function(data)
}
};
Trigger.prototype.InitDanubius = function()
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
let gaiaEnts = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0);
cmpTrigger.ritualEnts = [];
this.ritualEnts = [];
// To prevent spawning more than the limits, track IDs of current entities
cmpTrigger.ships = [];
cmpTrigger.heroes = [];
this.ships = [];
this.heroes = [];
// Remember gaia CCs to spawn attackers from
cmpTrigger.civicCenters = [];
this.civicCenters = [];
// Maps from gaia ship entity ID to ungarrison trigger point entity ID and land patrol triggerpoint name
cmpTrigger.shipTarget = {};
cmpTrigger.fillShipsTimer = undefined;
this.shipTarget = {};
this.fillShipsTimer = undefined;
// Be able to distinguish between the left and right riverside
let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize();
cmpTrigger.mapCenter = new Vector2D(mapSize / 2, mapSize / 2);
cmpTrigger.riverDirection = Vector2D.sub(
Engine.QueryInterface(cmpTrigger.GetTriggerPoints(triggerPointRiverDirection)[0], IID_Position).GetPosition2D(),
cmpTrigger.mapCenter);
this.mapCenter = new Vector2D(mapSize / 2, mapSize / 2);
this.riverDirection = Vector2D.sub(
Engine.QueryInterface(this.GetTriggerPoints(triggerPointRiverDirection)[0], IID_Position).GetPosition2D(),
this.mapCenter);
cmpTrigger.StartCelticRitual(gaiaEnts);
cmpTrigger.GarrisonAllGallicBuildings(gaiaEnts);
cmpTrigger.SpawnInitialCCDefenders(gaiaEnts);
cmpTrigger.SpawnCCAttackers(gaiaEnts);
let gaiaEnts = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0);
this.StartCelticRitual(gaiaEnts);
this.GarrisonAllGallicBuildings(gaiaEnts);
this.SpawnInitialCCDefenders(gaiaEnts);
this.SpawnCCAttackers();
cmpTrigger.SpawnShips();
cmpTrigger.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
cmpTrigger.DoRepeatedly(5 * 1000, "CheckShipRange", {});
this.SpawnShips();
this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {});
this.DoRepeatedly(5 * 1000, "CheckShipRange", {});
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.RegisterTrigger("OnInitGame", "InitDanubius", { "enabled": true });
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DanubiusOwnershipChange", { "enabled": true });
}
@@ -1,4 +1,10 @@
Trigger.prototype.InitElephantine = function()
{
this.InitElephantine_DefenderStance();
this.InitElephantine_GarrisonBuildings();
}
Trigger.prototype.InitElephantine_DefenderStance = function()
{
for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0))
{
@@ -6,24 +12,19 @@ Trigger.prototype.InitElephantine = function()
if (cmpIdentity && cmpIdentity.HasClass("Soldier"))
Engine.QueryInterface(ent, IID_UnitAI).SwitchToStance("defensive");
}
};
let kushSupportUnits = [
"units/kush_support_healer_e",
"units/kush_support_female_citizen"
];
Trigger.prototype.InitElephantine_GarrisonBuildings = function()
{
let kushInfantryUnits = TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier+Infantry", "kush", undefined, "Elite", true);
let kushSupportUnits = TriggerHelper.GetTemplateNamesByClasses("FemaleCitizen Healer", "kush", undefined, "Elite", true);
let kushInfantryUnits = [
"units/kush_infantry_archer_e",
"units/kush_infantry_spearman_e"
];
this.SpawnAndGarrison(0, "Tower", kushInfantryUnits, 1);
TriggerHelper.SpawnAndGarrisonAtClasses(0, "Tower", kushInfantryUnits, 1);
for (let identityClass of ["Wonder", "Temple", "Pyramid"])
this.SpawnAndGarrison(0, identityClass, kushInfantryUnits.concat(kushSupportUnits), 1);
TriggerHelper.SpawnAndGarrisonAtClasses(0, identityClass, kushInfantryUnits.concat(kushSupportUnits), 1);
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.RegisterTrigger("OnInitGame", "InitElephantine", { "enabled": true });
Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).RegisterTrigger("OnInitGame", "InitElephantine", { "enabled": true });
}
@@ -9,19 +9,24 @@ const dryRun = false;
const debugLog = false;
/**
* Least and greatest number of minutes to pass between spawning new treasures.
* Get the number of minutes to pass between spawning new treasures.
*/
var treasureTime = [3, 5];
var treasureTime = () => randFloat(3, 5);
/**
* Earliest and latest time when the first wave of attackers will be spawned.
* Get the time in minutes when the first wave of attackers will be spawned.
*/
var firstWaveTime = [4, 6];
var firstWaveTime = () => randFloat(4, 6);
/**
* Smallest and largest number of minutes between two consecutive waves.
* Maximum time in minutes between two consecutive waves.
*/
var waveTime = [2, 4];
var maxWaveTime = 4;
/**
* Get the next attacker wave delay.
*/
var waveTime = () => randFloat(0.5, 1) * maxWaveTime;
/**
* Roughly the number of attackers on the first wave.
@@ -36,17 +41,17 @@ var percentPerMinute = 1.05;
/**
* Greatest amount of attackers that can be spawned.
*/
var totalAttackerLimit = 150;
var totalAttackerLimit = 200;
/**
* Least and greatest amount of siege engines per wave.
*/
var siegeFraction = [0.2, 0.5];
var siegeFraction = () => randFloat(0.2, 0.5);
/**
* Potentially / definitely spawn a gaia hero after this number of minutes.
*/
var heroTime = [20, 60];
var heroTime = () => randFloat(20, 60);
/**
* The following templates can't be built by any player.
@@ -118,32 +123,12 @@ Trigger.prototype.debugLog = function(txt)
Trigger.prototype.LoadAttackerTemplates = function()
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let templateName of cmpTemplateManager.FindAllTemplates(false))
{
if (!templateName.startsWith("units/") || templateName.endsWith("_unpacked") || templateName.endsWith("_barracks"))
continue;
let identity = cmpTemplateManager.GetTemplate(templateName).Identity;
if (!attackerUnitTemplates[identity.Civ])
attackerUnitTemplates[identity.Civ] = {
"heroes": [],
"champions": [],
"siege": []
};
let classes = GetIdentityClasses(identity);
// Notice some heroes are elephants and war elephants are champions
if (classes.indexOf("Hero") != -1)
attackerUnitTemplates[identity.Civ].heroes.push(templateName);
else if (classes.indexOf("Siege") != -1 || classes.indexOf("Elephant") != -1 && classes.indexOf("Melee") != -1)
attackerUnitTemplates[identity.Civ].siege.push(templateName);
else if (classes.indexOf("Champion") != -1)
attackerUnitTemplates[identity.Civ].champions.push(templateName);
}
for (let civ of ["gaia", ...Object.keys(loadCivFiles(false))])
attackerUnitTemplates[civ] = {
"heroes": TriggerHelper.GetTemplateNamesByClasses("Hero", civ, undefined, true),
"champions": TriggerHelper.GetTemplateNamesByClasses("Champion+!Elephant", civ, undefined, true),
"siege": TriggerHelper.GetTemplateNamesByClasses("Siege Champion+Elephant", civ, "packed", undefined)
};
this.debugLog("Attacker templates:");
this.debugLog(uneval(attackerUnitTemplates));
@@ -185,7 +170,7 @@ Trigger.prototype.InitStartingUnits = function()
Trigger.prototype.InitializeEnemyWaves = function()
{
let time = randFloat(...firstWaveTime) * 60 * 1000;
let time = firstWaveTime() * 60 * 1000;
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({
"message": markForTranslation("The first wave will start in %(time)s!"),
"translateMessage": true
@@ -196,72 +181,39 @@ Trigger.prototype.InitializeEnemyWaves = function()
Trigger.prototype.StartAnEnemyWave = function()
{
let currentMin = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000;
let nextWaveTime = randFloat(...waveTime);
let nextWaveTime = waveTime();
let civ = pickRandom(Object.keys(attackerUnitTemplates));
// Determine total attacker count of the current wave.
// Exponential increase with time, capped to the limit and fluctuating proportionally with the current wavetime.
let totalAttackers = Math.ceil(Math.min(totalAttackerLimit,
initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / waveTime[1]));
initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / maxWaveTime));
this.debugLog("Spawning " + totalAttackers + " attackers");
let siegeRatio = siegeFraction();
let attackerTemplates = [];
this.debugLog("Spawning " + totalAttackers + " attackers, siege ratio " + siegeRatio.toFixed(2));
// Add hero
if (currentMin > randFloat(...heroTime) && attackerUnitTemplates[civ].heroes.length)
{
this.debugLog("Spawning hero");
let attackerCount = TriggerHelper.BalancedTemplateComposition(
[
{
"templates": attackerUnitTemplates[civ].heroes,
"count": currentMin > heroTime() && attackerUnitTemplates[civ].heroes.length ? 1 : 0
},
{
"templates": attackerUnitTemplates[civ].siege,
"frequency": siegeRatio
},
{
"templates": attackerUnitTemplates[civ].champions,
"frequency": 1 - siegeRatio
}
],
totalAttackers);
attackerTemplates.push({
"template": pickRandom(attackerUnitTemplates[civ].heroes),
"count": 1,
"hero": true
});
--totalAttackers;
}
// Random siege to champion ratio
let siegeRatio = randFloat(...siegeFraction);
let siegeCount = Math.round(siegeRatio * totalAttackers);
this.debugLog("Siege Ratio: " + Math.round(siegeRatio * 100) + "%");
let attackerTypeCounts = {
"siege": siegeCount,
"champions": totalAttackers - siegeCount
};
this.debugLog("Spawning:" + uneval(attackerTypeCounts));
// Random ratio of the given templates
for (let attackerType in attackerTypeCounts)
{
let attackerTypeTemplates = attackerUnitTemplates[civ][attackerType];
let attackerEntityRatios = new Array(attackerTypeTemplates.length).fill(1).map(i => randFloat(0, 1));
let attackerEntityRatioSum = attackerEntityRatios.reduce((current, sum) => current + sum, 0);
let remainder = attackerTypeCounts[attackerType];
for (let i in attackerTypeTemplates)
{
let count =
+i == attackerTypeTemplates.length - 1 ?
remainder :
Math.round(attackerEntityRatios[i] / attackerEntityRatioSum * attackerTypeCounts[attackerType]);
attackerTemplates.push({
"template": attackerTypeTemplates[i],
"count": count
});
remainder -= count;
}
if (remainder != 0)
warn("Didn't spawn as many attackers as intended: " + remainder);
}
this.debugLog("Templates: " + uneval(attackerTemplates));
this.debugLog("Templates: " + uneval(attackerCount));
// Spawn the templates
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let spawned = false;
for (let point of this.GetTriggerPoints("A"))
{
@@ -271,23 +223,25 @@ Trigger.prototype.StartAnEnemyWave = function()
break;
}
// Don't spawn attackers for defeated players and players that lost there cc after win
// Don't spawn attackers for defeated players and players that lost their cc after win
let playerID = QueryOwnerInterface(point, IID_Player).GetPlayerID();
let civicCentre = this.playerCivicCenter[playerID];
if (!civicCentre)
continue;
// Check in case the cc is garrisoned in another building
// Check if the cc is garrisoned in another building
let cmpPosition = Engine.QueryInterface(civicCentre, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let targetPos = cmpPosition.GetPosition2D();
for (let attackerTemplate of attackerTemplates)
for (let templateName in attackerCount)
{
let isHero = attackerUnitTemplates[civ].heroes.indexOf(templateName) != -1;
// Don't spawn gaia hero if the previous one is still alive
if (attackerTemplate.hero && this.gaiaHeroes[playerID])
if (this.gaiaHeroes[playerID] && isHero)
{
let cmpHealth = Engine.QueryInterface(this.gaiaHeroes[playerID], IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() != 0)
@@ -300,7 +254,8 @@ Trigger.prototype.StartAnEnemyWave = function()
if (dryRun)
continue;
let entities = TriggerHelper.SpawnUnits(point, attackerTemplate.template, attackerTemplate.count, 0);
let entities = TriggerHelper.SpawnUnits(point, templateName, attackerCount[templateName], 0);
ProcessCommand(0, {
"type": "attack-walk",
"entities": entities,
@@ -311,7 +266,7 @@ Trigger.prototype.StartAnEnemyWave = function()
"queued": true
});
if (attackerTemplate.hero)
if (isHero)
this.gaiaHeroes[playerID] = entities[0];
}
spawned = true;
@@ -320,11 +275,11 @@ Trigger.prototype.StartAnEnemyWave = function()
if (!spawned)
return;
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"message": markForTranslation("An enemy wave is attacking!"),
"translateMessage": true
});
this.DoAfterDelay(nextWaveTime * 60 * 1000, "StartAnEnemyWave", {});
};
@@ -335,7 +290,7 @@ Trigger.prototype.PlaceTreasures = function()
for (let point of triggerPoints)
TriggerHelper.SpawnUnits(point, pickRandom(treasures), 1, 0);
this.DoAfterDelay(randFloat(...treasureTime) * 60 * 1000, "PlaceTreasures", {});
this.DoAfterDelay(treasureTime() * 60 * 1000, "PlaceTreasures", {});
};
Trigger.prototype.OnOwnershipChanged = function(data)
@@ -115,7 +115,7 @@ TriggerHelper.SpawnGarrisonedUnits = function(entity, template, count, owner)
entities.push(ent);
}
else
error("failed to garrison entity " + template + " inside " + entity);
error("failed to garrison entity " + ent + " (" + template + ") inside " + entity);
}
return entities;
@@ -262,46 +262,158 @@ TriggerHelper.HasDealtWithTech = function(playerID, techName)
};
/**
* Composes a random set of the given templates of the given total size.
* Returns an object where the keys are template names and values are amounts.
* Returns all names of templates that match the given identity classes, constrainted to an optional civ.
*
* @param {String} classes - See MatchesClassList for the accepted formats, for example "Class1 Class2+!Class3".
* @param [String] civ - Optionally only retrieve templates of the given civ. Can be left undefined.
* @param [String] packedState - When retrieving siege engines filter for the "packed" or "unpacked" state
* @param [String] rank - If given, only return templates that have no or the given rank. For example "Elite".
* @param [Boolean] excludeBarracksVariants - Optionally exclude templates whose name ends with "_barracks"
*/
Trigger.prototype.RandomTemplateComposition = function(templates, count)
TriggerHelper.GetTemplateNamesByClasses = function(classes, civ, packedState, rank, excludeBarracksVariants)
{
let ratios = new Array(templates.length).fill(1).map(i => randFloat(0, 1));
let ratioSum = ratios.reduce((current, sum) => current + sum, 0);
let remainder = count;
let templateCounts = {};
for (let i = 0; i < templates.length; ++i)
let templateNames = [];
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let templateName of cmpTemplateManager.FindAllTemplates(false))
{
let currentCount = i == templates.length - 1 ? remainder : Math.round(ratios[i] / ratioSum * count);
if (!currentCount)
if (templateName.startsWith("campaigns/army_"))
continue;
templateCounts[templates[i]] = currentCount;
remainder -= currentCount;
if (excludeBarracksVariants && templateName.endsWith("_barracks"))
continue;
let template = cmpTemplateManager.GetTemplate(templateName);
if (civ && (!template.Identity || template.Identity.Civ != civ))
continue;
if (!MatchesClassList(GetIdentityClasses(template.Identity), classes))
continue;
if (rank && template.Identity.Rank && template.Identity.Rank != rank)
continue;
if (packedState && template.Pack && packedState != template.Pack.State)
continue;
templateNames.push(templateName);
}
if (remainder != 0)
error("Could not chose as many templates as intended: " + count + " vs " + uneval(templateCounts));
return templateNames;
};
/**
* Composes a random set of the given templates of the given total size.
*
* @param {String[]} templateNames - for example ["brit_infantry_javelinist_b", "brit_cavalry_swordsman_e"]
* @param {Number} totalCount - total amount of templates, in this example 12
* @returns an object where the keys are template names and values are amounts,
* for example { "brit_infantry_javelinist_b": 4, "brit_cavalry_swordsman_e": 8 }
*/
TriggerHelper.RandomTemplateComposition = function(templateNames, totalCount)
{
let frequencies = templateNames.map(() => randFloat(0, 1));
let frequencySum = frequencies.reduce((sum, frequency) => sum + frequency, 0);
let remainder = totalCount;
let templateCounts = {};
for (let i = 0; i < templateNames.length; ++i)
{
let count = i == templateNames.length - 1 ? remainder : Math.min(remainder, Math.round(frequencies[i] / frequencySum * totalCount));
if (!count)
continue;
templateCounts[templateNames[i]] = count;
remainder -= count;
}
return templateCounts;
};
/**
* Composes a random set of the given templates so that the sum of templates matches totalCount.
* For each template array that has a count item, it choses exactly that number of templates at random.
* The remaining template arrays are chosen depending on the given frequency.
* If a unique_entities array is given, it will only select the template if none of the given entityIDs
* already have that entity (useful to let heroes remain unique).
*
* @param {Object[]} templateBalancing - for example
* [
* { "templates": ["template1", "template2"], "frequency": 2 },
* { "templates": ["template3"], "frequency": 1 },
* { "templates": ["hero1", "hero2"], "unique_entities": [380, 495], "count": 1 }
* ]
* @param {Number} totalCount - total amount of templates, for example 5.
*
* @returns an object where the keys are template names and values are amounts,
* for example { "template1": 1, "template2": 3, "template3": 2, "hero1": 1 }
*/
TriggerHelper.BalancedTemplateComposition = function(templateBalancing, totalCount)
{
// Remove all unavailable unique templates (heroes) and empty template arrays
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let templateBalancingFiltered = [];
for (let templateBalance of templateBalancing)
{
let templateBalanceNew = clone(templateBalance);
if (templateBalanceNew.unique_entities)
templateBalanceNew.templates = templateBalanceNew.templates.filter(templateName =>
templateBalanceNew.unique_entities.every(ent => templateName != cmpTemplateManager.GetCurrentTemplateName(ent)));
if (templateBalanceNew.templates.length)
templateBalancingFiltered.push(templateBalanceNew);
}
// Helper function to add randomized templates to the result
let remainder = totalCount;
let results = {};
let addTemplates = (templateNames, count) => {
let templateCounts = TriggerHelper.RandomTemplateComposition(templateNames, count);
for (let templateName in templateCounts)
{
if (!results[templateName])
results[templateName] = 0;
results[templateName] += templateCounts[templateName]
remainder -= templateCounts[templateName];
}
};
// Add template groups with fixed counts
for (let templateBalance of templateBalancingFiltered)
if (templateBalance.count)
addTemplates(templateBalance.templates, Math.min(remainder, templateBalance.count));
// Add template groups with frequency weights
let templateBalancingFrequencies = templateBalancingFiltered.filter(templateBalance => !!templateBalance.frequency);
let templateBalancingFrequencySum = templateBalancingFrequencies.reduce((sum, templateBalance) => sum + templateBalance.frequency, 0);
for (let i = 0; i < templateBalancingFrequencies.length; ++i)
addTemplates(
templateBalancingFrequencies[i].templates,
i == templateBalancingFrequencies.length - 1 ?
remainder :
Math.min(remainder, Math.round(templateBalancingFrequencies[i].frequency / templateBalancingFrequencySum * totalCount)));
if (remainder != 0)
warn("Could not chose as many templates as intended, remaining " + remainder + ", chosen: " + uneval(results));
return results;
};
/**
* This will spawn random compositions of entities of the given templates at all garrisonholders of the given targetClass of the given player.
* The garrisonholder will be filled to capacityPercent.
* Returns an object where keys are entityIDs of the affected garrisonholders and the properties are template compositions, see RandomTemplateComposition.
*/
Trigger.prototype.SpawnAndGarrison = function(playerID, targetClass, templates, capacityPercent)
TriggerHelper.SpawnAndGarrisonAtClasses = function(playerID, classes, templates, capacityPercent)
{
let results = {};
for (let entGarrisonHolder of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(playerID))
{
let cmpIdentity = Engine.QueryInterface(entGarrisonHolder, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass(targetClass))
if (!cmpIdentity || !MatchesClassList(cmpIdentity.GetClassesList(), classes))
continue;
let cmpGarrisonHolder = Engine.QueryInterface(entGarrisonHolder, IID_GarrisonHolder);
@@ -312,8 +424,7 @@ Trigger.prototype.SpawnAndGarrison = function(playerID, targetClass, templates,
results[entGarrisonHolder] = this.RandomTemplateComposition(templates, Math.floor(cmpGarrisonHolder.GetCapacity() * capacityPercent));
for (let template in results[entGarrisonHolder])
for (let entSpawned of TriggerHelper.SpawnUnits(entGarrisonHolder, template, results[entGarrisonHolder][template], playerID))
Engine.QueryInterface(entGarrisonHolder, IID_GarrisonHolder).Garrison(entSpawned);
TriggerHelper.SpawnGarrisonedUnits(entGarrisonHolder, template, results[entGarrisonHolder][template], playerID);
}
return results;