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 27051) +++ ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal_triggers.js (revision 27052) @@ -1,641 +1,641 @@ /** * 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. */ 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.citizenSoldiers, ...jebelBarkal_templates.champions], + "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.champion_cavalry, "capacityRatio": 1 }, { "buildingClasses": ["House"], "unitTemplates": [...jebelBarkal_templates.females, ...jebelBarkal_templates.healers], "capacityRatio": 0.5 }, { "buildingClasses": ["StoneWall+Tower"], "unitTemplates": jebelBarkal_templates.champion_infantry_ranged, "capacityRatio": difficulty > 3 ? 1 : 0 }, { "buildingClasses": ["StoneWall+!Tower"], "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.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); }; /** * 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 }); }