Index: ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 21456) +++ ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 21457) @@ -1,640 +1,636 @@ // 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), "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": ["DefenseTower", "Outpost"], "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); /** * 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)); /** * 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 = 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); + this.civicCenters.add(gaiaEnt); for (let ccDefender of ccDefenders) 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 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.push(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.length) + 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(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); + this.ritualEnts.add(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; + let shipSpawnCount = shipCount(time, numPlayers) - this.ships.size; this.debugLog("Spawning " + shipSpawnCount + " ships"); - while (this.ships.length < shipSpawnCount) - this.ships.push(TriggerHelper.SpawnUnits(pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)), pickRandom(danubiusAttackerTemplates.ships), 1, gaulPlayer)[0]); + 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, "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(time, siegeEngines) { let champRatio = championRatio(time); return [ { "templates": danubiusAttackerTemplates.heroes, "count": randBool(heroProbability(time)) ? 1 : 0, - "unique_entities": this.heroes + "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 = 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 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.push(ents[0]); + this.heroes.add(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 ships = Array.from(this.ships); 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); + shipsLeft = shuffleArray(ships).slice(0, Math.round(ships.length / 2)); + shipsRight = ships.filter(ship => shipsLeft.indexOf(ship) == -1); } else if (ungarrisonLeft) - shipsLeft = this.ships; + shipsLeft = ships; else if (ungarrisonRight) - shipsRight = this.ships; + 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 = 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); - } + if (this.heroes.delete(data.entity)) + this.debugLog("Hero " + data.entity + " died"); - let ritualIdx = this.ritualEnts.indexOf(data.entity); - if (ritualIdx != -1) - this.ritualEnts.splice(ritualIdx, 1); - - let heroIdx = this.heroes.findIndex(ent => ent == data.entity); - if (ritualIdx != -1) - this.heroes.splice(heroIdx, 1); + if (this.ships.delete(data.entity)) + this.debugLog("Ship " + data.entity + " sunk"); - let ccIdx = this.civicCenters.indexOf(data.entity); - if (ccIdx != -1) - { + if (this.civicCenters.delete(data.entity)) this.debugLog("Gaia civic center " + data.entity + " destroyed or captured"); - this.civicCenters.splice(ccIdx, 1); - } + + this.ritualEnts.delete(data.entity); }; Trigger.prototype.InitDanubius = function() { - this.ritualEnts = []; + // 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 = []; - this.heroes = []; + this.ships = new Set(); + this.heroes = new Set(); // Remember gaia CCs to spawn attackers from - this.civicCenters = []; + this.civicCenters = new Set(); // 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 = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetMapSize(); 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 21456) +++ ps/trunk/binaries/data/mods/public/maps/random/elephantine_triggers.js (revision 21457) @@ -1,30 +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"); } }; 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); TriggerHelper.SpawnAndGarrisonAtClasses(0, "Tower", kushInfantryUnits, 1); for (let identityClass of ["Wonder", "Temple", "Pyramid"]) TriggerHelper.SpawnAndGarrisonAtClasses(0, identityClass, kushInfantryUnits.concat(kushSupportUnits), 1); }; { Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).RegisterTrigger("OnInitGame", "InitElephantine", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js (revision 21456) +++ ps/trunk/binaries/data/mods/public/maps/scripts/TriggerHelper.js (revision 21457) @@ -1,433 +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 " + 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. * * @param {String[]} templateNames - for example ["brit_infantry_javelinist_b", "brit_cavalry_swordsman_e"] * @param {Number} totalCount - total amount of templates, in this example 12 * @returns an object where the keys are template names and values are amounts, * for example { "brit_infantry_javelinist_b": 4, "brit_cavalry_swordsman_e": 8 } */ TriggerHelper.RandomTemplateComposition = function(templateNames, totalCount) { let frequencies = templateNames.map(() => randFloat(0, 1)); let frequencySum = frequencies.reduce((sum, frequency) => sum + frequency, 0); let remainder = totalCount; let templateCounts = {}; for (let i = 0; i < templateNames.length; ++i) { let count = i == templateNames.length - 1 ? remainder : Math.min(remainder, Math.round(frequencies[i] / frequencySum * totalCount)); if (!count) continue; templateCounts[templateNames[i]] = count; remainder -= count; } return templateCounts; }; /** * Composes a random set of the given templates so that the sum of templates matches totalCount. * For each template array that has a count item, it choses exactly that number of templates at random. * The remaining template arrays are chosen depending on the given frequency. * If a unique_entities array is given, it will only select the template if none of the given entityIDs * already have that entity (useful to let heroes remain unique). * * @param {Object[]} templateBalancing - for example * [ * { "templates": ["template1", "template2"], "frequency": 2 }, * { "templates": ["template3"], "frequency": 1 }, * { "templates": ["hero1", "hero2"], "unique_entities": [380, 495], "count": 1 } * ] * @param {Number} totalCount - total amount of templates, for example 5. * * @returns an object where the keys are template names and values are amounts, * for example { "template1": 1, "template2": 3, "template3": 2, "hero1": 1 } */ TriggerHelper.BalancedTemplateComposition = function(templateBalancing, totalCount) { // Remove all unavailable unique templates (heroes) and empty template arrays let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let templateBalancingFiltered = []; for (let templateBalance of templateBalancing) { let templateBalanceNew = clone(templateBalance); if (templateBalanceNew.unique_entities) templateBalanceNew.templates = templateBalanceNew.templates.filter(templateName => templateBalanceNew.unique_entities.every(ent => templateName != cmpTemplateManager.GetCurrentTemplateName(ent))); if (templateBalanceNew.templates.length) templateBalancingFiltered.push(templateBalanceNew); } // Helper function to add randomized templates to the result let remainder = totalCount; let results = {}; let addTemplates = (templateNames, count) => { let templateCounts = TriggerHelper.RandomTemplateComposition(templateNames, count); for (let templateName in templateCounts) { if (!results[templateName]) results[templateName] = 0; - results[templateName] += templateCounts[templateName] + results[templateName] += templateCounts[templateName]; remainder -= templateCounts[templateName]; } }; // Add template groups with fixed counts for (let templateBalance of templateBalancingFiltered) if (templateBalance.count) addTemplates(templateBalance.templates, Math.min(remainder, templateBalance.count)); // Add template groups with frequency weights let templateBalancingFrequencies = templateBalancingFiltered.filter(templateBalance => !!templateBalance.frequency); let templateBalancingFrequencySum = templateBalancingFrequencies.reduce((sum, templateBalance) => sum + templateBalance.frequency, 0); for (let i = 0; i < templateBalancingFrequencies.length; ++i) addTemplates( templateBalancingFrequencies[i].templates, i == templateBalancingFrequencies.length - 1 ? remainder : Math.min(remainder, Math.round(templateBalancingFrequencies[i].frequency / templateBalancingFrequencySum * totalCount))); if (remainder != 0) warn("Could not chose as many templates as intended, remaining " + remainder + ", chosen: " + uneval(results)); return results; }; /** * This will spawn random compositions of entities of the given templates at all garrisonholders of the given targetClass of the given player. * The garrisonholder will be filled to capacityPercent. * Returns an object where keys are entityIDs of the affected garrisonholders and the properties are template compositions, see RandomTemplateComposition. */ 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; // TODO: account for already garrisoned entities 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; }; Engine.RegisterGlobal("TriggerHelper", TriggerHelper);