/**
* @class
* 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;
/**
* Get the number of minutes to pass between spawning new treasures.
*/
var treasureTime = () => randFloat(3, 5);
/**
* Get the time in minutes when the first wave of attackers will be spawned.
*/
var firstWaveTime = () => randFloat(4, 6);
/**
* Maximum time in minutes between two consecutive waves.
*/
var maxWaveTime = 4;
/**
* Get the next attacker wave delay.
*/
var waveTime = () => randFloat(0.5, 1) * maxWaveTime;
/**
* 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 = 200;
/**
* Least and greatest amount of siege engines per wave.
*/
var siegeFraction = () => randFloat(0.2, 0.5);
/**
* Potentially / definitely spawn a gaia hero after this number of minutes.
*/
var heroTime = () => randFloat(20, 60);
/**
* The following templates can't be built by any player.
*/
var disabledTemplates = (civ) => [
// Economic structures
"structures/" + civ + "/corral",
"structures/" + civ + "/farmstead",
"structures/" + civ + "/field",
"structures/" + civ + "/storehouse",
"structures/" + civ + "/rotarymill",
"units/maur/support_elephant",
// Expansions
"structures/" + civ + "/civil_centre",
"structures/" + civ + "/military_colony",
// Walls
"structures/" + civ + "/wallset_stone",
"structures/rome/wallset_siege",
"structures/wallset_palisade",
// Shoreline
"structures/" + civ + "/dock",
"structures/brit/crannog",
"structures/cart/super_dock",
"structures/ptol/lighthouse"
];
/**
* Spawn these treasures in regular intervals.
*/
var treasures = [
"gaia/treasure/food_barrel",
"gaia/treasure/food_bin",
"gaia/treasure/food_crate",
"gaia/treasure/food_jars",
"gaia/treasure/metal",
"gaia/treasure/stone",
"gaia/treasure/wood",
"gaia/treasure/wood",
"gaia/treasure/wood"
];
/**
* An object that maps from civ [f.e. "spart"] to an object
* that has the keys "champions", "siege" and "heroes",
* which is an array containing all these templates,
* trainable from a building or not.
*/
var attackerUnitTemplates = {};
Trigger.prototype.InitSurvival = function()
{
this.InitStartingUnits();
this.LoadAttackerTemplates();
this.SetDisableTemplates();
this.PlaceTreasures();
this.InitializeEnemyWaves();
};
Trigger.prototype.debugLog = function(txt)
{
if (!debugLog)
return;
print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n");
};
Trigger.prototype.LoadAttackerTemplates = function()
{
for (let civ of ["gaia", ...Object.keys(loadCivFiles(false))])
attackerUnitTemplates[civ] = {
"heroes": TriggerHelper.GetTemplateNamesByClasses("Hero", civ, undefined, true),
"champions": TriggerHelper.GetTemplateNamesByClasses("Champion+!Elephant", civ, undefined, true),
"siege": TriggerHelper.GetTemplateNamesByClasses("Siege Champion+Elephant", civ, "packed", undefined)
};
this.debugLog("Attacker templates:");
this.debugLog(uneval(attackerUnitTemplates));
};
Trigger.prototype.SetDisableTemplates = function()
{
for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i)
QueryPlayerIDInterface(i).SetDisabledTemplates(disabledTemplates(QueryPlayerIDInterface(i, IID_Identity).GetCiv()));
};
/**
* Remember civic centers and make women invincible.
*/
Trigger.prototype.InitStartingUnits = function()
{
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
{
this.playerCivicCenter[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "CivilCentre")[0];
this.treasureFemale[playerID] = TriggerHelper.GetPlayerEntitiesByClass(playerID, "FemaleCitizen")[0];
Engine.QueryInterface(this.treasureFemale[playerID], IID_Resistance).SetInvulnerability(true);
}
};
Trigger.prototype.InitializeEnemyWaves = function()
{
let time = 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", {});
};
Trigger.prototype.StartAnEnemyWave = function()
{
let currentMin = TriggerHelper.GetMinutes();
let nextWaveTime = waveTime();
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 / maxWaveTime));
let siegeRatio = siegeFraction();
this.debugLog("Spawning " + totalAttackers + " attackers, siege ratio " + siegeRatio.toFixed(2));
let attackerCount = TriggerHelper.BalancedTemplateComposition(
[
{
"templates": attackerUnitTemplates[civ].heroes,
"count": currentMin > heroTime() && attackerUnitTemplates[civ].heroes.length ? 1 : 0
},
{
"templates": attackerUnitTemplates[civ].siege,
"frequency": siegeRatio
},
{
"templates": attackerUnitTemplates[civ].champions,
"frequency": 1 - siegeRatio
}
],
totalAttackers);
this.debugLog("Templates: " + uneval(attackerCount));
// Spawn the templates
let spawned = false;
for (let point of this.GetTriggerPoints("A"))
{
if (dryRun)
{
spawned = true;
break;
}
// Don't spawn attackers for defeated players and players that lost their cc after win
let cmpPlayer = QueryOwnerInterface(point, IID_Player);
if (!cmpPlayer)
continue;
let playerID = cmpPlayer.GetPlayerID();
let civicCentre = this.playerCivicCenter[playerID];
if (!civicCentre)
continue;
// Check if the cc is garrisoned in another building
let targetPos = TriggerHelper.GetEntityPosition2D(civicCentre);
if (!targetPos)
continue;
for (let templateName in attackerCount)
{
let isHero = attackerUnitTemplates[civ].heroes.indexOf(templateName) != -1;
// Don't spawn gaia hero if the previous one is still alive
if (this.gaiaHeroes[playerID] && isHero)
{
let cmpHealth = Engine.QueryInterface(this.gaiaHeroes[playerID], IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() != 0)
{
this.debugLog("Not spawning hero for player " + playerID + " as the previous one is still alive");
continue;
}
}
if (dryRun)
continue;
let entities = TriggerHelper.SpawnUnits(point, templateName, attackerCount[templateName], 0);
ProcessCommand(0, {
"type": "attack-walk",
"entities": entities,
"x": targetPos.x,
"z": targetPos.y,
"targetClasses": undefined,
"allowCapture": false,
"queued": true
});
if (isHero)
this.gaiaHeroes[playerID] = entities[0];
}
spawned = true;
}
if (!spawned)
return;
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"message": markForTranslation("An enemy wave is attacking!"),
"translateMessage": true
});
this.DoAfterDelay(nextWaveTime * 60 * 1000, "StartAnEnemyWave", {});
};
Trigger.prototype.PlaceTreasures = function()
{
let triggerPoints = this.GetTriggerPoints(pickRandom(["B", "C", "D"]));
for (let point of triggerPoints)
TriggerHelper.SpawnUnits(point, pickRandom(treasures), 1, 0);
this.DoAfterDelay(treasureTime() * 60 * 1000, "PlaceTreasures", {});
};
Trigger.prototype.OnOwnershipChanged = function(data)
{
if (data.entity == this.playerCivicCenter[data.from])
{
this.playerCivicCenter[data.from] = undefined;
TriggerHelper.DefeatPlayer(
data.from,
markForTranslation("%(player)s has been defeated (lost civic center)."));
}
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);
cmpTrigger.treasureFemale = [];
cmpTrigger.playerCivicCenter = [];
cmpTrigger.gaiaHeroes = [];
cmpTrigger.RegisterTrigger("OnInitGame", "InitSurvival", { "enabled": true });
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true });
}