Index: ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 24146) +++ ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 24147) @@ -1,641 +1,641 @@ // 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"], + "buildingClasses": ["Tower", "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)); /** * 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)); } }; /** * 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 = 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] || 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 // TODO: The Vector2D types don't survive deserialization, so use an object with x and y properties only! let mapSize = TriggerHelper.GetMapSizeTerrain(); this.mapCenter = clone(new Vector2D(mapSize / 2, mapSize / 2)); this.riverDirection = clone(Vector2D.sub( TriggerHelper.GetEntityPosition2D(this.GetTriggerPoints(triggerPointRiverDirection)[0]), this.mapCenter)); this.StartCelticRitual(); this.GarrisonAllGallicBuildings(); 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/extinct_volcano_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/extinct_volcano_triggers.js (revision 24146) +++ ps/trunk/binaries/data/mods/public/maps/random/extinct_volcano_triggers.js (revision 24147) @@ -1,191 +1,191 @@ /** * Whether to log the water levels and which units became killed or transformed to visual actors. */ var debugLog = false; /** * Whether to rise the water to the maximum level in a minute or two. */ var debugWaterRise = false; /** * Duration in minutes for which the notification will be shown that states that the water will rise soon. */ var waterRiseNotificationDuration = 1; /** * Time in minutes between increases of the water level. * If the water rises too fast, the hills are of no strategic importance, * building structures would be pointless. * * At height 27, most trees are not gatherable anymore and enemies not reachable. * At height 37 most hills are barely usable. * * At min 30 stuff at the ground level should not be gatherable anymore. * At min 45 CC should be destroyed. * * Notice regular and military docks will raise with the water! */ var waterIncreaseTime = [0.5, 1]; /** * Number of meters the waterheight increases each step. * Each time the water level is changed, the pathfinder grids have to be recomputed. * Therefore raising the level should occur as rarely as possible, i.e. have the value * as big as possible, but as small as needed to keep it visually authentic. */ var waterLevelIncreaseHeight = 1; /** * At which height to stop increasing the water level. * Since players can survive on ships, don't endlessly raise the water. */ var maxWaterLevel = 70; /** * Let buildings, relics and siege engines become actors, but kill organic units. */ var drownClass = "Organic"; /** * Maximum height that units and structures can be submerged before drowning or becoming destructed. */ var drownHeight = 1; /** * One of these warnings is printed some minutes before the water level starts to rise. */ var waterWarningTexts = [ markForTranslation("It keeps on raining, we will have to evacuate soon!"), markForTranslation("The rivers are standing high, we need to find a safe place!"), markForTranslation("We have to find dry ground, our lands will drown soon!"), markForTranslation("The lakes start swallowing the land, we have to find shelter!") ]; /** * Units to be garrisoned in the wooden towers. */ var garrisonedUnits = "units/rome_champion_infantry_swordsman_02"; Trigger.prototype.RaisingWaterNotification = function() { Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({ "message": pickRandom(waterWarningTexts), "translateMessage": true }, waterRiseNotificationDuration * 60 * 1000); }; Trigger.prototype.DebugLog = function(txt) { if (!debugLog) return; print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n"); }; Trigger.prototype.GarrisonWoodenTowers = function() { for (let gaiaEnt of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0)) { let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity); - if (!cmpIdentity || !cmpIdentity.HasClass("DefenseTower")) + if (!cmpIdentity || !cmpIdentity.HasClass("Tower")) continue; let cmpGarrisonHolder = Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder); if (!cmpGarrisonHolder) continue; for (let newEnt of TriggerHelper.SpawnUnits(gaiaEnt, garrisonedUnits, cmpGarrisonHolder.GetCapacity(), 0)) if (Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder).Garrison(newEnt)) Engine.QueryInterface(newEnt, IID_UnitAI).Autogarrison(gaiaEnt); } }; Trigger.prototype.RaiseWaterLevelStep = function() { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let time = cmpTimer.GetTime(); let cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); let newLevel = cmpWaterManager.GetWaterLevel() + waterLevelIncreaseHeight; cmpWaterManager.SetWaterLevel(newLevel); this.DebugLog("Raising water level to " + Math.round(newLevel) + " took " + (cmpTimer.GetTime() - time)); if (newLevel < maxWaterLevel) this.DoAfterDelay((debugWaterRise ? 10 : randFloat(...waterIncreaseTime) * 60) * 1000, "RaiseWaterLevelStep", {}); else this.DebugLog("Water reached final level"); let actorTemplates = {}; let killedTemplates = {}; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let ent of cmpRangeManager.GetGaiaAndNonGaiaEntities()) { let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (!cmpPosition || !cmpPosition.IsInWorld()) continue; let pos = cmpPosition.GetPosition(); if (pos.y + drownHeight >= newLevel) continue; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) continue; let templateName = cmpTemplateManager.GetCurrentTemplateName(ent); // Animals and units drown let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (cmpHealth && cmpIdentity.HasClass(drownClass)) { cmpHealth.Kill(); if (debugLog) killedTemplates[templateName] = (killedTemplates[templateName] || 0) + 1; continue; } // Resources and buildings become actors // Do not use ChangeEntityTemplate for performance and // because we don't need nor want the effects of MT_EntityRenamed let cmpVisualActor = Engine.QueryInterface(ent, IID_Visual); if (!cmpVisualActor) continue; let height = cmpPosition.GetHeightOffset(); let rot = cmpPosition.GetRotation(); let actorTemplate = cmpTemplateManager.GetTemplate(templateName).VisualActor.Actor; let seed = cmpVisualActor.GetActorSeed(); Engine.DestroyEntity(ent); let newEnt = Engine.AddEntity("actor|" + actorTemplate); Engine.QueryInterface(newEnt, IID_Visual).SetActorSeed(seed); let cmpNewPos = Engine.QueryInterface(newEnt, IID_Position); cmpNewPos.JumpTo(pos.x, pos.z); cmpNewPos.SetHeightOffset(height); cmpNewPos.SetXZRotation(rot.x, rot.z); cmpNewPos.SetYRotation(rot.y); if (debugLog) actorTemplates[templateName] = (actorTemplates[templateName] || 0) + 1; } this.DebugLog("Checking entities took " + (cmpTimer.GetTime() - time)); this.DebugLog("Killed: " + uneval(killedTemplates)); this.DebugLog("Converted to actors: " + uneval(actorTemplates)); }; { let waterRiseTime = debugWaterRise ? 0 : (InitAttributes.settings.SeaLevelRiseTime || 0); let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.GarrisonWoodenTowers(); cmpTrigger.DoAfterDelay((waterRiseTime - waterRiseNotificationDuration) * 60 * 1000, "RaisingWaterNotification", {}); cmpTrigger.DoAfterDelay(waterRiseTime * 60 * 1000, "RaiseWaterLevelStep", {}); } 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 24146) +++ ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal_triggers.js (revision 24147) @@ -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], "capacityRatio": 1 }, { - "buildingClasses": ["DefenseTower"], + "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 }); } Index: ps/trunk/binaries/data/mods/public/maps/skirmishes/libyan_oasis_2p.xml =================================================================== --- ps/trunk/binaries/data/mods/public/maps/skirmishes/libyan_oasis_2p.xml (revision 24146) +++ ps/trunk/binaries/data/mods/public/maps/skirmishes/libyan_oasis_2p.xml (revision 24147) @@ -1,8871 +1,8871 @@ cumulus 0.00195313 0 clap 20.6689 1.0 0.71875 0 1 1.07813 0.160156 hdr 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 1 2 1 1 1 1 2 2 2 2 0 0 0 0 0 0 Index: ps/trunk/binaries/data/mods/public/maps/tutorials/introductory_tutorial.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/tutorials/introductory_tutorial.js (revision 24146) +++ ps/trunk/binaries/data/mods/public/maps/tutorials/introductory_tutorial.js (revision 24147) @@ -1,432 +1,432 @@ Trigger.prototype.tutorialGoals = [ { "instructions": markForTranslation("Welcome to the 0 A.D. tutorial."), }, { "instructions": markForTranslation("Left-click on a female citizen and then right-click on a berry bush to make that female citizen gather food. Female citizens gather vegetables faster than other units."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit") this.NextGoal(); } }, { "instructions": markForTranslation("Select the citizen-soldier, right-click on a tree near the Civic Center to begin gathering Wood. Citizen Soldiers gather Wood faster than female citizens."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "tree") this.NextGoal(); } }, { "instructions": [ { "text": markForTranslation("Select the Civic Center building and hold %(hotkey)s while clicking on the Hoplite icon once to begin training a batch of Hoplites."), "hotkey": "session.batchtrain" } ], "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_infantry_spearman_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the HOPLITE icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": markForTranslation("Select the two idle female citizens and build a house nearby by selecting the house icon. Place the house by left-clicking on a piece of land."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House")) this.NextGoal(); } }, { "instructions": markForTranslation("When they are ready, select the newly trained Hoplites and assign them to build a storehouse beside some nearby trees. They will begin to gather Wood when it's constructed."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Storehouse")) this.NextGoal(); } }, { "instructions": [ { "text": markForTranslation("Train a batch of Skirmishers by holding %(hotkey)s and clicking on the Skirmisher icon in the Civic Center."), "hotkey": "session.batchtrain" } ], "Init": function() { this.trainingDone = false; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_infantry_javelineer_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the Skirmisher icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": markForTranslation("Build a farmstead in an open space beside the Civic Center using any idle builders."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Farmstead")) this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; } }, { "instructions": markForTranslation("Let's wait for the farmstead to be built."), "OnTrainingFinished": function(msg) { this.trainingDone = true; }, "OnStructureBuilt": function(msg) { if (TriggerHelper.EntityMatchesClassList(msg.building, "Farmstead")) this.NextGoal(); } }, { "instructions": markForTranslation("Once the farmstead is constructed, its builders will automatically begin gathering food if there is any nearby. Select the builders and instead make them construct a field beside the farmstead."), "Init": function() { this.farmStarted = false; }, "IsDone": function() { return this.farmStarted && this.trainingDone; }, "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Field")) this.farmStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("The field's builders will now automatically begin gathering food from the field. Using the newly created group of skirmishers, get them to build another house nearby."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House")) this.NextGoal(); } }, { "instructions": markForTranslation("Train a batch of Hoplites at the Civic Center. Select the Civic Center and with it selected right-click on a tree nearby. Units from the Civic Center will now automatically gather Wood."), "Init": function() { this.rallyPointSet = false; this.trainingStarted = false; }, "IsDone": function() { return this.rallyPointSet && this.trainingStarted; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_infantry_spearman_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the Hoplite icon."); this.WarningMessage(txt); return; } this.trainingStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree") { this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over the tree and right-click when you see your cursor change into a Wood icon.")); return; } this.rallyPointSet = true; if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Order the idle Skirmishers to build an outpost to the north east at the edge of your territory. This will be the fifth Village Phase structure that you have built, allowing you to advance to the Town Phase."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Outpost")) this.NextGoal(); } }, { "instructions": markForTranslation("Select the Civic Center again and advance to Town Phase by clicking on the II icon (you have to wait for the outpost to be built first). This will allow Town Phase buildings to be constructed."), "IsDone": function() { return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_generic"); }, "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("While waiting for the phasing up, you may reassign your idle workers to gathering the resources you are short of."), "IsDone": function() { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let playerEnt = cmpPlayerManager.GetPlayerByID(this.playerID); let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched("phase_town_generic"); }, "OnResearchFinished": function(msg) { if (msg.tech == "phase_town_generic") this.NextGoal(); } }, { "instructions": markForTranslation("Start training a batch of female citizens in the Civic Center and set its rally point to the farm (right click on it)."), "Init": function() { this.rallyPointSet = false; this.trainingStarted = false; }, "IsDone": function() { return this.rallyPointSet && this.trainingStarted; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart_support_female_citizen" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the female citizen icon."); this.WarningMessage(txt); return; } this.trainingStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain") return; this.rallyPointSet = true; if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Build a Barracks nearby. Whenever your population limit is reached, build an extra house using any available builder units."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Barracks")) this.NextGoal(); } }, { - "instructions": markForTranslation("Prepare for an attack by an enemy player. Train more soldiers using the Barracks, and get idle soldiers to build a Defense Tower near your Outpost."), + "instructions": markForTranslation("Prepare for an attack by an enemy player. Train more soldiers using the Barracks, and get idle soldiers to build a Tower near your Outpost."), "OnPlayerCommand": function(msg) { - if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "DefenseTower")) + if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Tower")) this.NextGoal(); } }, { "instructions": markForTranslation("Build a Forge and research the Infantry Training technology (sword icon) to improve infantry hack attack."), "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "Forge")) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy is coming. Train more soldiers to fight off the enemies."), "OnResearchFinished": function(msg) { this.LaunchAttack(); this.NextGoal(); } }, { "instructions": markForTranslation("Try to repel the attack."), "OnOwnershipChanged": function(msg) { if (msg.to != INVALID_PLAYER) return; if (this.IsAttackRepelled()) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy attack has been thwarted. Now build a market and a temple while you assign new units to gather required resources."), "Init": function() { this.marketStarted = false; this.templeStarted = false; }, "IsDone": function() { return this.marketStarted && this.templeStarted; }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "repair") return; this.marketStarted = this.marketStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Market"); this.templeStarted = this.templeStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Temple"); if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Once you meet the City Phase requirements, select your Civic Center and advance to City Phase."), "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("While waiting for the phase change, you may train more soldiers at the Barracks."), "OnResearchFinished": function(msg) { if (msg.tech == "phase_city_generic") this.NextGoal(); } }, { "instructions": markForTranslation("Now that you are in City Phase, build a fortress nearby (gather some stone first if needed) and then use it to construct 2 Battering Rams."), "Init": function() { this.ramCount = 0; }, "IsDone": function() { return this.ramCount > 1; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate == "units/spart_siege_ram") ++this.ramCount; if (this.IsDone()) { this.RemoveChampions(); this.NextGoal(); } } }, { "instructions": [ markForTranslation("Stop all your soldiers gathering resources and instead task small groups to find the enemy Civic Center on the map. Once The enemy's base has been spotted, send your siege weapons and all remaining soldiers to destroy it.\n"), markForTranslation("Female citizens should continue to gather resources.") ], "OnOwnershipChanged": function(msg) { if (msg.from != this.enemyID) return; if (TriggerHelper.EntityMatchesClassList(msg.entity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy has been defeated. These tutorial tasks are now completed."), } ]; Trigger.prototype.LaunchAttack = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); - let target = + let target = entities.find(e => { let cmpIdentity = Engine.QueryInterface(e, IID_Identity); - return cmpIdentity && cmpIdentity.HasClass("DefenseTower") && Engine.QueryInterface(e, IID_Position); - }) || + return cmpIdentity && cmpIdentity.HasClass("Tower") && Engine.QueryInterface(e, IID_Position); + }) || entities.find(e => { let cmpIdentity = Engine.QueryInterface(e, IID_Identity); return cmpIdentity && cmpIdentity.HasClass("CivilCentre") && Engine.QueryInterface(e, IID_Position); }); let position = Engine.QueryInterface(target, IID_Position).GetPosition2D(); this.attackers = cmpRangeManager.GetEntitiesByPlayer(this.enemyID).filter(e => { - let cmpIdentity = Engine.QueryInterface(e, IID_Identity); + let cmpIdentity = Engine.QueryInterface(e, IID_Identity); return Engine.QueryInterface(e, IID_UnitAI) && cmpIdentity && cmpIdentity.HasClass("CitizenSoldier"); }); ProcessCommand(this.enemyID, { "type": "attack-walk", "entities": this.attackers, "x": position.x, "z": position.y, "targetClasses": { "attack": ["Unit"] }, "allowCapture": false, "queued": false }); }; Trigger.prototype.IsAttackRepelled = function() { return !this.attackers.some(e => Engine.QueryInterface(e, IID_Health) && Engine.QueryInterface(e, IID_Health).GetHitpoints() > 0); }; Trigger.prototype.RemoveChampions = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let champions = cmpRangeManager.GetEntitiesByPlayer(this.enemyID).filter(e => Engine.QueryInterface(e, IID_Identity).HasClass("Champion")); let keep = 6; for (let ent of champions) { let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (!cmpHealth) Engine.DestroyEntity(ent); else if (--keep < 0) cmpHealth.Kill(); } }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.playerID = 1; cmpTrigger.enemyID = 2; cmpTrigger.RegisterTrigger("OnInitGame", "InitTutorial", { "enabled": true }); }