Index: ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 27936) +++ ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 27937) @@ -1,640 +1,661 @@ // 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; 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), + "infantry_ranged": TriggerHelper.GetTemplateNamesByClasses("Infantry+Ranged", "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, "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": danubiusAttackerTemplates.females.concat(danubiusAttackerTemplates.healers) }, { "buildingClasses": ["CivCentre", "Temple"], "unitTemplates": danubiusAttackerTemplates.champions, }, { - "buildingClasses": ["Tower", "Outpost"], + "buildingClasses": ["Tower"], "unitTemplates": danubiusAttackerTemplates.champion_infantry } ]; +var gallicBuildingTurret = [ + { + "buildingClasses": ["Outpost"], + "unitTemplates": danubiusAttackerTemplates.infantry_ranged + } +]; + /** * 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); /** * Number of siege engines to add per shipload. */ 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)); /** * 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 = [ "special/formations/box", "special/formations/battle_line", "special/formations/line_closed", "special/formations/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(TriggerHelper.GetMinutes()) + "] " + txt + "\n"); }; Trigger.prototype.GarrisonAllGallicBuildings = function() { this.debugLog("Garrisoning all gallic buildings"); for (let buildingGarrison of gallicBuildingGarrison) for (let buildingClass of buildingGarrison.buildingClasses) { let unitCounts = TriggerHelper.SpawnAndGarrisonAtClasses(gaulPlayer, buildingClass, buildingGarrison.unitTemplates, 1); this.debugLog("Garrisoning at " + buildingClass + ": " + uneval(unitCounts)); } }; +Trigger.prototype.TurretAllGallicBuildings = function() +{ + this.debugLog("Turreting all gallic buildings"); + + for (let buildingTurret of gallicBuildingTurret) + for (let buildingClass of buildingTurret.buildingClasses) + { + let unitCounts = TriggerHelper.SpawnAndTurretAtClasses(gaulPlayer, buildingClass, buildingTurret.unitTemplates, 1); + this.debugLog("Turreting at " + buildingClass + ": " + uneval(unitCounts)); + } +}; + /** * Spawn units of the template at each gaia Civic Center and set them to defensive. */ Trigger.prototype.SpawnInitialCCDefenders = function() { this.debugLog("To defend CCs, spawning " + uneval(ccDefenders)); for (let ent of this.civicCenters) for (let ccDefender of ccDefenders) for (let spawnedEnt of TriggerHelper.SpawnUnits(ent, pickRandom(ccDefender.templates), ccDefender.count, gaulPlayer)) TriggerHelper.SetUnitStance(spawnedEnt, "defensive"); }; Trigger.prototype.SpawnCCAttackers = function() { let time = TriggerHelper.GetMinutes(); let [spawnLeft, spawnRight] = this.GetActiveRiversides(); for (let gaiaCC of this.civicCenters) { if (!TriggerHelper.IsInWorld(gaiaCC)) continue; let isLeft = this.IsLeftRiverside(gaiaCC); if (isLeft && !spawnLeft || !isLeft && !spawnRight) continue; let templateCounts = TriggerHelper.BalancedTemplateComposition(this.GetAttackerComposition(time, false), ccAttackerCount(time)); this.debugLog("Spawning civic center attackers at " + gaiaCC + ": " + uneval(templateCounts)); let ccAttackers = []; for (let templateName in templateCounts) { let ents = TriggerHelper.SpawnUnits(gaiaCC, templateName, templateCounts[templateName], gaulPlayer); if (danubiusAttackerTemplates.heroes.indexOf(templateName) != -1 && ents[0]) this.heroes.add(ents[0]); ccAttackers = ccAttackers.concat(ents); } let patrolPointRef = isLeft ? triggerPointCCAttackerPatrolLeft : triggerPointCCAttackerPatrolRight; this.AttackAndPatrol(ccAttackers, unitTargetClass, patrolPointRef, "CCAttackers", false); } if (this.civicCenters.size) 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() { for (let ent of TriggerHelper.GetPlayerEntitiesByClass(gaulPlayer, "Human")) { if (randBool(ritualProbability)) this.ritualEnts.add(ent); TriggerHelper.SetUnitStance(ent, "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 = TriggerHelper.GetMinutes(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers().length; let shipSpawnCount = shipCount(time, numPlayers) - this.ships.size; this.debugLog("Spawning " + shipSpawnCount + " ships"); while (this.ships.size < shipSpawnCount) this.ships.add( TriggerHelper.SpawnUnits( pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)), pickRandom(danubiusAttackerTemplates.ships), 1, gaulPlayer)[0]); for (let ship of this.ships) this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ship", true); 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(time, siegeEngines) { let champRatio = championRatio(time); return [ { "templates": danubiusAttackerTemplates.heroes, "count": randBool(heroProbability(time)) ? 1 : 0, "unique_entities": Array.from(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 = TriggerHelper.GetMinutes(); for (let ship of this.ships) { let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder); if (!cmpGarrisonHolder) continue; let templateCounts = TriggerHelper.BalancedTemplateComposition( this.GetAttackerComposition(time, true), Math.max(0, attackersPerShip(time) - cmpGarrisonHolder.GetEntities().length)); this.debugLog("Filling ship " + ship + " with " + uneval(templateCounts)); for (let templateName in templateCounts) { let ents = TriggerHelper.SpawnGarrisonedUnits(ship, templateName, templateCounts[templateName], gaulPlayer); if (danubiusAttackerTemplates.heroes.indexOf(templateName) != -1 && ents[0]) this.heroes.add(ents[0]); } } this.fillShipsTimer = this.DoAfterDelay(shipFillInterval() * 60 * 1000, "FillShips", {}); }; /** * Attack the closest enemy target around, then patrol the map. */ Trigger.prototype.AttackAndPatrol = function(entities, targetClass, triggerPointRef, debugName, attack) { if (!entities.length) return; let healers = TriggerHelper.MatchEntitiesByClass(entities, "Healer").filter(TriggerHelper.IsInWorld); if (healers.length) { let healerTargets = TriggerHelper.MatchEntitiesByClass(entities, "Hero Champion"); if (!healerTargets.length) healerTargets = TriggerHelper.MatchEntitiesByClass(entities, "Soldier"); ProcessCommand(gaulPlayer, { "type": "guard", "entities": healers, "target": pickRandom(healerTargets), "queued": false }); } let attackers = TriggerHelper.MatchEntitiesByClass(entities, "!Healer").filter(TriggerHelper.IsInWorld); if (!attackers.length) return; let isLeft = this.IsLeftRiverside(attackers[0]); let targets = TriggerHelper.MatchEntitiesByClass(TriggerHelper.GetAllPlayersEntities(), targetClass); let closestTarget; let minDistance = Infinity; for (let target of targets) { if (!TriggerHelper.IsInWorld(target) || this.IsLeftRiverside(target) != isLeft) continue; let targetDistance = PositionHelper.DistanceBetweenEntities(attackers[0], target); if (targetDistance < minDistance) { closestTarget = target; minDistance = targetDistance; } } this.debugLog(debugName + " " + uneval(attackers) + " attack " + uneval(closestTarget)); if (attack && closestTarget) ProcessCommand(gaulPlayer, { "type": "attack", "entities": attackers, "target": closestTarget, "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 targetPos = TriggerHelper.GetEntityPosition2D(patrolTarget); ProcessCommand(gaulPlayer, { "type": "patrol", "entities": attackers, "x": targetPos.x, "z": targetPos.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 TriggerHelper.GetAllPlayersEntitiesByClass(siegeTargetClass)) { if (this.IsLeftRiverside(ent)) left = true; else right = true; if (left && right) break; } return [left, right]; }; Trigger.prototype.IsLeftRiverside = function(ent) { return Vector2D.sub(TriggerHelper.GetEntityPosition2D(ent), this.mapCenter).cross(this.riverDirection) < 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 ships = Array.from(this.ships); let shipsLeft = []; let shipsRight = []; if (ungarrisonLeft && ungarrisonRight) { shipsLeft = shuffleArray(ships).slice(0, Math.round(ships.length / 2)); shipsRight = ships.filter(ship => shipsLeft.indexOf(ship) == -1); } else if (ungarrisonLeft) shipsLeft = ships; else if (ungarrisonRight) shipsRight = 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 = TriggerHelper.GetEntityPosition2D(ungarrisonPoint); 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] || PositionHelper.DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance) continue; let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder); if (!cmpGarrisonHolder) continue; let humans = TriggerHelper.MatchEntitiesByClass(cmpGarrisonHolder.GetEntities(), "Human"); let siegeEngines = TriggerHelper.MatchEntitiesByClass(cmpGarrisonHolder.GetEntities(), "Siege"); this.debugLog("Ungarrisoning ship " + ship + " at " + uneval(this.shipTarget[ship])); cmpGarrisonHolder.UnloadAll(); this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships", true); if (randBool(formationProbability)) TriggerHelper.SetUnitFormation(gaulPlayer, humans, pickRandom(unitFormations)); this.AttackAndPatrol(siegeEngines, siegeTargetClass, this.shipTarget[ship].landPointRef, "Siege Engines", true); // Order soldiers at last, so the follow-player observer feature focuses the soldiers this.AttackAndPatrol(humans, unitTargetClass, this.shipTarget[ship].landPointRef, "Units", true); delete this.shipTarget[ship]; } }; Trigger.prototype.DanubiusOwnershipChange = function(data) { if (data.from != 0) return; if (this.heroes.delete(data.entity)) this.debugLog("Hero " + data.entity + " died"); if (this.ships.delete(data.entity)) this.debugLog("Ship " + data.entity + " sunk"); if (this.civicCenters.delete(data.entity)) this.debugLog("Gaia civic center " + data.entity + " destroyed or captured"); this.ritualEnts.delete(data.entity); }; Trigger.prototype.InitDanubius = function() { // Set a custom animation of idle ritual units frequently this.ritualEnts = new Set(); // To prevent spawning more than the limits, track IDs of current entities this.ships = new Set(); this.heroes = new Set(); // Remember gaia CCs to spawn attackers from this.civicCenters = new Set(TriggerHelper.GetPlayerEntitiesByClass(gaulPlayer, "CivCentre")); // Depends on this.heroes Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).RegisterTrigger("OnOwnershipChanged", "DanubiusOwnershipChange", { "enabled": true }); // Maps from gaia ship entity ID to ungarrison trigger point entity ID and land patrol triggerpoint name this.shipTarget = {}; this.fillShipsTimer = undefined; // Be able to distinguish between the left and right riverside let mapSize = TriggerHelper.GetMapSizeTerrain(); this.mapCenter = new Vector2D(mapSize / 2, mapSize / 2); this.riverDirection = Vector2D.sub( TriggerHelper.GetEntityPosition2D(this.GetTriggerPoints(triggerPointRiverDirection)[0]), this.mapCenter); this.StartCelticRitual(); this.GarrisonAllGallicBuildings(); + this.TurretAllGallicBuildings(); this.SpawnInitialCCDefenders(); this.SpawnCCAttackers(); this.SpawnShips(); this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {}); this.DoRepeatedly(5 * 1000, "CheckShipRange", {}); }; { Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).RegisterTrigger("OnInitGame", "InitDanubius", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal_triggers.js (revision 27936) +++ ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal_triggers.js (revision 27937) @@ -1,641 +1,654 @@ /** * The city is patroled along its paths by infantry champions that respawn reoccuringly. * There are increasingly great gaia attacks started from the different buildings. * The players can destroy gaia buildings to reduce the number of attackers for the future. */ /** * If set to true, it will print how many templates would be spawned if the players were not defeated. */ const dryRun = false; /** * If enabled, prints the number of units to the command line output. */ const showDebugLog = false; /** * Since Gaia doesn't have a TechnologyManager, Advanced and Elite soldiers have the same statistics as Basic. */ var jebelBarkal_rank = "Basic"; /** * Limit the total amount of gaia units spawned for performance reasons. */ var jebelBarkal_maxPopulation = 8 * 150; /** * These are the templates spawned at the gamestart and during the game. */ var jebelBarkal_templateClasses = deepfreeze({ "heroes": "Hero", "champions": "Champion+!Elephant", "elephants": "Champion+Elephant", "champion_infantry": "Champion+Infantry", "champion_infantry_melee": "Champion+Infantry+Melee", "champion_infantry_ranged": "Champion+Infantry+Ranged", "champion_cavalry": "Champion+Cavalry", "champion_cavalry_melee": "Champion+Cavalry+Melee", "citizenSoldiers": "CitizenSoldier", "citizenSoldier_infantry": "CitizenSoldier+Infantry", "citizenSoldier_infantry_melee": "CitizenSoldier+Infantry+Melee", "citizenSoldier_infantry_ranged": "CitizenSoldier+Infantry+Ranged", "citizenSoldier_cavalry": "CitizenSoldier+Cavalry", "citizenSoldier_cavalry_melee": "CitizenSoldier+Cavalry+Melee", "healers": "Healer", "females": "FemaleCitizen" }); var jebelBarkal_templates = deepfreeze(Object.keys(jebelBarkal_templateClasses).reduce((templates, name) => { templates[name] = TriggerHelper.GetTemplateNamesByClasses(jebelBarkal_templateClasses[name], "kush", undefined, jebelBarkal_rank, true); return templates; }, {})); /** * These are the formations patroling and attacking units can use. */ var jebelBarkal_formations = [ "special/formations/line_closed", "special/formations/box" ]; /** * Balancing helper function. * * @returns min0 value at the beginning of the game, min60 after an hour of gametime or longer and * a proportionate number between these two values before the first hour is reached. */ var scaleByTime = (minCurrent, min0, min60) => min0 + (min60 - min0) * Math.min(1, minCurrent / 60); /** - * @returns min0 value at the beginning of the game, min60 after an hour of gametime or longer and - * a proportionate number between these two values before the first hour is reached. + * @returns min value at map size 128 (very small), max at map size 512 and + * a proportionate number between these two values. */ var scaleByMapSize = (min, max) => min + (max - min) * (TriggerHelper.GetMapSizeTiles() - 128) / (512 - 128); /** * Defensive Infantry units patrol along the paths of the city. */ var jebelBarkal_cityPatrolGroup_count = time => TriggerHelper.GetMapSizeTiles() > 192 ? scaleByTime(time, 3, scaleByMapSize(3, 10)) : 0; var jebelBarkal_cityPatrolGroup_interval = time => scaleByTime(time, 5, 3); var jebelBarkal_cityPatrolGroup_balancing = { "buildingClasses": ["Wonder", "Temple", "CivCentre", "Fortress", "Barracks", "Embassy"], "unitCount": time => Math.min(20, scaleByTime(time, 10, 45)), "unitComposition": (time, heroes) => [ { "templates": jebelBarkal_templates.champion_infantry_melee, "frequency": scaleByTime(time, 0, 2) }, { "templates": jebelBarkal_templates.champion_infantry_ranged, "frequency": scaleByTime(time, 0, 3) }, { "templates": jebelBarkal_templates.citizenSoldier_infantry_melee, "frequency": scaleByTime(time, 2, 0) }, { "templates": jebelBarkal_templates.citizenSoldier_infantry_ranged, "frequency": scaleByTime(time, 3, 0) } ], "targetClasses": () => "Unit+!Ship" }; /** * Frequently the buildings spawn different units that attack the players groupwise. * Leave more time between the attacks in later stages of the game since the attackers become much stronger over time. */ var jebelBarkal_attackInterval = (time, difficulty) => randFloat(5, 7) + time / difficulty / 10; /** * Prevent city patrols chasing the starting units in nomad mode. */ var jebelBarkal_firstCityPatrolTime = (difficulty, isNomad) => (isNomad ? 7 - difficulty : 0); /** * Delay the first attack in nomad mode. */ var jebelBarkal_firstAttackTime = (difficulty, isNomad) => jebelBarkal_attackInterval(0, difficulty) + 2 * Math.max(0, 3 - difficulty) + (isNomad ? 9 - difficulty : 0); /** * Account for varying mapsizes and number of players when spawning attackers. */ var jebelBarkal_attackerGroup_sizeFactor = (numPlayers, numInitialSpawnPoints, difficulty) => numPlayers / numInitialSpawnPoints * difficulty * 0.85; /** * Assume gaia to be the native kushite player. */ var jebelBarkal_playerID = 0; /** * City patrols soldiers will patrol along these triggerpoints on the crossings of the city paths. */ var jebelBarkal_cityPatrolGroup_triggerPointPath = "A"; /** * Attackers will patrol these points after having finished the attack-walk order. */ var jebelBarkal_attackerGroup_triggerPointPatrol = "B"; /** * Number of points the attackers patrol. */ var jebelBarkal_patrolPointCount = 6; /** * Healers near the wonder run these animations when idle. */ var jebelBarkal_ritualAnimations = ["attack_capture", "promotion", "heal"]; /** * This defines which units are spawned and garrisoned at the gamestart per building. */ var jebelBarkal_buildingGarrison = difficulty => [ { "buildingClasses": ["Wonder", "Temple", "CivCentre", "Fortress"], "unitTemplates": jebelBarkal_templates.champions, "capacityRatio": 1 }, { "buildingClasses": ["Barracks", "Embassy"], "unitTemplates": [...jebelBarkal_templates.citizenSoldier_infantry, ...jebelBarkal_templates.champion_infantry], "capacityRatio": 1 }, { "buildingClasses": ["Tower"], "unitTemplates": jebelBarkal_templates.champion_infantry, "capacityRatio": 1 }, { "buildingClasses": ["ElephantStable"], "unitTemplates": jebelBarkal_templates.elephants, "capacityRatio": 1 }, { "buildingClasses": ["Stable"], "unitTemplates": [...jebelBarkal_templates.citizenSoldier_cavalry, ...jebelBarkal_templates.champion_cavalry], "capacityRatio": 1 }, { "buildingClasses": ["House"], "unitTemplates": [...jebelBarkal_templates.females, ...jebelBarkal_templates.healers], "capacityRatio": 0.5 }, { - "buildingClasses": ["StoneWall+Tower"], + "buildingClasses": ["WallTower"], "unitTemplates": jebelBarkal_templates.champion_infantry_ranged, "capacityRatio": difficulty > 3 ? 1 : 0 - }, + } +]; + +/** + * This defines which units are spawned and turretted at the gamestart per building. + */ +var jebelBarkal_buildingTurret = difficulty => [ { - "buildingClasses": ["StoneWall+!Tower"], + "buildingClasses": ["WallLong", "WallMedium", "WallShort"], "unitTemplates": difficulty > 3 ? jebelBarkal_templates.champion_infantry_ranged : jebelBarkal_templates.citizenSoldier_infantry_ranged, "capacityRatio": (difficulty - 2) / 3 } ]; /** * This defines which units are spawned at the different buildings at the given time. * The buildings are ordered by strength. * Notice that there are always 2 groups of these count spawned, one for each side! * The units should do a walk-attack to random player CCs */ var jebelBarkal_attackerGroup_balancing = [ { // This should be the most influential building "buildingClasses": ["Wonder"], "unitCount": time => scaleByTime(time, 0, 85), "unitComposition": (time, heroes) => [ { "templates": jebelBarkal_templates.heroes, "count": randBool(scaleByTime(time, -0.5, 2)) ? 1 : 0, "unique_entities": heroes }, { "templates": jebelBarkal_templates.healers, "frequency": randFloat(0, 0.1) }, { "templates": jebelBarkal_templates.champions, "frequency": scaleByTime(time, 0, 0.6) }, { "templates": jebelBarkal_templates.champion_infantry_ranged, "frequency": scaleByTime(time, 0, 0.4) }, { "templates": jebelBarkal_templates.citizenSoldiers, "frequency": scaleByTime(time, 1, 0) }, { "templates": jebelBarkal_templates.citizenSoldier_infantry_ranged, "frequency": scaleByTime(time, 1, 0) } ], "formations": jebelBarkal_formations, "targetClasses": () => "Unit+!Ship" }, { "buildingClasses": ["Fortress"], "unitCount": time => scaleByTime(time, 0, 45), "unitComposition": (time, heroes) => [ { "templates": jebelBarkal_templates.heroes, "count": randBool(scaleByTime(time, -0.5, 1.5)) ? 1 : 0, "unique_entities": heroes }, { "templates": jebelBarkal_templates.champions, "frequency": scaleByTime(time, 0, 1) }, { "templates": jebelBarkal_templates.citizenSoldiers, "frequency": scaleByTime(time, 1, 0) } ], "formations": jebelBarkal_formations, "targetClasses": () => "Unit+!Ship" }, { // These should only train the strongest units "buildingClasses": ["Temple"], "unitCount": time => Math.min(45, scaleByTime(time, -30, 90)), "unitComposition": (time, heroes) => [ { "templates": jebelBarkal_templates.heroes, "count": randBool(scaleByTime(time, -0.5, 1)) ? 1 : 0, "unique_entities": heroes }, { "templates": jebelBarkal_templates.champion_infantry_melee, "frequency": 0.5 }, { "templates": jebelBarkal_templates.champion_infantry_ranged, "frequency": 0.5 }, { "templates": jebelBarkal_templates.healers, "frequency": randFloat(0.05, 0.2) } ], "formations": jebelBarkal_formations, "targetClasses": () => "Unit+!Ship" }, { "buildingClasses": ["CivCentre"], "unitCount": time => Math.min(40, scaleByTime(time, 0, 80)), "unitComposition": (time, heroes) => [ { "templates": jebelBarkal_templates.heroes, "count": randBool(scaleByTime(time, -0.5, 0.5)) ? 1 : 0, "unique_entities": heroes }, { "templates": jebelBarkal_templates.champion_infantry, "frequency": scaleByTime(time, 0, 1) }, { "templates": jebelBarkal_templates.citizenSoldiers, "frequency": scaleByTime(time, 1, 0) } ], "formations": jebelBarkal_formations, "targetClasses": () => "Unit+!Ship" }, { "buildingClasses": ["Stable"], "unitCount": time => Math.min(30, scaleByTime(time, 0, 80)), "unitComposition": (time, heroes) => [ { "templates": jebelBarkal_templates.citizenSoldier_cavalry_melee, "frequency": scaleByTime(time, 2, 0) }, { "templates": jebelBarkal_templates.champion_cavalry_melee, "frequency": scaleByTime(time, 0, 1) } ], "formations": jebelBarkal_formations, "targetClasses": () => "Unit+!Ship" }, { "buildingClasses": ["Barracks", "Embassy"], "unitCount": time => Math.min(35, scaleByTime(time, 0, 70)), "unitComposition": (time, heroes) => [ { "templates": jebelBarkal_templates.citizenSoldier_infantry, "frequency": 1 } ], "formations": jebelBarkal_formations, "targetClasses": () => "Unit+!Ship" }, { "buildingClasses": ["ElephantStable", "Wonder"], "unitCount": time => scaleByTime(time, 1, 14), "unitComposition": (time, heroes) => [ { "templates": jebelBarkal_templates.elephants, "frequency": 1 } ], "formations": [], "targetClasses": () => pickRandom(["Defensive SiegeEngine Monument Wonder", "Structure"]) } ]; Trigger.prototype.debugLog = function(txt) { if (showDebugLog) print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n"); }; Trigger.prototype.JebelBarkal_Init = function() { let isNomad = !TriggerHelper.GetAllPlayersEntitiesByClass("CivCentre").length; this.JebelBarkal_TrackUnits(); this.RegisterTrigger("OnOwnershipChanged", "JebelBarkal_OwnershipChange", { "enabled": true }); this.JebelBarkal_SetDefenderStance(); this.JebelBarkal_StartRitualAnimations(); this.JebelBarkal_GarrisonBuildings(); + this.JebelBarkal_TurretBuildings(); this.DoAfterDelay(jebelBarkal_firstCityPatrolTime(this.GetDifficulty(), isNomad) * 60 * 1000, "JebelBarkal_SpawnCityPatrolGroups", {}); this.JebelBarkal_StartAttackTimer(jebelBarkal_firstAttackTime(this.GetDifficulty(), isNomad)); }; Trigger.prototype.JebelBarkal_TrackUnits = function() { // Each item is an entity ID this.jebelBarkal_heroes = []; this.jebelBarkal_ritualHealers = TriggerHelper.GetPlayerEntitiesByClass(jebelBarkal_playerID, "Healer"); // Each item is an array of entity IDs this.jebelBarkal_patrolingUnits = []; // Keep track of population limit for attackers this.jebelBarkal_attackerUnits = []; // Array of entityIDs where patrol groups can spawn this.jebelBarkal_patrolGroupSpawnPoints = TriggerHelper.GetPlayerEntitiesByClass( jebelBarkal_playerID, jebelBarkal_cityPatrolGroup_balancing.buildingClasses); this.debugLog("Patrol spawn points: " + uneval(this.jebelBarkal_patrolGroupSpawnPoints)); // Array of entityIDs where attacker groups can spawn this.jebelBarkal_attackerGroupSpawnPoints = TriggerHelper.GetPlayerEntitiesByClass( jebelBarkal_playerID, jebelBarkal_attackerGroup_balancing.reduce((classes, attackerSpawning) => classes.concat(attackerSpawning.buildingClasses), [])); this.numInitialSpawnPoints = this.jebelBarkal_attackerGroupSpawnPoints.length; this.debugLog("Attacker spawn points: " + uneval(this.jebelBarkal_attackerGroupSpawnPoints)); }; Trigger.prototype.JebelBarkal_SetDefenderStance = function() { for (let ent of TriggerHelper.GetPlayerEntitiesByClass(jebelBarkal_playerID, "Human")) TriggerHelper.SetUnitStance(ent, "defensive"); }; Trigger.prototype.JebelBarkal_StartRitualAnimations = function() { this.DoRepeatedly(5 * 1000, "JebelBarkal_UpdateRitualAnimations", {}); }; Trigger.prototype.JebelBarkal_UpdateRitualAnimations = function() { for (let ent of this.jebelBarkal_ritualHealers) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE") continue; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (cmpVisual && jebelBarkal_ritualAnimations.indexOf(cmpVisual.GetAnimationName()) == -1) cmpVisual.SelectAnimation(pickRandom(jebelBarkal_ritualAnimations), false, 1, ""); } }; Trigger.prototype.JebelBarkal_GarrisonBuildings = function() { for (let buildingGarrison of jebelBarkal_buildingGarrison(this.GetDifficulty())) TriggerHelper.SpawnAndGarrisonAtClasses(jebelBarkal_playerID, buildingGarrison.buildingClasses, buildingGarrison.unitTemplates, buildingGarrison.capacityRatio); }; +Trigger.prototype.JebelBarkal_TurretBuildings = function() +{ + for (let buildingTurret of jebelBarkal_buildingTurret(this.GetDifficulty())) + TriggerHelper.SpawnAndTurretAtClasses(jebelBarkal_playerID, buildingTurret.buildingClasses, buildingTurret.unitTemplates, buildingTurret.capacityRatio); +}; + /** * Spawn new groups if old ones were wiped out. */ Trigger.prototype.JebelBarkal_SpawnCityPatrolGroups = function() { if (!this.jebelBarkal_patrolGroupSpawnPoints.length) return; let time = TriggerHelper.GetMinutes(); let groupCount = Math.floor(Math.max(0, jebelBarkal_cityPatrolGroup_count(time)) - this.jebelBarkal_patrolingUnits.length); this.debugLog("Spawning " + groupCount + " city patrol groups, " + this.jebelBarkal_patrolingUnits.length + " exist"); for (let i = 0; i < groupCount; ++i) { let spawnEnt = pickRandom(this.jebelBarkal_patrolGroupSpawnPoints); let templateCounts = TriggerHelper.BalancedTemplateComposition( jebelBarkal_cityPatrolGroup_balancing.unitComposition(time, this.jebelBarkal_heroes), jebelBarkal_cityPatrolGroup_balancing.unitCount(time)); this.debugLog(uneval(templateCounts)); let groupEntities = this.JebelBarkal_SpawnTemplates(spawnEnt, templateCounts); this.jebelBarkal_patrolingUnits.push(groupEntities); for (let ent of groupEntities) TriggerHelper.SetUnitStance(ent, "defensive"); TriggerHelper.SetUnitFormation(jebelBarkal_playerID, groupEntities, pickRandom(jebelBarkal_formations)); for (let patrolTarget of shuffleArray(this.GetTriggerPoints(jebelBarkal_cityPatrolGroup_triggerPointPath))) { let pos = TriggerHelper.GetEntityPosition2D(patrolTarget); ProcessCommand(jebelBarkal_playerID, { "type": "patrol", "entities": groupEntities, "x": pos.x, "z": pos.y, "targetClasses": { "attack": jebelBarkal_cityPatrolGroup_balancing.targetClasses() }, "queued": true, "allowCapture": false }); } } this.DoAfterDelay(jebelBarkal_cityPatrolGroup_interval(time) * 60 * 1000, "JebelBarkal_SpawnCityPatrolGroups", {}); }; Trigger.prototype.JebelBarkal_SpawnTemplates = function(spawnEnt, templateCounts) { let groupEntities = []; for (let templateName in templateCounts) { let ents = TriggerHelper.SpawnUnits(spawnEnt, templateName, templateCounts[templateName], jebelBarkal_playerID); groupEntities = groupEntities.concat(ents); if (jebelBarkal_templates.heroes.indexOf(templateName) != -1 && ents[0]) this.jebelBarkal_heroes.push(ents[0]); } return groupEntities; }; /** * Spawn a group of attackers at every remaining building. */ Trigger.prototype.JebelBarkal_SpawnAttackerGroups = function() { if (!this.jebelBarkal_attackerGroupSpawnPoints) return; let time = TriggerHelper.GetMinutes(); this.JebelBarkal_StartAttackTimer(jebelBarkal_attackInterval(time, this.GetDifficulty())); this.debugLog("Attacker wave (at most " + (jebelBarkal_maxPopulation - this.jebelBarkal_attackerUnits.length) + " attackers)"); let activePlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers(); let playerEntities = activePlayers.map(playerID => TriggerHelper.GetEntitiesByPlayer(playerID).filter(TriggerHelper.IsInWorld)); let patrolPoints = this.GetTriggerPoints(jebelBarkal_attackerGroup_triggerPointPatrol); let groupSizeFactor = jebelBarkal_attackerGroup_sizeFactor( activePlayers.length, this.numInitialSpawnPoints, this.GetDifficulty()); let totalSpawnCount = 0; for (let spawnPointBalancing of jebelBarkal_attackerGroup_balancing) { let targets = playerEntities.reduce((allTargets, playerEnts) => allTargets.concat(shuffleArray(TriggerHelper.MatchEntitiesByClass(playerEnts, spawnPointBalancing.targetClasses())).slice(0, 10)), []); if (!targets.length) continue; for (let spawnEnt of TriggerHelper.MatchEntitiesByClass(this.jebelBarkal_attackerGroupSpawnPoints, spawnPointBalancing.buildingClasses)) { let unitCount = Math.min( jebelBarkal_maxPopulation - this.jebelBarkal_attackerUnits.length, groupSizeFactor * spawnPointBalancing.unitCount(time)); // Spawn between 0 and 1 elephants per stable in a 1v1 on a normal mapsize at the beginning unitCount = Math.floor(unitCount) + (randBool(unitCount % 1) ? 1 : 0); if (unitCount <= 0) continue; let templateCounts = TriggerHelper.BalancedTemplateComposition(spawnPointBalancing.unitComposition(time, this.jebelBarkal_heroes), unitCount); totalSpawnCount += unitCount; this.debugLog("Spawning " + unitCount + " attackers at " + uneval(spawnPointBalancing.buildingClasses) + " " + spawnEnt + ":\n" + uneval(templateCounts)); if (dryRun) continue; let spawnedEntities = this.JebelBarkal_SpawnTemplates(spawnEnt, templateCounts); this.jebelBarkal_attackerUnits = this.jebelBarkal_attackerUnits.concat(spawnedEntities); let formation = pickRandom(spawnPointBalancing.formations); if (formation) TriggerHelper.SetUnitFormation(jebelBarkal_playerID, spawnedEntities, formation); let entityGroups = formation ? [spawnedEntities] : spawnedEntities.reduce((entityGroup, ent) => entityGroup.concat([[ent]]), []); for (let i = 0; i < jebelBarkal_patrolPointCount; ++i) for (let entities of entityGroups) { let pos = TriggerHelper.GetEntityPosition2D(pickRandom(i == 0 ? targets : patrolPoints)); ProcessCommand(jebelBarkal_playerID, { "type": "patrol", "entities": entities, "x": pos.x, "z": pos.y, "targetClasses": { "attack": spawnPointBalancing.targetClasses() }, "queued": true, "allowCapture": false }); } } } this.debugLog("Total attackers: " + totalSpawnCount); if (totalSpawnCount) Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({ "message": markForTranslation("Napata is attacking!"), "translateMessage": true }); }; Trigger.prototype.JebelBarkal_StartAttackTimer = function(delay) { let nextAttack = delay * 60 * 1000; Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({ "message": markForTranslation("Napata will attack in %(time)s!"), "players": [-1, 0], "translateMessage": true }, nextAttack); this.DoAfterDelay(nextAttack, "JebelBarkal_SpawnAttackerGroups", {}); }; /** * Keep track of heroes, so that each of them remains unique. * Keep track of spawn points, as only there units should be spawned. */ Trigger.prototype.JebelBarkal_OwnershipChange = function(data) { if (data.from != 0) return; let trackedEntityArrays = [ this.jebelBarkal_heroes, this.jebelBarkal_ritualHealers, this.jebelBarkal_patrolGroupSpawnPoints, this.jebelBarkal_attackerGroupSpawnPoints, this.jebelBarkal_attackerUnits, ...this.jebelBarkal_patrolingUnits, ]; for (let array of trackedEntityArrays) { let idx = array.indexOf(data.entity); if (idx != -1) array.splice(idx, 1); } this.jebelBarkal_patrolingUnits = this.jebelBarkal_patrolingUnits.filter(entities => entities.length); }; { Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).RegisterTrigger("OnInitGame", "JebelBarkal_Init", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js (revision 27936) +++ ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js (revision 27937) @@ -1,605 +1,605 @@ // 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.IsInWorld = function(ent) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); return cmpPosition && cmpPosition.IsInWorld(); }; TriggerHelper.GetEntityPosition2D = function(ent) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) return undefined; return cmpPosition.GetPosition2D(); }; TriggerHelper.GetOwner = function(ent) { let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) return cmpOwnership.GetOwner(); return -1; }; /** * This returns the mapsize in number of tiles, the value corresponding to map_sizes.json, also used by random map scripts. */ TriggerHelper.GetMapSizeTiles = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetTilesPerSide(); }; /** * This returns the mapsize in the the coordinate system used in the simulation/, especially cmpPosition. */ TriggerHelper.GetMapSizeTerrain = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize(); }; /** * Returns the elapsed ingame time in milliseconds since the gamestart. */ TriggerHelper.GetTime = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime(); }; /** * Returns the elpased ingame time in minutes since the gamestart. Useful for balancing. */ TriggerHelper.GetMinutes = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000; }; TriggerHelper.GetEntitiesByPlayer = function(playerID) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(playerID); }; TriggerHelper.GetAllPlayersEntities = function() { return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities(); }; TriggerHelper.SetUnitStance = function(ent, stance) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.SwitchToStance(stance); }; TriggerHelper.SetUnitFormation = function(playerID, entities, formation) { ProcessCommand(playerID, { "type": "formation", "entities": entities, "formation": formation }); }; /** * Creates a new unit taking into account possible 0 experience promotion. * @param {number} owner Player id of the owner of the new units. * @param {string} template Name of the template. * @returns the created entity id. */ TriggerHelper.AddUpgradeTemplate = function(owner, template) { const upgradedTemplate = GetUpgradedTemplate(owner, template); if (upgradedTemplate !== template) warn(`tried to spawn template '${template}' but upgraded template '${upgradedTemplate}' will be spawned instead. You might want to create a template that is not affected by this promotion.`); return Engine.AddEntity(upgradedTemplate); }; /** * 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) { const ent = this.AddUpgradeTemplate(owner, 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) { const ent = this.AddUpgradeTemplate(owner, template); let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(owner); let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable); if (cmpGarrisonable && cmpGarrisonable.Garrison(entity)) entities.push(ent); else error("failed to garrison entity " + ent + " (" + template + ") inside " + entity); } return entities; }; /** * Can be used to spawn turreted units on top of a building/ship. * * @param entity Entity id of the turret holder on which units will be turreted * @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 turretholder entity. */ TriggerHelper.SpawnTurretedUnits = function(entity, template, count, owner) { let entities = []; let cmpTurretHolder = Engine.QueryInterface(entity, IID_TurretHolder); if (!cmpTurretHolder) { error("tried to create turreted entities inside a non-turretholder"); return entities; } if (owner == null) owner = TriggerHelper.GetOwner(entity); for (let i = 0; i < count; ++i) { const ent = this.AddUpgradeTemplate(owner, template); let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); if (cmpOwnership) cmpOwnership.SetOwner(owner); let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable); if (cmpTurretable && cmpTurretable.OccupyTurret(entity)) entities.push(ent); else error("failed to turret 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 players including gaia and defeated ones. */ 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 classes - List of the classes we are checking if the entity matches. */ TriggerHelper.EntityMatchesClassList = function(entity, classes) { let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes); }; TriggerHelper.MatchEntitiesByClass = function(entities, classes) { return entities.filter(ent => TriggerHelper.EntityMatchesClassList(ent, classes)); }; TriggerHelper.GetPlayerEntitiesByClass = function(playerID, classes) { return TriggerHelper.MatchEntitiesByClass(TriggerHelper.GetEntitiesByPlayer(playerID), classes); }; TriggerHelper.GetAllPlayersEntitiesByClass = function(classes) { return TriggerHelper.MatchEntitiesByClass(TriggerHelper.GetAllPlayersEntities(), classes); }; /** * 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.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. * * @param {string[]} templateNames - for example ["brit_infantry_javelineer_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_javelineer_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 choose 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. */ 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 || !MatchesClassList(cmpIdentity.GetClassesList(), classes)) - continue; - let cmpGarrisonHolder = Engine.QueryInterface(entGarrisonHolder, IID_GarrisonHolder); if (!cmpGarrisonHolder) continue; + let cmpIdentity = Engine.QueryInterface(entGarrisonHolder, IID_Identity); + if (!cmpIdentity || !MatchesClassList(cmpIdentity.GetClassesList(), classes)) + continue; + // TODO: account for already garrisoned entities and garrison size. results[entGarrisonHolder] = this.RandomTemplateComposition(templates, Math.floor(cmpGarrisonHolder.GetCapacity() * capacityPercent)); for (let template in results[entGarrisonHolder]) TriggerHelper.SpawnGarrisonedUnits(entGarrisonHolder, template, results[entGarrisonHolder][template], playerID); } return results; }; /** * This will spawn random compositions of entities of the given templates in all turretholders of the given targetClass of the given player. * The turretholder will be filled to capacityPercent. * Returns an object where keys are entityIDs of the affected turretholders and the properties are template compositions, see RandomTemplateComposition. */ TriggerHelper.SpawnAndTurretAtClasses = function(playerID, classes, templates, capacityPercent) { let results = {}; for (let entTurretHolder of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(playerID)) { let cmpTurretHolder = Engine.QueryInterface(entTurretHolder, IID_TurretHolder); if (!cmpTurretHolder) continue; let cmpIdentity = Engine.QueryInterface(entTurretHolder, IID_Identity); if (!cmpIdentity || !MatchesClassList(cmpIdentity.GetClassesList(), classes)) continue; results[entTurretHolder] = this.RandomTemplateComposition(templates, Math.max(Math.floor(cmpTurretHolder.GetTurretPoints().length * capacityPercent - cmpTurretHolder.GetEntities().length), 0)); for (let template in results[entTurretHolder]) TriggerHelper.SpawnTurretedUnits(entTurretHolder, template, results[entTurretHolder][template], playerID); } return results; }; Engine.RegisterGlobal("TriggerHelper", TriggerHelper);