Changeset View
Changeset View
Standalone View
Standalone View
ps/trunk/binaries/data/mods/public/simulation/components/Trainer.js
Property | Old Value | New Value |
---|---|---|
svn:eol-style | null | native \ No newline at end of property |
svn:mime-type | null | text/plain \ No newline at end of property |
function Trainer() {} | |||||
Trainer.prototype.Schema = | |||||
"<a:help>Allows the entity to train new units.</a:help>" + | |||||
"<a:example>" + | |||||
"<BatchTimeModifier>0.7</BatchTimeModifier>" + | |||||
"<Entities datatype='tokens'>" + | |||||
"\n units/{civ}/support_female_citizen\n units/{native}/support_trader\n units/athen/infantry_spearman_b\n " + | |||||
"</Entities>" + | |||||
"</a:example>" + | |||||
"<optional>" + | |||||
"<element name='BatchTimeModifier' a:help='Modifier that influences the time benefit for batch training. Defaults to 1, which means no benefit.'>" + | |||||
"<ref name='nonNegativeDecimal'/>" + | |||||
"</element>" + | |||||
"</optional>" + | |||||
"<optional>" + | |||||
"<element name='Entities' a:help='Space-separated list of entity template names that this entity can train. The special string \"{civ}\" will be automatically replaced by the civ code of the entity's owner, while the string \"{native}\" will be automatically replaced by the entity's civ code.'>" + | |||||
"<attribute name='datatype'>" + | |||||
"<value>tokens</value>" + | |||||
"</attribute>" + | |||||
"<text/>" + | |||||
"</element>" + | |||||
"</optional>"; | |||||
/** | |||||
* This object represents a batch of entities being trained. | |||||
*/ | |||||
Trainer.prototype.Item = function() {}; | |||||
/** | |||||
* @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.prototype.Init = 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] === undefined ? 1 : 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 || 1) * 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); | |||||
nwtour: There was a situation that here this.population happens to be undefined
Crash on save game… | |||||
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) | |||||
{ | |||||
// 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() | |||||
{ | |||||
this.paused = true; | |||||
}; | |||||
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), | |||||
"timeRemaining": this.timeRemaining, | |||||
"paused": this.paused, | |||||
"metadata": this.metadata | |||||
}; | |||||
}; | |||||
Trainer.prototype.Item.prototype.Serialize = function(id) | |||||
{ | |||||
return { | |||||
"id": id, | |||||
"count": this.count, | |||||
"entities": this.entities, | |||||
"metadata": this.metadata, | |||||
"missingPopSpace": this.missingPopSpace, | |||||
"paused": this.paused, | |||||
"player": this.player, | |||||
"trainer": this.trainer, | |||||
"resource": this.resources, | |||||
"started": this.started, | |||||
"templateName": this.templateName, | |||||
"timeRemaining": this.timeRemaining, | |||||
"timeTotal": this.timeTotal, | |||||
}; | |||||
}; | |||||
Trainer.prototype.Item.prototype.Deserialize = function(data) | |||||
{ | |||||
this.Init(data.templateName, data.count, data.trainer, data.metadata); | |||||
this.entities = data.entities; | |||||
this.missingPopSpace = data.missingPopSpace; | |||||
this.paused = data.paused; | |||||
this.player = data.player; | |||||
this.trainer = data.trainer; | |||||
this.resources = data.resources; | |||||
this.started = data.started; | |||||
this.timeRemaining = data.timeRemaining; | |||||
this.timeTotal = data.timeTotal; | |||||
}; | |||||
Trainer.prototype.Init = function() | |||||
{ | |||||
this.nextID = 1; | |||||
this.queue = new Map(); | |||||
}; | |||||
Trainer.prototype.Serialize = function() | |||||
{ | |||||
const queue = []; | |||||
for (const [id, item] of this.queue) | |||||
queue.push(item.Serialize(id)); | |||||
return { | |||||
"entitiesMap": this.entitiesMap, | |||||
"nextID": this.nextID, | |||||
"queue": queue | |||||
}; | |||||
}; | |||||
Trainer.prototype.Deserialize = function(data) | |||||
{ | |||||
this.Init(); | |||||
this.entitiesMap = data.entitiesMap; | |||||
this.nextID = data.nextID; | |||||
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(); | |||||
if (!this.template.Entities) | |||||
nwtourUnsubmitted Not Done Inline ActionsDuring the game once a message appeared: ERROR: JavaScript error: simulation/components/Trainer.js line 464 this.template is undefined Trainer.prototype.CalculateEntitiesMap@simulation/components/Trainer.js:464:6 Trainer.prototype.OnOwnershipChanged@simulation/components/Trainer.js:698:8 Capturable.prototype.RegisterCapturePointsChanged@simulation/components/Capturable.js:176:15 Capturable.prototype.TimerTick@simulation/components/Capturable.js:219:8 Timer.prototype.OnUpdate@simulation/components/Timer.js:139:44 ERROR: Script message handler OnOwnershipChanged failed nwtour: During the game once a message appeared:
```
ERROR: JavaScript error… | |||||
return; | |||||
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 = cmpPlayer?.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()); | |||||
}; | |||||
/* | |||||
* 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; | |||||
}; | |||||
/** | |||||
* @return {Object} - The multipliers to change the costs of any training activity with. | |||||
*/ | |||||
Trainer.prototype.GetTrainCostMultiplier = function() | |||||
{ | |||||
const trainCostMultiplier = {}; | |||||
for (const res in this.template.TrainCostMultiplier) | |||||
trainCostMultiplier[res] = ApplyValueModificationsToEntity( | |||||
"Trainer/TrainCostMultiplier/" + res, | |||||
+this.template.TrainCostMultiplier[res], | |||||
this.entity); | |||||
return 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(); | |||||
item.Init(templateName, count, this.entity, metadata); | |||||
const trainCostMultiplier = this.GetTrainCostMultiplier(); | |||||
const batchTimeMultiplier = this.GetBatchTime(count); | |||||
if (!item.Queue(trainCostMultiplier, batchTimeMultiplier)) | |||||
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 training. | |||||
*/ | |||||
Trainer.prototype.UnpauseBatch = function(id) | |||||
{ | |||||
this.queue.get(id).Unpause(); | |||||
}; | |||||
/** | |||||
* @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); |
Wildfire Games · Phabricator
There was a situation that here this.population happens to be undefined
Crash on save game:
ERROR: Cannot serialize NaN values.
ERROR: Script component Player of entity 6 failed to serialize: Serialize_InvalidScriptValue
Serializing:
({playerID:4, name:"\u0427\u0430\u043D\u0434\u0440\u0430\u0433\u0443\u043F\u0442\u0430 \u041C\u0430\u0443\u0440\u044C\u044F", civ:"maur", color:{r:0.9058823529411765, g:0.7843137254901961, b:0.0196078431372549, a:1}, diplomacyColor:(void 0), displayDiplomacyColor:false, popUsed:NaN, popBonuses:0, maxPop:100, trainingBlocked:false, resourceCount:{food:190, wood:175, stone:262, metal:339}, resourceGatherers:{food:0, wood:0, stone:0, metal:0}, tradingGoods:[{goods:"food", proba:0}, {goods:"wood", proba:0}, {goods:"stone", proba:70}, {goods:"metal", proba:30}], team:1, teamsLocked:false, state:"active", diplomacy:[-1, -1, 1, 1, 1, 1], sharedDropsites:false, formations:["special/formations/null", "special/formations/box", "special/formations/column_closed", "special/formations/line_closed", "special/formations/column_open", "special/formations/line_open", "special/formations/flank", "special/formations/battle_line", "special/formations/skirmish", "special/formations/wedge"], startCam:(void 0), controlAllUnits:false, isAI:true, cheatsEnabled:true, panelEntities:[], resourceNames:{food:"Food", wood:"Wood", stone:"Stone", metal:"Metal"}, disabledTemplates:{}, disabledTechnologies:{}, spyCostMultiplier:1, barterEntities:[], barterMultiplier:{buy:{food:"1.0", metal:"1.0", stone:"1.0", wood:"1.0"}, sell:{food:"1.0", metal:"1.0", stone:"1.0", wood:"1.0"}}})
terminate called after throwing an instance of 'PSERROR_Serialize_InvalidScriptValue'