Index: ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 21444) +++ ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 21445) @@ -1,686 +1,640 @@ // Ships respawn every few minutes, attack the closest warships, then patrol the sea. // To prevent unlimited spawning of ships, no more than the amount of ships intended at a given time are spawned. // Ships are filled or refilled with new units. // The number of ships, number of units per ship, as well as ratio of siege engines, champion and heroes // increases with time, while keeping an individual and randomized composition for each ship. // Each hero exists at most once per map. // Every few minutes, equal amount of ships unload units at the sides of the river unless // one side of the river was wiped from players. // Siege engines attack defensive structures, units attack units then patrol that side of the river. 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 } ]; /** * Notice if gaia becomes too strong, players will just turtle and try to outlast the players on the other side. * However we want interaction and fights between the teams. * This can be accomplished by not wiping out players buildings entirely. */ /** * Time in minutes between two consecutive waves spawned from the gaia civic centers, if they still exist. */ var ccAttackerInterval = t => randFloat(6, 8); /** * Number of attackers spawned at a civic center at t minutes ingame time. */ var ccAttackerCount = t => Math.min(20, Math.max(0, Math.round(t * 1.5))); /** * Time between two consecutive waves. */ var shipRespawnTime = () => randFloat(8, 10); /** * Limit of ships on the map when spawning them. * Have at least two ships, so that both sides will be visited. */ var shipCount = (t, numPlayers) => Math.max(2, Math.round(Math.min(1.5, t / 10) * numPlayers)); /** * Order all ships to ungarrison at the shoreline. */ var shipUngarrisonInterval = () => randFloat(5, 7); /** * Time between refillings of all ships with new soldiers. */ var shipFillInterval = () => randFloat(4, 5); /** * Total count of gaia attackers per shipload. */ var attackersPerShip = t => Math.min(30, Math.round(t * 2)); /** * Likelihood of adding a non-existing hero at t minutes. */ var heroProbability = t => Math.max(0, Math.min(1, (t - 25) / 60)); /** * Percent of healers to add per shipload after potentially adding a hero and siege engines. */ 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. * Rest will be citizen soldiers. */ var championRatio = t => Math.min(1, Math.max(0, (t - 25) / 75)); /** * Ships and land units will queue attack orders for this amount of closest units. */ var targetCount = 3; /** * Number of trigger points to patrol when not having enemies to attack. */ var patrolCount = 5; /** * Which units ships should focus when attacking and patrolling. */ var shipTargetClass = "Warship"; /** * Which entities siege engines should focus when attacking and patrolling. */ var siegeTargetClass = "Defensive"; /** * Which entities units should focus when attacking and patrolling. */ var unitTargetClass = "Unit+!Ship"; /** * Ungarrison ships when being in this range of the target. */ var shipUngarrisonDistance = 50; /** * Currently formations are not working properly and enemies in vision range are often ignored. * So only have a small chance of using formations. */ var formationProbability = 0.2; var unitFormations = [ "box", "battle_line", "line_closed", "column_closed" ]; /** * Chance for the units at the meeting place to participate in the ritual. */ var ritualProbability = 0.75; /** * Units celebrating at the meeting place will perform one of these animations * if idle and switch back when becoming idle again. */ var ritualAnimations = { "female": ["attack_slaughter"], "male": ["attack_capture", "promotion", "attack_slaughter"], "healer": ["attack_capture", "promotion", "heal"] }; var triggerPointShipSpawn = "A"; var triggerPointShipPatrol = "B"; var triggerPointUngarrisonLeft = "C"; var triggerPointUngarrisonRight = "D"; var triggerPointLandPatrolLeft = "E"; var triggerPointLandPatrolRight = "F"; var triggerPointCCAttackerPatrolLeft = "G"; var triggerPointCCAttackerPatrolRight = "H"; var triggerPointRiverDirection = "I"; /** * Which playerID to use for the opposing gallic reinforcements. */ var gaulPlayer = 0; Trigger.prototype.debugLog = function(txt) { if (showDebugLog) print( "DEBUG [" + Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n"); }; Trigger.prototype.GarrisonAllGallicBuildings = function(gaiaEnts) { this.debugLog("Garrisoning all gallic buildings"); 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)); } }; /** * Spawn units of the template at each gaia Civic Center and set them to defensive. */ Trigger.prototype.SpawnInitialCCDefenders = function(gaiaEnts) { this.debugLog("To defend CCs, spawning " + uneval(ccDefenders)); for (let gaiaEnt of gaiaEnts) { let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity); if (!cmpIdentity || !cmpIdentity.HasClass("CivCentre")) continue; 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"); } }; Trigger.prototype.SpawnCCAttackers = function() { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000; let [spawnLeft, spawnRight] = this.GetActiveRiversides(); for (let gaiaCC of this.civicCenters) { let isLeft = this.IsLeftRiverside(gaiaCC) 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); } let patrolPointRef = isLeft ? triggerPointCCAttackerPatrolLeft : triggerPointCCAttackerPatrolRight; this.AttackAndPatrol(ccAttackers, unitTargetClass, patrolPointRef, "CCAttackers", false); } if (this.civicCenters.length) this.DoAfterDelay(ccAttackerInterval() * 60 * 1000, "SpawnCCAttackers", {}); }; /** * Remember most Humans present at the beginning of the match (before spawning any unit) and * make them defensive. */ Trigger.prototype.StartCelticRitual = function(gaiaEnts) { for (let ent of gaiaEnts) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity || !cmpIdentity.HasClass("Human")) continue; if (randBool(ritualProbability)) this.ritualEnts.push(ent); let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SwitchToStance("defensive"); } this.DoRepeatedly(5 * 1000, "UpdateCelticRitual", {}); }; /** * Play one of the given animations for most participants if and only if they are idle. */ Trigger.prototype.UpdateCelticRitual = function() { for (let ent of this.ritualEnts) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE") continue; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) continue; let animations = ritualAnimations[ cmpIdentity.HasClass("Healer") ? "healer" : cmpIdentity.HasClass("Female") ? "female" : "male"]; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (!cmpVisual) continue; if (animations.indexOf(cmpVisual.GetAnimationName()) == -1) cmpVisual.SelectAnimation(pickRandom(animations), false, 1, ""); } }; /** * Spawn ships with a unique attacker composition each until * the number of ships is reached that is supposed to exist at the given time. */ Trigger.prototype.SpawnShips = function() { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000; let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); let shipSpawnCount = shipCount(time, numPlayers) - this.ships.length; 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"); this.DoAfterDelay(shipRespawnTime(time) * 60 * 1000, "SpawnShips", {}); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.fillShipsTimer); 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() { let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000; for (let ship of this.ships) { let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder); 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]); } } this.fillShipsTimer = this.DoAfterDelay(shipFillInterval() * 60 * 1000, "FillShips", {}); }; /** * Attack the closest enemy ships around, then patrol the sea. */ Trigger.prototype.AttackAndPatrol = function(attackers, targetClass, triggerPointRef, debugName, attack = true) { if (!attackers.length) return; let allTargets = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities().filter(ent => { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClass); }); let targets = allTargets.sort((ent1, ent2) => DistanceBetweenEntities(attackers[0], ent1) - DistanceBetweenEntities(attackers[0], ent2)).slice(0, targetCount); this.debugLog(debugName + " " + uneval(attackers) + " attack " + uneval(targets)); if (attack) for (let target of targets) ProcessCommand(gaulPlayer, { "type": "attack", "entities": attackers, "target": target, "queued": true, "allowCapture": false }); let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount); this.debugLog(debugName + " " + uneval(attackers) + " patrol to " + uneval(patrolTargets)); for (let patrolTarget of patrolTargets) { let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D(); ProcessCommand(gaulPlayer, { "type": "patrol", "entities": attackers, "x": pos.x, "z": pos.y, "targetClasses": { "attack": [targetClass] }, "queued": true, "allowCapture": false }); } }; /** * To avoid unloading unlimited amounts of units on empty riversides, * only add attackers to riversides where player buildings exist that are * actually targeted. */ Trigger.prototype.GetActiveRiversides = function() { let left = false; let right = false; for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities()) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity || !cmpIdentity.HasClass(siegeTargetClass)) continue; if (this.IsLeftRiverside(ent)) left = true; else right = true; if (left && right) break; } return [left, right]; }; Trigger.prototype.IsLeftRiverside = function(ent) { return this.riverDirection.cross(Vector2D.sub(Engine.QueryInterface(ent, IID_Position).GetPosition2D(), this.mapCenter)) > 0; }; /** * Order all ships to abort naval warfare and move to the shoreline all few minutes. */ Trigger.prototype.UngarrisonShipsOrder = function() { let [ungarrisonLeft, ungarrisonRight] = this.GetActiveRiversides(); if (!ungarrisonLeft && !ungarrisonRight) return; // Determine which ships should ungarrison on which side of the river let shipsLeft = []; let shipsRight = []; if (ungarrisonLeft && ungarrisonRight) { shipsLeft = shuffleArray(this.ships).slice(0, Math.round(this.ships.length / 2)); shipsRight = this.ships.filter(ship => shipsLeft.indexOf(ship) == -1); } else if (ungarrisonLeft) shipsLeft = this.ships; else if (ungarrisonRight) shipsRight = this.ships; // Determine which ships should ungarrison and patrol at which trigger point names let sides = []; if (shipsLeft.length) sides.push({ "ships": shipsLeft, "ungarrisonPointRef": triggerPointUngarrisonLeft, "landPointRef": triggerPointLandPatrolLeft }); if (shipsRight.length) sides.push({ "ships": shipsRight, "ungarrisonPointRef": triggerPointUngarrisonRight, "landPointRef": triggerPointLandPatrolRight }); // Order those ships to move to a randomly chosen trigger point on the determined // side of the river. Remember that chosen ungarrison point and the name of the // trigger points where the ungarrisoned units should patrol afterwards. for (let side of sides) for (let ship of side.ships) { let ungarrisonPoint = pickRandom(this.GetTriggerPoints(side.ungarrisonPointRef)); let ungarrisonPos = Engine.QueryInterface(ungarrisonPoint, IID_Position).GetPosition2D(); this.debugLog("Ship " + ship + " will ungarrison at " + side.ungarrisonPointRef + " (" + ungarrisonPos.x + "," + ungarrisonPos.y + ")"); Engine.QueryInterface(ship, IID_UnitAI).Walk(ungarrisonPos.x, ungarrisonPos.y, false); this.shipTarget[ship] = { "landPointRef": side.landPointRef, "ungarrisonPoint": ungarrisonPoint }; } this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {}); }; /** * Check frequently whether the ships are close enough to unload at the shoreline. */ Trigger.prototype.CheckShipRange = function() { for (let ship of this.ships) { if (!this.shipTarget[ship] || DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance) continue; let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder); if (!cmpGarrisonHolder) continue; let attackers = cmpGarrisonHolder.GetEntities(); let siegeEngines = attackers.filter(ent => Engine.QueryInterface(ent, IID_Identity).HasClass("Siege")); let others = attackers.filter(ent => siegeEngines.indexOf(ent) == -1); this.debugLog("Ungarrisoning ship " + ship + " at " + uneval(this.shipTarget[ship])); cmpGarrisonHolder.UnloadAll(); if (randBool(formationProbability)) ProcessCommand(gaulPlayer, { "type": "formation", "entities": others, "name": "special/formations/" + pickRandom(unitFormations) }); this.AttackAndPatrol(siegeEngines, siegeTargetClass, this.shipTarget[ship].landPointRef, "Siege"); this.AttackAndPatrol(others, unitTargetClass, this.shipTarget[ship].landPointRef, "Units"); delete this.shipTarget[ship]; this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships"); } }; Trigger.prototype.DanubiusOwnershipChange = function(data) { if (data.from != 0) return; let shipIdx = this.ships.indexOf(data.entity); if (shipIdx != -1) { this.debugLog("Ship " + data.entity + " sunk"); this.ships.splice(shipIdx, 1); } let ritualIdx = this.ritualEnts.indexOf(data.entity); 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); let ccIdx = this.civicCenters.indexOf(data.entity); if (ccIdx != -1) { this.debugLog("Gaia civic center " + data.entity + " destroyed or captured"); this.civicCenters.splice(ccIdx, 1); } }; - +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); - - cmpTrigger.StartCelticRitual(gaiaEnts); - cmpTrigger.GarrisonAllGallicBuildings(gaiaEnts); - cmpTrigger.SpawnInitialCCDefenders(gaiaEnts); - cmpTrigger.SpawnCCAttackers(gaiaEnts); - - cmpTrigger.SpawnShips(); - cmpTrigger.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {}); - cmpTrigger.DoRepeatedly(5 * 1000, "CheckShipRange", {}); + this.mapCenter = new Vector2D(mapSize / 2, mapSize / 2); + this.riverDirection = Vector2D.sub( + Engine.QueryInterface(this.GetTriggerPoints(triggerPointRiverDirection)[0], IID_Position).GetPosition2D(), + this.mapCenter); + + let gaiaEnts = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0); + this.StartCelticRitual(gaiaEnts); + this.GarrisonAllGallicBuildings(gaiaEnts); + this.SpawnInitialCCDefenders(gaiaEnts); + this.SpawnCCAttackers(); + + 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 }); } Index: ps/trunk/binaries/data/mods/public/maps/random/elephantine_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/elephantine_triggers.js (revision 21444) +++ ps/trunk/binaries/data/mods/public/maps/random/elephantine_triggers.js (revision 21445) @@ -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 }); } Index: ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js (revision 21444) +++ ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js (revision 21445) @@ -1,368 +1,323 @@ /** * 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 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. */ var initialAttackers = 5; /** * Increase the number of attackers exponentially, by this percent value per minute. */ 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. */ var disabledTemplates = (civ) => [ // Economic structures "structures/" + civ + "_corral", "structures/" + civ + "_farmstead", "structures/" + civ + "_field", "structures/" + civ + "_storehouse", "structures/" + civ + "_rotarymill", "units/maur_support_elephant", // Expansions "structures/" + civ + "_civil_centre", "structures/" + civ + "_military_colony", // Walls "structures/" + civ + "_wallset_stone", "structures/rome_wallset_siege", "other/wallset_palisade", // Shoreline "structures/" + civ + "_dock", "structures/brit_crannog", "structures/cart_super_dock", "structures/ptol_lighthouse" ]; /** * Spawn these treasures in regular intervals. */ var treasures = [ "gaia/treasure/food_barrel", "gaia/treasure/food_bin", "gaia/treasure/food_crate", "gaia/treasure/food_jars", "gaia/treasure/metal", "gaia/treasure/stone", "gaia/treasure/wood", "gaia/treasure/wood", "gaia/treasure/wood" ]; /** * An object that maps from civ [f.e. "spart"] to an object * that has the keys "champions", "siege" and "heroes", * which is an array containing all these templates, * trainable from a building or not. */ var attackerUnitTemplates = {}; Trigger.prototype.InitSurvival = function() { this.InitStartingUnits(); this.LoadAttackerTemplates(); this.SetDisableTemplates(); this.PlaceTreasures(); this.InitializeEnemyWaves(); }; Trigger.prototype.debugLog = function(txt) { if (!debugLog) return; print("DEBUG [" + Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n"); }; 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)); }; Trigger.prototype.SetDisableTemplates = function() { for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i) { let cmpPlayer = QueryPlayerIDInterface(i); cmpPlayer.SetDisabledTemplates(disabledTemplates(cmpPlayer.GetCiv())); } }; /** * Remember civic centers and make women invincible. */ Trigger.prototype.InitStartingUnits = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i) { let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i); for (let entity of playerEntities) { if (TriggerHelper.EntityMatchesClassList(entity, "CivilCentre")) this.playerCivicCenter[i] = entity; else if (TriggerHelper.EntityMatchesClassList(entity, "FemaleCitizen")) { this.treasureFemale[i] = entity; let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(true); } } } }; 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 }, time); this.DoAfterDelay(time, "StartAnEnemyWave", {}); }; 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")) { if (dryRun) { spawned = true; 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) { this.debugLog("Not spawning hero for player " + playerID + " as the previous one is still alive"); continue; } } 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, "x": targetPos.x, "z": targetPos.y, "targetClasses": undefined, "allowCapture": false, "queued": true }); - if (attackerTemplate.hero) + if (isHero) this.gaiaHeroes[playerID] = entities[0]; } spawned = true; } 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", {}); }; Trigger.prototype.PlaceTreasures = function() { let point = pickRandom(["B", "C", "D"]); let triggerPoints = this.GetTriggerPoints(point); 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) { if (data.entity == this.playerCivicCenter[data.from]) { this.playerCivicCenter[data.from] = undefined; TriggerHelper.DefeatPlayer( data.from, markForTranslation("%(player)s has been defeated (lost civic center).")); } else if (data.entity == this.treasureFemale[data.from]) { this.treasureFemale[data.from] = undefined; Engine.DestroyEntity(data.entity); } }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.treasureFemale = []; cmpTrigger.playerCivicCenter = []; cmpTrigger.gaiaHeroes = []; cmpTrigger.RegisterTrigger("OnInitGame", "InitSurvival", { "enabled": true }); cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js (revision 21444) +++ ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js (revision 21445) @@ -1,322 +1,433 @@ // Contains standardized functions suitable for using in trigger scripts. // Do not use them in any other simulation script. var TriggerHelper = {}; TriggerHelper.GetPlayerIDFromEntity = function(ent) { let cmpPlayer = Engine.QueryInterface(ent, IID_Player); if (cmpPlayer) return cmpPlayer.GetPlayerID(); return -1; }; TriggerHelper.GetOwner = function(ent) { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) return cmpOwnership.GetOwner(); return -1; }; /** * Can be used to "force" a building/unit to spawn a group of entities. * * @param source Entity id of the point where they will be spawned from * @param template Name of the template * @param count Number of units to spawn * @param owner Player id of the owner of the new units. By default, the owner * of the source entity. */ TriggerHelper.SpawnUnits = function(source, template, count, owner) { let entities = []; let cmpFootprint = Engine.QueryInterface(source, IID_Footprint); let cmpPosition = Engine.QueryInterface(source, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) { error("tried to create entity from a source without position"); return entities; } if (owner == null) owner = TriggerHelper.GetOwner(source); for (let i = 0; i < count; ++i) { let ent = Engine.AddEntity(template); let cmpEntPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpEntPosition) { Engine.DestroyEntity(ent); error("tried to create entity without position"); continue; } let cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpEntOwnership) cmpEntOwnership.SetOwner(owner); entities.push(ent); let pos; if (cmpFootprint) pos = cmpFootprint.PickSpawnPoint(ent); // TODO this can happen if the player build on the place // where our trigger point is // We should probably warn the trigger maker in some way, // but not interrupt the game for the player if (!pos || pos.y < 0) pos = cmpPosition.GetPosition(); cmpEntPosition.JumpTo(pos.x, pos.z); } return entities; }; /** * Can be used to spawn garrisoned units inside a building/ship. * * @param entity Entity id of the garrison holder in which units will be garrisoned * @param template Name of the template * @param count Number of units to spawn * @param owner Player id of the owner of the new units. By default, the owner * of the garrisonholder entity. */ TriggerHelper.SpawnGarrisonedUnits = function(entity, template, count, owner) { let entities = []; let cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder); if (!cmpGarrisonHolder) { error("tried to create garrisoned entities inside a non-garrisonholder"); return entities; } if (owner == null) owner = TriggerHelper.GetOwner(entity); for (let i = 0; i < count; ++i) { let ent = Engine.AddEntity(template); let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(owner); if (cmpGarrisonHolder.PerformGarrison(ent)) { if (Engine.QueryInterface(ent, IID_UnitAI)) Engine.QueryInterface(ent, IID_UnitAI).Autogarrison(entity); entities.push(ent); } else - error("failed to garrison entity " + template + " inside " + entity); + error("failed to garrison entity " + ent + " (" + template + ") inside " + entity); } return entities; }; /** * Spawn units from all trigger points with this reference * If player is defined, only spaw units from the trigger points * that belong to that player * @param ref Trigger point reference name to spawn units from * @param template Template name * @param count Number of spawned entities per Trigger point * @param owner Owner of the spawned units. Default: the owner of the origins * @return A list of new entities per origin like * {originId1: [entId1, entId2], originId2: [entId3, entId4], ...} */ TriggerHelper.SpawnUnitsFromTriggerPoints = function(ref, template, count, owner = null) { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); let triggerPoints = cmpTrigger.GetTriggerPoints(ref); let entities = {}; for (let point of triggerPoints) entities[point] = TriggerHelper.SpawnUnits(point, template, count, owner); return entities; }; /** * Returns the resource type that can be gathered from an entity */ TriggerHelper.GetResourceType = function(entity) { let cmpResourceSupply = Engine.QueryInterface(entity, IID_ResourceSupply); if (!cmpResourceSupply) return undefined; return cmpResourceSupply.GetType(); }; /** * The given player will win the game. * If it's not a last man standing game, then allies will win too and others will be defeated. * * @param {number} playerID - The player who will win. * @param {function} victoryReason - Function that maps from number to plural string, for example * n => markForPluralTranslation( * "%(lastPlayer)s has won (game mode).", * "%(players)s and %(lastPlayer)s have won (game mode).", * n)); * It's a function since we don't know in advance how many players will have won. */ TriggerHelper.SetPlayerWon = function(playerID, victoryReason, defeatReason) { let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); cmpEndGameManager.MarkPlayerAndAlliesAsWon(playerID, victoryReason, defeatReason); }; /** * Defeats a single player. * * @param {number} - ID of that player. * @param {string} - String to be shown in chat, for example * markForTranslation("%(player)s has been defeated (objective).") */ TriggerHelper.DefeatPlayer = function(playerID, defeatReason) { let cmpPlayer = QueryPlayerIDInterface(playerID); if (cmpPlayer) cmpPlayer.SetState("defeated", defeatReason); }; /** * Returns the number of current players */ TriggerHelper.GetNumberOfPlayers = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); }; /** * A function to determine if an entity matches specific classes. * See globalscripts/Templates.js for details of MatchesClassList. * * @param entity - ID of the entity that we want to check for classes. * @param classlist - List of the classes we are checking if the entity matches. */ TriggerHelper.EntityMatchesClassList = function(entity, classlist) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classlist); }; /** * Return valid gaia-owned spawn points on land in neutral territory. * If there are none, use those available in player-owned territory. */ TriggerHelper.GetLandSpawnPoints = function() { let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let neutralSpawnPoints = []; let nonNeutralSpawnPoints = []; for (let ent of cmpRangeManager.GetEntitiesByPlayer(0)) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpIdentity || !cmpPosition || !cmpPosition.IsInWorld()) continue; let templateName = cmpTemplateManager.GetCurrentTemplateName(ent); if (!templateName) continue; let template = cmpTemplateManager.GetTemplate(templateName); if (!template || template.UnitMotionFlying) continue; let pos = cmpPosition.GetPosition(); if (pos.y <= cmpWaterManager.GetWaterLevel(pos.x, pos.z)) continue; if (cmpTerritoryManager.GetOwner(pos.x, pos.z) == 0) neutralSpawnPoints.push(ent); else nonNeutralSpawnPoints.push(ent); } return neutralSpawnPoints.length ? neutralSpawnPoints : nonNeutralSpawnPoints; }; TriggerHelper.HasDealtWithTech = function(playerID, techName) { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let playerEnt = cmpPlayerManager.GetPlayerByID(playerID); let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); return cmpTechnologyManager && (cmpTechnologyManager.IsTechnologyQueued(techName) || cmpTechnologyManager.IsTechnologyStarted(techName) || cmpTechnologyManager.IsTechnologyResearched(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; }; /** * 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); if (!cmpGarrisonHolder) continue; // TODO: account for already garrisoned entities 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; }; Engine.RegisterGlobal("TriggerHelper", TriggerHelper);