Index: ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js (revision 26871)
+++ ps/trunk/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js (revision 26872)
@@ -1,311 +1,308 @@
/**
* 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)
- {
- let cmpPlayer = QueryPlayerIDInterface(i);
- cmpPlayer.SetDisabledTemplates(disabledTemplates(cmpPlayer.GetCiv()));
- }
+ 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 });
}
Index: ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js
===================================================================
--- ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js (revision 26871)
+++ ps/trunk/binaries/data/mods/public/maps/scripts/Regicide.js (revision 26872)
@@ -1,139 +1,139 @@
Trigger.prototype.InitRegicideGame = function(msg)
{
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
let regicideGarrison = cmpEndGameManager.GetGameSettings().regicideGarrison;
let playersCivs = [];
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
- playersCivs[playerID] = QueryPlayerIDInterface(playerID).GetCiv();
+ playersCivs[playerID] = QueryPlayerIDInterface(playerID, IID_Identity).GetCiv();
// Get all hero templates of these civs
let heroTemplates = {};
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for (let templateName of cmpTemplateManager.FindAllTemplates(false))
{
if (!templateName.startsWith("units/"))
continue;
let identity = cmpTemplateManager.GetTemplate(templateName).Identity;
let classes = GetIdentityClasses(identity);
if (classes.indexOf("Hero") == -1 ||
playersCivs.every(civ => civ != identity.Civ))
continue;
if (!heroTemplates[identity.Civ])
heroTemplates[identity.Civ] = [];
if (heroTemplates[identity.Civ].indexOf(templateName) == -1)
heroTemplates[identity.Civ].push({
"templateName": regicideGarrison ? templateName : "ungarrisonable|" + templateName,
"classes": classes
});
}
// Sort available spawn points by preference
let spawnPreferences = ["CivilCentre", "Structure", "Ship"];
let getSpawnPreference = entity => {
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
if (!cmpIdentity)
return -1;
let classes = cmpIdentity.GetClassesList();
for (let i in spawnPreferences)
if (classes.indexOf(spawnPreferences[i]) != -1)
return spawnPreferences.length - i;
return 0;
};
// Attempt to spawn one hero per player
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let playerID = 1; playerID < TriggerHelper.GetNumberOfPlayers(); ++playerID)
{
let spawnPoints = cmpRangeManager.GetEntitiesByPlayer(playerID).sort((entity1, entity2) =>
getSpawnPreference(entity2) - getSpawnPreference(entity1));
// Spawn the hero on land as close as possible
if (!regicideGarrison && TriggerHelper.EntityMatchesClassList(spawnPoints[0], "Ship"))
{
let shipPosition = Engine.QueryInterface(spawnPoints[0], IID_Position).GetPosition2D();
let distanceToShip = entity =>
Engine.QueryInterface(entity, IID_Position).GetPosition2D().distanceToSquared(shipPosition);
spawnPoints = TriggerHelper.GetLandSpawnPoints().sort((entity1, entity2) =>
distanceToShip(entity1) - distanceToShip(entity2));
}
this.regicideHeroes[playerID] = this.SpawnRegicideHero(playerID, heroTemplates[playersCivs[playerID]], spawnPoints);
}
};
/**
* Spawn a random hero at one of the given locations (which are checked in order).
* Garrison it if the location is a ship.
*
* @param spawnPoints - entity IDs at which to spawn
*/
Trigger.prototype.SpawnRegicideHero = function(playerID, heroTemplates, spawnPoints)
{
for (let heroTemplate of shuffleArray(heroTemplates))
for (let spawnPoint of spawnPoints)
{
let cmpPosition = Engine.QueryInterface(spawnPoint, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
// Consider nomad maps where units start on a ship
let isShip = TriggerHelper.EntityMatchesClassList(spawnPoint, "Ship");
if (isShip)
{
let cmpGarrisonHolder = Engine.QueryInterface(spawnPoint, IID_GarrisonHolder);
if (cmpGarrisonHolder.IsFull() ||
!MatchesClassList(heroTemplate.classes, cmpGarrisonHolder.GetAllowedClasses()))
continue;
}
let hero = TriggerHelper.SpawnUnits(spawnPoint, heroTemplate.templateName, 1, playerID);
if (!hero.length)
continue;
hero = hero[0];
if (isShip)
{
let cmpUnitAI = Engine.QueryInterface(hero, IID_UnitAI);
cmpUnitAI.Garrison(spawnPoint);
}
return hero;
}
error("Couldn't spawn hero for player " + playerID);
return undefined;
};
Trigger.prototype.RenameRegicideHero = function(data)
{
let index = this.regicideHeroes.indexOf(data.entity);
if (index != -1)
this.regicideHeroes[index] = data.newentity;
};
Trigger.prototype.CheckRegicideDefeat = function(data)
{
if (data.entity == this.regicideHeroes[data.from])
TriggerHelper.DefeatPlayer(
data.from,
markForTranslation("%(player)s has been defeated (lost hero)."));
};
{
let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.regicideHeroes = [];
cmpTrigger.DoAfterDelay(0, "InitRegicideGame", {});
cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckRegicideDefeat", { "enabled": true });
cmpTrigger.RegisterTrigger("OnEntityRenamed", "RenameRegicideHero", { "enabled": true });
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js (revision 26871)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js (revision 26872)
@@ -1,733 +1,728 @@
function Trainer() {}
Trainer.prototype.Schema =
"Allows the entity to train new units." +
"" +
"0.7" +
"" +
"\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
/**
* This object represents a batch of entities being trained.
* @param {string} templateName - The name of the template we ought to train.
* @param {number} count - The size of the batch to train.
* @param {number} trainer - The entity ID of our trainer.
* @param {string} metadata - Optionally any metadata to attach to us.
*/
Trainer.prototype.Item = function(templateName, count, trainer, metadata)
{
this.count = count;
this.templateName = templateName;
this.trainer = trainer;
this.metadata = metadata;
};
/**
* Prepare for the queue.
* @param {Object} trainCostMultiplier - The multipliers to use when calculating costs.
* @param {number} batchTimeMultiplier - The factor to use when training this batches.
*
* @return {boolean} - Whether the item was successfully initiated.
*/
Trainer.prototype.Item.prototype.Queue = function(trainCostMultiplier, batchTimeMultiplier)
{
if (!Number.isInteger(this.count) || this.count <= 0)
{
error("Invalid batch count " + this.count + ".");
return false;
}
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
const template = cmpTemplateManager.GetTemplate(this.templateName);
if (!template)
return false;
const cmpPlayer = QueryOwnerInterface(this.trainer);
if (!cmpPlayer)
return false;
this.player = cmpPlayer.GetPlayerID();
this.resources = {};
const totalResources = {};
for (const res in template.Cost.Resources)
{
this.resources[res] = trainCostMultiplier[res] *
ApplyValueModificationsToTemplate(
"Cost/Resources/" + res,
+template.Cost.Resources[res],
this.player,
template);
totalResources[res] = Math.floor(this.count * this.resources[res]);
}
// TrySubtractResources should report error to player (they ran out of resources).
if (!cmpPlayer.TrySubtractResources(totalResources))
return false;
this.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, this.player, template);
if (template.TrainingRestrictions)
{
const unitCategory = template.TrainingRestrictions.Category;
const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
{
if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, this.count, this.templateName, template.TrainingRestrictions.MatchLimit))
// Already warned, return.
{
cmpPlayer.RefundResources(totalResources);
return false;
}
// ToDo: Should warn here v and return?
cmpPlayerEntityLimits.ChangeCount(unitCategory, this.count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, this.count);
}
}
const buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, this.player, template);
const time = batchTimeMultiplier * trainCostMultiplier.time * buildTime * 1000;
this.timeRemaining = time;
this.timeTotal = time;
const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("OnTrainingQueued", {
"playerid": this.player,
"unitTemplate": this.templateName,
"count": this.count,
"metadata": this.metadata,
"trainerEntity": this.trainer
});
return true;
};
/**
* Destroy cached entities, refund resources and free (population) limits.
*/
Trainer.prototype.Item.prototype.Stop = function()
{
// Destroy any cached entities (those which didn't spawn for some reason).
if (this.entities?.length)
{
for (const ent of this.entities)
Engine.DestroyEntity(ent);
delete this.entities;
}
const cmpPlayer = QueryPlayerIDInterface(this.player);
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
const template = cmpTemplateManager.GetTemplate(this.templateName);
if (template.TrainingRestrictions)
{
const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -this.count);
if (template.TrainingRestrictions.MatchLimit)
cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, -this.count);
}
const cmpStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker);
const totalCosts = {};
for (const resource in this.resources)
{
totalCosts[resource] = Math.floor(this.count * this.resources[resource]);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceUsedCounter(resource, -totalCosts[resource]);
}
if (cmpPlayer)
{
if (this.started)
cmpPlayer.UnReservePopulationSlots(this.population * this.count);
cmpPlayer.RefundResources(totalCosts);
cmpPlayer.UnBlockTraining();
}
delete this.resources;
};
/**
* This starts the item, reserving population.
* @return {boolean} - Whether the item was started successfully.
*/
Trainer.prototype.Item.prototype.Start = function()
{
const cmpPlayer = QueryPlayerIDInterface(this.player);
if (!cmpPlayer)
return false;
const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(this.templateName);
this.population = ApplyValueModificationsToTemplate(
"Cost/Population",
+template.Cost.Population,
this.player,
template);
this.missingPopSpace = cmpPlayer.TryReservePopulationSlots(this.population * this.count);
if (this.missingPopSpace)
{
cmpPlayer.BlockTraining();
return false;
}
cmpPlayer.UnBlockTraining();
Engine.PostMessage(this.trainer, MT_TrainingStarted, { "entity": this.trainer });
this.started = true;
return true;
};
Trainer.prototype.Item.prototype.Finish = function()
{
this.Spawn();
if (!this.count)
this.finished = true;
};
/*
* This function creates the entities and places them in world if possible
* (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
*/
Trainer.prototype.Item.prototype.Spawn = function()
{
const createdEnts = [];
const spawnedEnts = [];
// We need entities to test spawning, but we don't want to waste resources,
// so only create them once and use as needed.
if (!this.entities)
{
this.entities = [];
for (let i = 0; i < this.count; ++i)
this.entities.push(Engine.AddEntity(this.templateName));
}
let autoGarrison;
const cmpRallyPoint = Engine.QueryInterface(this.trainer, IID_RallyPoint);
if (cmpRallyPoint)
{
const data = cmpRallyPoint.GetData()[0];
if (data?.target && data.target == this.trainer && data.command == "garrison")
autoGarrison = true;
}
const cmpFootprint = Engine.QueryInterface(this.trainer, IID_Footprint);
const cmpPosition = Engine.QueryInterface(this.trainer, IID_Position);
const positionTrainer = cmpPosition && cmpPosition.GetPosition();
const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
const cmpPlayerStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker);
while (this.entities.length)
{
const ent = this.entities[0];
const cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
let garrisoned = false;
if (autoGarrison)
{
const cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
{
// Temporary owner affectation needed for GarrisonHolder checks.
cmpNewOwnership.SetOwnerQuiet(this.player);
garrisoned = cmpGarrisonable.Garrison(this.trainer);
cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER);
}
}
if (!garrisoned)
{
const pos = cmpFootprint.PickSpawnPoint(ent);
if (pos.y < 0)
break;
const cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
cmpNewPosition.JumpTo(pos.x, pos.z);
if (positionTrainer)
cmpNewPosition.SetYRotation(positionTrainer.horizAngleTo(pos));
spawnedEnts.push(ent);
}
// Decrement entity count in the EntityLimits component
// since it will be increased by EntityLimits.OnGlobalOwnershipChanged,
// i.e. we replace a 'trained' entity by 'alive' one.
// Must be done after spawn check so EntityLimits decrements only if unit spawns.
if (cmpPlayerEntityLimits)
{
const cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
if (cmpTrainingRestrictions)
cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1);
}
cmpNewOwnership.SetOwner(this.player);
if (cmpPlayerStatisticsTracker)
cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
this.count--;
this.entities.shift();
createdEnts.push(ent);
}
if (spawnedEnts.length && !autoGarrison && cmpRallyPoint)
for (const com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts))
ProcessCommand(this.player, com);
const cmpPlayer = QueryOwnerInterface(this.trainer);
if (createdEnts.length)
{
if (this.population)
cmpPlayer.UnReservePopulationSlots(this.population * createdEnts.length);
// Play a sound, but only for the first in the batch (to avoid nasty phasing effects).
PlaySound("trained", createdEnts[0]);
Engine.PostMessage(this.trainer, MT_TrainingFinished, {
"entities": createdEnts,
"owner": this.player,
"metadata": this.metadata
});
}
if (this.count)
{
cmpPlayer.BlockTraining();
if (!this.spawnNotified)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("Can't find free space to spawn trained units."),
"translateMessage": true
});
this.spawnNotified = true;
}
}
else
{
cmpPlayer.UnBlockTraining();
delete this.spawnNotified;
}
};
/**
* @param {number} allocatedTime - The time allocated to this item.
* @return {number} - The time used for this item.
*/
Trainer.prototype.Item.prototype.Progress = function(allocatedTime)
{
if (this.paused)
this.Unpause();
// We couldn't start this timeout, try again later.
if (!this.started && !this.Start())
return allocatedTime;
if (this.timeRemaining > allocatedTime)
{
this.timeRemaining -= allocatedTime;
return allocatedTime;
}
this.Finish();
return this.timeRemaining;
};
Trainer.prototype.Item.prototype.Pause = function()
{
if (this.started)
this.paused = true;
else if (this.missingPopSpace)
{
delete this.missingPopSpace;
QueryOwnerInterface(this.trainer)?.UnBlockTraining();
}
};
Trainer.prototype.Item.prototype.Unpause = function()
{
delete this.paused;
};
/**
* @return {Object} - Some basic information of this batch.
*/
Trainer.prototype.Item.prototype.GetBasicInfo = function()
{
return {
"unitTemplate": this.templateName,
"count": this.count,
"neededSlots": this.missingPopSpace,
"progress": 1 - (this.timeRemaining / (this.timeTotal || 1)),
"timeRemaining": this.timeRemaining,
"paused": this.paused,
"metadata": this.metadata
};
};
Trainer.prototype.Item.prototype.SerializableAttributes = [
"count",
"entities",
"metadata",
"missingPopSpace",
"paused",
"player",
"population",
"trainer",
"resources",
"started",
"templateName",
"timeRemaining",
"timeTotal"
];
Trainer.prototype.Item.prototype.Serialize = function(id)
{
const result = {
"id": id
};
for (const att of this.SerializableAttributes)
if (this.hasOwnProperty(att))
result[att] = this[att];
return result;
};
Trainer.prototype.Item.prototype.Deserialize = function(data)
{
for (const att of this.SerializableAttributes)
if (att in data)
this[att] = data[att];
};
Trainer.prototype.Init = function()
{
this.nextID = 1;
this.queue = new Map();
this.trainCostMultiplier = {};
};
Trainer.prototype.SerializableAttributes = [
"entitiesMap",
"nextID",
"trainCostMultiplier"
];
Trainer.prototype.Serialize = function()
{
const queue = [];
for (const [id, item] of this.queue)
queue.push(item.Serialize(id));
const result = {
"queue": queue
};
for (const att of this.SerializableAttributes)
if (this.hasOwnProperty(att))
result[att] = this[att];
return result;
};
Trainer.prototype.Deserialize = function(data)
{
for (const att of this.SerializableAttributes)
if (att in data)
this[att] = data[att];
this.queue = new Map();
for (const item of data.queue)
{
const newItem = new this.Item();
newItem.Deserialize(item);
this.queue.set(item.id, newItem);
}
};
/*
* Returns list of entities that can be trained by this entity.
*/
Trainer.prototype.GetEntitiesList = function()
{
return Array.from(this.entitiesMap.values());
};
/**
* Calculate the new list of producible entities
* and update any entities currently being produced.
*/
Trainer.prototype.CalculateEntitiesMap = function()
{
// Don't reset the map, it's used below to update entities.
if (!this.entitiesMap)
this.entitiesMap = new Map();
const string = this.template?.Entities?._string || "";
// Tokens can be added -> process an empty list to get them.
let addedTokens = ApplyValueModificationsToEntity("Trainer/Entities/_string", "", this.entity);
if (!addedTokens && !string)
return;
addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/);
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
const cmpPlayer = QueryOwnerInterface(this.entity);
const disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {};
/**
* Process tokens:
* - process token modifiers (this is a bit tricky).
* - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID
* - remove disabled entities
* - upgrade templates where necessary
* This also updates currently queued production (it's more convenient to do it here).
*/
const removeAllQueuedTemplate = (token) => {
const queue = clone(this.queue);
const template = this.entitiesMap.get(token);
for (const [id, item] of queue)
if (item.templateName == template)
this.StopBatch(id);
};
// ToDo: Notice this doesn't account for entity limits changing due to the template change.
const updateAllQueuedTemplate = (token, updateTo) => {
const template = this.entitiesMap.get(token);
for (const [id, item] of this.queue)
if (item.templateName === template)
item.templateName = updateTo;
};
const toks = string.split(/\s+/);
for (const tok of addedTokens)
toks.push(tok);
const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity)?.GetCiv();
const playerCiv = QueryOwnerInterface(this.entity, IID_Identity)?.GetCiv();
const addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {});
this.entitiesMap = toks.reduce((entMap, token) => {
const rawToken = token;
if (!(token in addedDict))
{
// This is a bit wasteful but I can't think of a simpler/better way.
// The list of token is unlikely to be a performance bottleneck anyways.
token = ApplyValueModificationsToEntity("Trainer/Entities/_string", token, this.entity);
token = token.split(/\s+/);
if (token.every(tok => addedTokens.indexOf(tok) !== -1))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = token[0];
}
// Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
if (nativeCiv)
token = token.replace(/\{native\}/g, nativeCiv);
if (playerCiv)
token = token.replace(/\{civ\}/g, playerCiv);
// Filter out disabled and invalid entities.
if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token))
{
removeAllQueuedTemplate(rawToken);
return entMap;
}
token = this.GetUpgradedTemplate(token);
entMap.set(rawToken, token);
updateAllQueuedTemplate(rawToken, token);
return entMap;
}, new Map());
this.CalculateTrainCostMultiplier();
};
/*
* Returns the upgraded template name if necessary.
*/
Trainer.prototype.GetUpgradedTemplate = function(templateName)
{
const cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return templateName;
const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
while (template && template.Promotion !== undefined)
{
const requiredXp = ApplyValueModificationsToTemplate(
"Promotion/RequiredXp",
+template.Promotion.RequiredXp,
cmpPlayer.GetPlayerID(),
template);
if (requiredXp > 0)
break;
templateName = template.Promotion.Entity;
template = cmpTemplateManager.GetTemplate(templateName);
}
return templateName;
};
Trainer.prototype.CalculateTrainCostMultiplier = function()
{
for (const res of Resources.GetCodes().concat(["time"]))
this.trainCostMultiplier[res] = ApplyValueModificationsToEntity(
"Trainer/TrainCostMultiplier/" + res,
+(this.template?.TrainCostMultiplier?.[res] || 1),
this.entity);
};
/**
* @return {Object} - The multipliers to change the costs of any training activity with.
*/
Trainer.prototype.TrainCostMultiplier = function()
{
return this.trainCostMultiplier;
};
/*
* Returns batch build time.
*/
Trainer.prototype.GetBatchTime = function(batchSize)
{
// TODO: work out what equation we should use here.
return Math.pow(batchSize, ApplyValueModificationsToEntity(
"Trainer/BatchTimeModifier",
+(this.template?.BatchTimeModifier || 1),
this.entity));
};
/**
* @param {string} templateName - The template name to check.
* @return {boolean} - Whether we can train this template.
*/
Trainer.prototype.CanTrain = function(templateName)
{
return this.GetEntitiesList().includes(templateName);
};
/**
* @param {string} templateName - The entity to queue.
* @param {number} count - The batch size.
* @param {string} metadata - Any metadata attached to the item.
*
* @return {number} - The ID of the item. -1 if the item could not be queued.
*/
Trainer.prototype.QueueBatch = function(templateName, count, metadata)
{
const item = new this.Item(templateName, count, this.entity, metadata);
if (!item.Queue(this.TrainCostMultiplier(), this.GetBatchTime(count)))
return -1;
const id = this.nextID++;
this.queue.set(id, item);
return id;
};
/**
* @param {number} id - The ID of the batch being trained here we need to stop.
*/
Trainer.prototype.StopBatch = function(id)
{
this.queue.get(id).Stop();
this.queue.delete(id);
};
/**
* @param {number} id - The ID of the training.
*/
Trainer.prototype.PauseBatch = function(id)
{
this.queue.get(id).Pause();
};
/**
* @param {number} id - The ID of the batch to check.
* @return {boolean} - Whether we are currently training the batch.
*/
Trainer.prototype.HasBatch = function(id)
{
return this.queue.has(id);
};
/**
* @parameter {number} id - The id of the training.
* @return {Object} - Some basic information about the training.
*/
Trainer.prototype.GetBatch = function(id)
{
const item = this.queue.get(id);
return item?.GetBasicInfo();
};
/**
* @param {number} id - The ID of the item we spent time on.
* @param {number} allocatedTime - The time we spent on the given item.
* @return {number} - The time we've actually used.
*/
Trainer.prototype.Progress = function(id, allocatedTime)
{
const item = this.queue.get(id);
const usedTime = item.Progress(allocatedTime);
if (item.finished)
this.queue.delete(id);
return usedTime;
};
-Trainer.prototype.OnCivChanged = function()
-{
- this.CalculateEntitiesMap();
-};
-
Trainer.prototype.OnOwnershipChanged = function(msg)
{
if (msg.to != INVALID_PLAYER)
this.CalculateEntitiesMap();
};
Trainer.prototype.OnValueModification = function(msg)
{
// If the promotion requirements of units is changed,
// update the entities list so that automatically promoted units are shown
// appropriately in the list.
if (msg.component != "Promotion" && (msg.component != "Trainer" ||
!msg.valueNames.some(val => val.startsWith("Trainer/Entities/"))))
return;
if (msg.entities.indexOf(this.entity) === -1)
return;
// This also updates the queued production if necessary.
this.CalculateEntitiesMap();
// Inform the GUI that it'll need to recompute the selection panel.
// TODO: it would be better to only send the message if something actually changing
// for the current training queue.
const cmpPlayer = QueryOwnerInterface(this.entity);
if (cmpPlayer)
Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
};
Trainer.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.CalculateEntitiesMap();
};
Engine.RegisterComponentType(IID_Trainer, "Trainer", Trainer);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Player.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Player.js (revision 26871)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Player.js (revision 26872)
@@ -1,48 +1,41 @@
/**
- * Message of the form { "player": number, "from": string, "to": string }
- * sent from Player component to warn other components when a player changed civs.
- * This should only happen in Atlas.
- */
-Engine.RegisterMessageType("CivChanged");
-
-/**
* Message of the form { "player": number, "otherPlayer": number }
* sent from Player component when diplomacy changed for one player or between two players.
*/
Engine.RegisterMessageType("DiplomacyChanged");
/**
* Message of the form {}
* sent from Player component.
*/
Engine.RegisterMessageType("DisabledTechnologiesChanged");
/**
* Message of the form {}
* sent from Player component.
*/
Engine.RegisterMessageType("DisabledTemplatesChanged");
/**
* Message of the form { "playerID": number }
* sent from Player component when a player is defeated.
*/
Engine.RegisterMessageType("PlayerDefeated");
/**
* Message of the form { "playerID": number }
* sent from Player component when a player has won.
*/
Engine.RegisterMessageType("PlayerWon");
/**
* Message of the form { "to": number, "from": number, "amounts": object }
* sent from Player component whenever a tribute is sent.
*/
Engine.RegisterMessageType("TributeExchanged");
/**
* Message of the form { "player": player, "type": "cheat" }
* sent from Player when some multiplier of that player has changed
*/
Engine.RegisterMessageType("MultiplierChanged");