Differential D145 Diff 995 ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js
Changeset View
Changeset View
Standalone View
Standalone View
ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js
/** | |||||
* If set to true, it will print how many templates would be spawned if the players were not defeated. | |||||
*/ | |||||
const dryRun = false; | |||||
/** | |||||
* If enabled, prints the number of units to the command line output. | |||||
*/ | |||||
const debugLog = false; | |||||
/** | |||||
* Least and greatest number of minutes to pass between spawning new treasures. | |||||
*/ | |||||
var treasureTime = [3, 5]; | |||||
/** | |||||
* Earliest and latest time when the first wave of attackers will be spawned. | |||||
*/ | |||||
var firstWaveTime = [4, 6]; | |||||
/** | |||||
* Smallest and largest number of minutes between two consecutive waves. | |||||
*/ | |||||
var waveTime = [2, 4]; | |||||
/** | |||||
* Roughly the number of attackers on the first wave. | |||||
*/ | |||||
var initialAttackers = 5; | |||||
/** | |||||
* Increase the number of attackers exponentially, by this percent value per minute. | |||||
*/ | |||||
var percentPerMinute = 1.05; | |||||
/** | |||||
* Greatest amount of attackers that can be spawned. | |||||
*/ | |||||
var totalAttackerLimit = 150; | |||||
/** | |||||
* Least and greatest amount of siege engines per wave. | |||||
*/ | |||||
var siegeFraction = [0.2, 0.5]; | |||||
/** | |||||
* Potentially / definitely spawn a gaia hero after this number of minutes. | |||||
*/ | |||||
var heroTime = [20, 60]; | |||||
/** | |||||
* The following templates can't be built by any player. | |||||
*/ | |||||
var disabledTemplates = (civ) => [ | var disabledTemplates = (civ) => [ | ||||
// Economic structures | // Economic structures | ||||
"structures/" + civ + "_corral", | "structures/" + civ + "_corral", | ||||
"structures/" + civ + "_farmstead", | "structures/" + civ + "_farmstead", | ||||
"structures/" + civ + "_field", | "structures/" + civ + "_field", | ||||
"structures/" + civ + "_storehouse", | "structures/" + civ + "_storehouse", | ||||
"structures/" + civ + "_rotarymill", | "structures/" + civ + "_rotarymill", | ||||
"units/maur_support_elephant", | "units/maur_support_elephant", | ||||
Show All 9 Lines | var disabledTemplates = (civ) => [ | ||||
// Shoreline | // Shoreline | ||||
"structures/" + civ + "_dock", | "structures/" + civ + "_dock", | ||||
"structures/brit_crannog", | "structures/brit_crannog", | ||||
"structures/cart_super_dock", | "structures/cart_super_dock", | ||||
"structures/ptol_lighthouse" | "structures/ptol_lighthouse" | ||||
]; | ]; | ||||
var treasures = | /** | ||||
[ | * Spawn these treasures in regular intervals. | ||||
*/ | |||||
var treasures = [ | |||||
"gaia/special_treasure_food_barrel", | "gaia/special_treasure_food_barrel", | ||||
"gaia/special_treasure_food_bin", | "gaia/special_treasure_food_bin", | ||||
"gaia/special_treasure_food_crate", | "gaia/special_treasure_food_crate", | ||||
"gaia/special_treasure_food_jars", | "gaia/special_treasure_food_jars", | ||||
"gaia/special_treasure_metal", | "gaia/special_treasure_metal", | ||||
"gaia/special_treasure_stone", | "gaia/special_treasure_stone", | ||||
"gaia/special_treasure_wood", | "gaia/special_treasure_wood", | ||||
"gaia/special_treasure_wood", | "gaia/special_treasure_wood", | ||||
"gaia/special_treasure_wood" | "gaia/special_treasure_wood" | ||||
]; | ]; | ||||
var attackerEntityTemplates = | /** | ||||
[ | * An object that maps from civ [f.e. "spart"] to an object | ||||
[ | * that has the keys "champions", "siege" and "heroes", | ||||
"units/athen_champion_infantry", | * which is an array containing all these templates, | ||||
"units/athen_champion_marine", | * trainable from a building or not. | ||||
"units/athen_champion_ranged", | */ | ||||
"units/athen_mechanical_siege_lithobolos_packed", | var attackerUnitTemplates = {}; | ||||
"units/athen_mechanical_siege_oxybeles_packed", | |||||
], | Trigger.prototype.InitSurvival = function() | ||||
[ | { | ||||
"units/brit_champion_cavalry", | this.InitStartingUnits(); | ||||
"units/brit_champion_infantry", | this.LoadAttackerTemplates(); | ||||
"units/brit_mechanical_siege_ram", | this.SetDisableTemplates(); | ||||
], | this.PlaceTreasures(); | ||||
[ | this.InitializeEnemyWaves(); | ||||
"units/cart_champion_cavalry", | }; | ||||
"units/cart_champion_elephant", | |||||
"units/cart_champion_infantry", | |||||
"units/cart_champion_pikeman", | |||||
], | |||||
[ | |||||
"units/gaul_champion_cavalry", | |||||
"units/gaul_champion_fanatic", | |||||
"units/gaul_champion_infantry", | |||||
"units/gaul_mechanical_siege_ram", | |||||
], | |||||
[ | |||||
"units/iber_champion_cavalry", | |||||
"units/iber_champion_infantry", | |||||
"units/iber_mechanical_siege_ram", | |||||
], | |||||
[ | |||||
"units/mace_champion_cavalry", | |||||
"units/mace_champion_infantry_a", | |||||
"units/mace_champion_infantry_e", | |||||
"units/mace_mechanical_siege_lithobolos_packed", | |||||
"units/mace_mechanical_siege_oxybeles_packed", | |||||
], | |||||
[ | |||||
"units/maur_champion_chariot", | |||||
"units/maur_champion_elephant", | |||||
"units/maur_champion_infantry", | |||||
"units/maur_champion_maiden", | |||||
"units/maur_champion_maiden_archer", | |||||
], | |||||
[ | |||||
"units/pers_champion_cavalry", | |||||
"units/pers_champion_infantry", | |||||
"units/pers_champion_elephant", | |||||
], | |||||
[ | |||||
"units/ptol_champion_cavalry", | |||||
"units/ptol_champion_elephant", | |||||
], | |||||
[ | |||||
"units/rome_champion_cavalry", | |||||
"units/rome_champion_infantry", | |||||
"units/rome_mechanical_siege_ballista_packed", | |||||
"units/rome_mechanical_siege_scorpio_packed", | |||||
], | |||||
[ | |||||
"units/sele_champion_cavalry", | |||||
"units/sele_champion_chariot", | |||||
"units/sele_champion_elephant", | |||||
"units/sele_champion_infantry_pikeman", | |||||
"units/sele_champion_infantry_swordsman", | |||||
], | |||||
[ | |||||
"units/spart_champion_infantry_pike", | |||||
"units/spart_champion_infantry_spear", | |||||
"units/spart_champion_infantry_sword", | |||||
"units/spart_mechanical_siege_ram", | |||||
], | |||||
]; | |||||
Trigger.prototype.StartAnEnemyWave = function() | Trigger.prototype.debugLog = function(txt) | ||||
{ | { | ||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | if (!debugLog) | ||||
let attackerTemplates = attackerEntityTemplates[Math.floor(Math.random() * attackerEntityTemplates.length)]; | return; | ||||
// A soldier for each 2-3 minutes of the game. Should be waves of 20 soldiers after an hour | |||||
let nextTime = Math.round(120000 + Math.random() * 60000); | |||||
let attackersPerTemplate = Math.ceil(cmpTimer.GetTime() / nextTime / attackerTemplates.length); | |||||
let spawned = false; | |||||
for (let point of this.GetTriggerPoints("A")) | print("DEBUG [" + Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n"); | ||||
}; | |||||
Trigger.prototype.LoadAttackerTemplates = function() | |||||
{ | { | ||||
let cmpPlayer = QueryOwnerInterface(point, IID_Player); | let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); | ||||
if (cmpPlayer.GetPlayerID() == 0 || cmpPlayer.GetState() != "active") | |||||
continue; | |||||
let cmpPosition = Engine.QueryInterface(this.playerCivicCenter[cmpPlayer.GetPlayerID()], IID_Position); | for (let templateName of cmpTemplateManager.FindAllTemplates(false)) | ||||
if (!cmpPosition || !cmpPosition.IsInWorld) | { | ||||
if (!templateName.startsWith("units/") || templateName.endsWith("_unpacked") || templateName.endsWith("_barracks")) | |||||
continue; | continue; | ||||
let targetPos = cmpPosition.GetPosition(); | |||||
for (let template of attackerTemplates) | let identity = cmpTemplateManager.GetTemplate(templateName).Identity; | ||||
{ | |||||
let entities = TriggerHelper.SpawnUnits(point, template, attackersPerTemplate, 0); | |||||
ProcessCommand(0, { | if (!attackerUnitTemplates[identity.Civ]) | ||||
"type": "attack-walk", | attackerUnitTemplates[identity.Civ] = { | ||||
"entities": entities, | "heroes": [], | ||||
"x": targetPos.x, | "champions": [], | ||||
"z": targetPos.z, | "siege": [] | ||||
"queued": true, | }; | ||||
"targetClasses": undefined | |||||
}); | |||||
} | |||||
spawned = true; | |||||
} | |||||
if (!spawned) | let classes = GetIdentityClasses(identity); | ||||
return; | |||||
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); | // Notice some heroes are elephants and war elephants are champions | ||||
cmpGUIInterface.PushNotification({ | if (classes.indexOf("Hero") != -1) | ||||
"message": markForTranslation("An enemy wave is attacking!"), | attackerUnitTemplates[identity.Civ].heroes.push(templateName); | ||||
"translateMessage": true | else if (classes.indexOf("Siege") != -1 || classes.indexOf("Elephant") != -1 && classes.indexOf("Melee") != -1) | ||||
}); | attackerUnitTemplates[identity.Civ].siege.push(templateName); | ||||
this.DoAfterDelay(nextTime, "StartAnEnemyWave", {}); // The next wave will come in 3 minutes | else if (classes.indexOf("Champion") != -1) | ||||
attackerUnitTemplates[identity.Civ].champions.push(templateName); | |||||
} | |||||
this.debugLog("Attacker templates:"); | |||||
this.debugLog(uneval(attackerUnitTemplates)); | |||||
}; | }; | ||||
Trigger.prototype.InitGame = function() | Trigger.prototype.SetDisableTemplates = function() | ||||
{ | |||||
for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i) | |||||
{ | { | ||||
let numberOfPlayers = TriggerHelper.GetNumberOfPlayers(); | let cmpPlayer = QueryPlayerIDInterface(i); | ||||
// Find all of the civic centers, disable some structures | cmpPlayer.SetDisabledTemplates(disabledTemplates(cmpPlayer.GetCiv())); | ||||
for (let i = 1; i < numberOfPlayers; ++i) | } | ||||
}; | |||||
/** | |||||
* Remember civic centers and make women invincible. | |||||
*/ | |||||
Trigger.prototype.InitStartingUnits = function() | |||||
{ | { | ||||
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); | let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); | ||||
let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i); // Get all of each player's entities | for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i) | ||||
{ | |||||
let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i); | |||||
for (let entity of playerEntities) | for (let entity of playerEntities) | ||||
{ | { | ||||
if (TriggerHelper.EntityHasClass(entity, "CivilCentre")) | if (TriggerHelper.EntityHasClass(entity, "CivilCentre")) | ||||
this.playerCivicCenter[i] = entity; | this.playerCivicCenter[i] = entity; | ||||
else if (TriggerHelper.EntityHasClass(entity, "FemaleCitizen")) | else if (TriggerHelper.EntityHasClass(entity, "FemaleCitizen")) | ||||
{ | { | ||||
this.treasureFemale[i] = entity; | |||||
let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver); | let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver); | ||||
cmpDamageReceiver.SetInvulnerability(true); | cmpDamageReceiver.SetInvulnerability(true); | ||||
let cmpHealth = Engine.QueryInterface(entity, IID_Health); | let cmpHealth = Engine.QueryInterface(entity, IID_Health); | ||||
cmpHealth.SetUndeletable(true); | cmpHealth.SetUndeletable(true); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
}; | |||||
this.PlaceTreasures(); | Trigger.prototype.InitializeEnemyWaves = function() | ||||
{ | |||||
let time = randFloat(...firstWaveTime) * 60 * 1000; | |||||
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({ | |||||
"message": markForTranslation("The first wave will start in %(time)s!"), | |||||
"translateMessage": true | |||||
}, time); | |||||
this.DoAfterDelay(time, "StartAnEnemyWave", {}); | |||||
}; | |||||
for (let i = 1; i < numberOfPlayers; ++i) | Trigger.prototype.StartAnEnemyWave = function() | ||||
{ | { | ||||
let cmpPlayer = QueryPlayerIDInterface(i); | let currentMin = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000; | ||||
let civ = cmpPlayer.GetCiv(); | let nextWaveTime = randFloat(...waveTime); | ||||
cmpPlayer.SetDisabledTemplates(disabledTemplates(civ)); | let civ = pickRandom(Object.keys(attackerUnitTemplates)); | ||||
// Determine total attacker count of the current wave. | |||||
// Exponential increase with time, capped to the limit and fluctuating proportionally with the current wavetime. | |||||
let totalAttackers = Math.ceil(Math.min(totalAttackerLimit, | |||||
initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / waveTime[1])); | |||||
this.debugLog("Spawning " + totalAttackers + " attackers"); | |||||
let attackerTemplates = []; | |||||
// Add hero | |||||
if (currentMin > randFloat(...heroTime) && attackerUnitTemplates[civ].heroes.length) | |||||
{ | |||||
this.debugLog("Spawning hero"); | |||||
attackerTemplates.push({ | |||||
"template": pickRandom(attackerUnitTemplates[civ].heroes), | |||||
"count": 1, | |||||
"hero": true | |||||
}); | |||||
--totalAttackers; | |||||
} | } | ||||
// Random siege to champion ratio | |||||
let siegeRatio = randFloat(...siegeFraction); | |||||
let siegeCount = Math.round(siegeRatio * totalAttackers); | |||||
this.debugLog("Siege Ratio: " + Math.round(siegeRatio * 100) + "%"); | |||||
let attackerTypeCounts = { | |||||
"siege": siegeCount, | |||||
"champions": totalAttackers - siegeCount | |||||
}; | }; | ||||
Trigger.prototype.PlaceTreasures = function() | this.debugLog("Spawning:" + uneval(attackerTypeCounts)); | ||||
// Random ratio of the given templates | |||||
for (let attackerType in attackerTypeCounts) | |||||
{ | { | ||||
let point = ["B", "C", "D"][Math.floor(Math.random() * 3)]; | let attackerTypeTemplates = attackerUnitTemplates[civ][attackerType]; | ||||
let triggerPoints = this.GetTriggerPoints(point); | let attackerEntityRatios = new Array(attackerTypeTemplates.length).fill(1).map(i => randFloat(0, 1)); | ||||
for (let point of triggerPoints) | let attackerEntityRatioSum = attackerEntityRatios.reduce((current, sum) => current + sum, 0); | ||||
let remainder = attackerTypeCounts[attackerType]; | |||||
for (let i in attackerTypeTemplates) | |||||
{ | |||||
let count = | |||||
+i == attackerTypeTemplates.length - 1 ? | |||||
remainder : | |||||
Math.round(attackerEntityRatios[i] / attackerEntityRatioSum * attackerTypeCounts[attackerType]); | |||||
attackerTemplates.push({ | |||||
"template": attackerTypeTemplates[i], | |||||
"count": count | |||||
}); | |||||
remainder -= count; | |||||
} | |||||
if (remainder != 0) | |||||
warn("Didn't spawn as many attackers as intended: " + remainder); | |||||
} | |||||
this.debugLog("Templates: " + uneval(attackerTemplates)); | |||||
// Spawn the templates | |||||
let spawned = false; | |||||
for (let point of this.GetTriggerPoints("A")) | |||||
{ | { | ||||
let template = treasures[Math.floor(Math.random() * treasures.length)]; | if (dryRun) | ||||
TriggerHelper.SpawnUnits(point, template, 1, 0); | { | ||||
spawned = true; | |||||
break; | |||||
} | } | ||||
this.DoAfterDelay(4*60*1000, "PlaceTreasures", {}); // Place more treasures after 4 minutes | |||||
}; | |||||
Trigger.prototype.InitializeEnemyWaves = function() | let cmpPlayer = QueryOwnerInterface(point, IID_Player); | ||||
// Trigger point owned by Gaia if the player is defeated | |||||
if (cmpPlayer.GetPlayerID() == 0) | |||||
continue; | |||||
let targetPos = Engine.QueryInterface(this.playerCivicCenter[cmpPlayer.GetPlayerID()], IID_Position).GetPosition2D(); | |||||
for (let attackerTemplate of attackerTemplates) | |||||
{ | { | ||||
let time = (5 + Math.round(Math.random() * 10)) * 60 * 1000; | // Don't spawn gaia hero if the previous one is still alive | ||||
if (attackerTemplate.hero && this.gaiaHeroes[cmpPlayer.GetPlayerID()]) | |||||
{ | |||||
let cmpHealth = Engine.QueryInterface(this.gaiaHeroes[cmpPlayer.GetPlayerID()], IID_Health); | |||||
if (cmpHealth && cmpHealth.GetHitpoints() != 0) | |||||
{ | |||||
this.debugLog("Not spawning hero for player " + cmpPlayer.GetPlayerID() + " as the previous one is still alive"); | |||||
continue; | |||||
} | |||||
} | |||||
if (dryRun) | |||||
continue; | |||||
let entities = TriggerHelper.SpawnUnits(point, attackerTemplate.template, attackerTemplate.count, 0); | |||||
ProcessCommand(0, { | |||||
"type": "attack-walk", | |||||
"entities": entities, | |||||
"x": targetPos.x, | |||||
"z": targetPos.y, | |||||
"queued": true, | |||||
"targetClasses": undefined | |||||
}); | |||||
if (attackerTemplate.hero) | |||||
this.gaiaHeroes[cmpPlayer.GetPlayerID()] = entities[0]; | |||||
} | |||||
spawned = true; | |||||
} | |||||
if (!spawned) | |||||
return; | |||||
let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); | let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); | ||||
cmpGUIInterface.AddTimeNotification({ | cmpGUIInterface.PushNotification({ | ||||
"message": markForTranslation("The first wave will start in %(time)s!"), | "message": markForTranslation("An enemy wave is attacking!"), | ||||
"translateMessage": true | "translateMessage": true | ||||
}, time); | }); | ||||
this.DoAfterDelay(time, "StartAnEnemyWave", {}); | this.DoAfterDelay(nextWaveTime * 60 * 1000, "StartAnEnemyWave", {}); | ||||
}; | |||||
Trigger.prototype.PlaceTreasures = function() | |||||
{ | |||||
let point = pickRandom(["B", "C", "D"]); | |||||
let triggerPoints = this.GetTriggerPoints(point); | |||||
for (let point of triggerPoints) | |||||
TriggerHelper.SpawnUnits(point, pickRandom(treasures), 1, 0); | |||||
this.DoAfterDelay(randFloat(...treasureTime) * 60 * 1000, "PlaceTreasures", {}); | |||||
}; | }; | ||||
Trigger.prototype.DefeatPlayerOnceCCIsDestroyed = function(data) | Trigger.prototype.OnOwnershipChanged = function(data) | ||||
{ | { | ||||
if (data.entity == this.playerCivicCenter[data.from]) | if (data.entity == this.playerCivicCenter[data.from]) | ||||
TriggerHelper.DefeatPlayer(data.from); | TriggerHelper.DefeatPlayer(data.from); | ||||
else if (data.entity == this.treasureFemale[data.from]) | |||||
{ | |||||
this.treasureFemale[data.from] = undefined; | |||||
Engine.DestroyEntity(data.entity); | |||||
} | |||||
}; | }; | ||||
{ | { | ||||
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); | let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); | ||||
cmpTrigger.playerCivicCenter = {}; | |||||
cmpTrigger.DoAfterDelay(1000, "InitializeEnemyWaves", {}); | cmpTrigger.treasureFemale = []; | ||||
cmpTrigger.RegisterTrigger("OnInitGame", "InitGame", { "enabled": true }); | cmpTrigger.playerCivicCenter = []; | ||||
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DefeatPlayerOnceCCIsDestroyed", { "enabled": true }); | cmpTrigger.gaiaHeroes = []; | ||||
cmpTrigger.RegisterTrigger("OnInitGame", "InitSurvival", { "enabled": true }); | |||||
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true }); | |||||
} | } |
Wildfire Games · Phabricator