From 85bee9c8e3e287aed8048e44107d9a0dd5e8b789 Mon Sep 17 00:00:00 2001 From: elexis Date: Tue, 6 Mar 2018 13:31:34 +0000 Subject: [PATCH] Refactor and move random template composition triggerscript code used for gaia attacker waves from Danubius (rP19434 / D204) and Survival Of The Fittest (rP19359 / 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 rP20659 / 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 rP21416. git-svn-id: https://svn.wildfiregames.com/public/ps/trunk@21445 3db68df2-c116-0410-a063-a993310a9797 --- .../mods/public/maps/random/danubius_triggers.js | 230 +++++++++------------ .../public/maps/random/elephantine_triggers.js | 27 +-- .../maps/random/survivalofthefittest_triggers.js | 155 +++++--------- .../data/mods/public/maps/scripts/TriggerHelper.js | 145 +++++++++++-- 4 files changed, 289 insertions(+), 268 deletions(-) diff --git a/binaries/data/mods/public/maps/random/danubius_triggers.js b/binaries/data/mods/public/maps/random/danubius_triggers.js index 045dfcb0ad..a2aaf866c3 100644 --- a/binaries/data/mods/public/maps/random/danubius_triggers.js +++ b/binaries/data/mods/public/maps/random/danubius_triggers.js @@ -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)); - - for (let spawn of toSpawn) - { - // 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); + let templateCounts = TriggerHelper.BalancedTemplateComposition( + this.GetAttackerComposition(time, true), + Math.max(0, attackersPerShip(time) - cmpGarrisonHolder.GetEntities().length)); - if (spawn.hero) - this.heroes.push({ "template": spawn.template, "ent": ent }); + this.debugLog("Filling ship " + ship + " with " + uneval(templateCounts)); - cmpGarrisonHolder.Garrison(ent); - } + for (let templateName in templateCounts) + { + 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 }); } diff --git a/binaries/data/mods/public/maps/random/elephantine_triggers.js b/binaries/data/mods/public/maps/random/elephantine_triggers.js index 13af677158..4600ff094a 100644 --- a/binaries/data/mods/public/maps/random/elephantine_triggers.js +++ b/binaries/data/mods/public/maps/random/elephantine_triggers.js @@ -1,29 +1,30 @@ 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)) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (cmpIdentity && cmpIdentity.HasClass("Soldier")) Engine.QueryInterface(ent, IID_UnitAI).SwitchToStance("defensive"); } +}; - let kushSupportUnits = [ - "units/kush_support_healer_e", - "units/kush_support_female_citizen" - ]; - - let kushInfantryUnits = [ - "units/kush_infantry_archer_e", - "units/kush_infantry_spearman_e" - ]; +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); - 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 }); } diff --git a/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js b/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js index 38cecd1218..1688a2336a 100644 --- a/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js +++ b/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js @@ -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])); - - this.debugLog("Spawning " + totalAttackers + " attackers"); - - let attackerTemplates = []; - - // Add hero - if (currentMin > randFloat(...heroTime) && attackerUnitTemplates[civ].heroes.length) - { - this.debugLog("Spawning hero"); - - attackerTemplates.push({ - "template": pickRandom(attackerUnitTemplates[civ].heroes), - "count": 1, - "hero": true - }); - --totalAttackers; - } + initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / maxWaveTime)); - // Random siege to champion ratio - let siegeRatio = randFloat(...siegeFraction); - let siegeCount = Math.round(siegeRatio * totalAttackers); + let siegeRatio = siegeFraction(); - this.debugLog("Siege Ratio: " + Math.round(siegeRatio * 100) + "%"); + this.debugLog("Spawning " + totalAttackers + " attackers, siege ratio " + siegeRatio.toFixed(2)); - 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); - } + 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); - 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) diff --git a/binaries/data/mods/public/maps/scripts/TriggerHelper.js b/binaries/data/mods/public/maps/scripts/TriggerHelper.js index 2b8dcbbe3f..18414bbef6 100644 --- a/binaries/data/mods/public/maps/scripts/TriggerHelper.js +++ b/binaries/data/mods/public/maps/scripts/TriggerHelper.js @@ -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,31 +262,143 @@ TriggerHelper.HasDealtWithTech = function(playerID, techName) }; /** + * 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" + */ +TriggerHelper.GetTemplateNamesByClasses = function(classes, civ, packedState, rank, excludeBarracksVariants) +{ + let templateNames = []; + let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + for (let templateName of cmpTemplateManager.FindAllTemplates(false)) + { + if (templateName.startsWith("campaigns/army_")) + continue; + + 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); + } + + return templateNames; +}; +/** * 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. + * + * @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 } */ -Trigger.prototype.RandomTemplateComposition = function(templates, count) +TriggerHelper.RandomTemplateComposition = function(templateNames, totalCount) { - let ratios = new Array(templates.length).fill(1).map(i => randFloat(0, 1)); - let ratioSum = ratios.reduce((current, sum) => current + sum, 0); + let frequencies = templateNames.map(() => randFloat(0, 1)); + let frequencySum = frequencies.reduce((sum, frequency) => sum + frequency, 0); - let remainder = count; + let remainder = totalCount; let templateCounts = {}; - for (let i = 0; i < templates.length; ++i) + for (let i = 0; i < templateNames.length; ++i) { - let currentCount = i == templates.length - 1 ? remainder : Math.round(ratios[i] / ratioSum * count); - if (!currentCount) + let count = i == templateNames.length - 1 ? remainder : Math.min(remainder, Math.round(frequencies[i] / frequencySum * totalCount)); + if (!count) continue; - templateCounts[templates[i]] = currentCount; - remainder -= currentCount; + 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) - error("Could not chose as many templates as intended: " + count + " vs " + uneval(templateCounts)); + warn("Could not chose as many templates as intended, remaining " + remainder + ", chosen: " + uneval(results)); - return templateCounts; + return results; }; /** @@ -294,14 +406,14 @@ Trigger.prototype.RandomTemplateComposition = function(templates, count) * 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; -- 2.11.4.GIT