LCOV - code coverage report
Current view: top level - simulation/components - Trainer.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 272 314 86.6 %
Date: 2023-04-02 12:52:40 Functions: 33 39 84.6 %

          Line data    Source code
       1             : function Trainer() {}
       2             : 
       3           1 : Trainer.prototype.Schema =
       4             :         "<a:help>Allows the entity to train new units.</a:help>" +
       5             :         "<a:example>" +
       6             :                 "<BatchTimeModifier>0.7</BatchTimeModifier>" +
       7             :                 "<Entities datatype='tokens'>" +
       8             :                         "\n    units/{civ}/support_female_citizen\n    units/{native}/support_trader\n    units/athen/infantry_spearman_b\n  " +
       9             :                 "</Entities>" +
      10             :         "</a:example>" +
      11             :         "<optional>" +
      12             :                 "<element name='BatchTimeModifier' a:help='Modifier that influences the time benefit for batch training. Defaults to 1, which means no benefit.'>" +
      13             :                         "<ref name='nonNegativeDecimal'/>" +
      14             :                 "</element>" +
      15             :         "</optional>" +
      16             :         "<optional>" +
      17             :                 "<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&apos;s owner, while the string \"{native}\" will be automatically replaced by the entity&apos;s civ code.'>" +
      18             :                         "<attribute name='datatype'>" +
      19             :                                 "<value>tokens</value>" +
      20             :                         "</attribute>" +
      21             :                         "<text/>" +
      22             :                 "</element>" +
      23             :         "</optional>";
      24             : 
      25             : /**
      26             :  * This object represents a batch of entities being trained.
      27             :  * @param {string} templateName - The name of the template we ought to train.
      28             :  * @param {number} count - The size of the batch to train.
      29             :  * @param {number} trainer - The entity ID of our trainer.
      30             :  * @param {string} metadata - Optionally any metadata to attach to us.
      31             :  */
      32           1 : Trainer.prototype.Item = function(templateName, count, trainer, metadata)
      33             : {
      34           8 :         this.count = count;
      35           8 :         this.templateName = templateName;
      36           8 :         this.trainer = trainer;
      37           8 :         this.metadata = metadata;
      38             : };
      39             : 
      40             : /**
      41             :  * Prepare for the queue.
      42             :  * @param {Object} trainCostMultiplier - The multipliers to use when calculating costs.
      43             :  * @param {number} batchTimeMultiplier - The factor to use when training this batches.
      44             :  *
      45             :  * @return {boolean} - Whether the item was successfully initiated.
      46             :  */
      47           1 : Trainer.prototype.Item.prototype.Queue = function(trainCostMultiplier, batchTimeMultiplier)
      48             : {
      49           6 :         if (!Number.isInteger(this.count) || this.count <= 0)
      50             :         {
      51           0 :                 error("Invalid batch count " + this.count + ".");
      52           0 :                 return false;
      53             :         }
      54           6 :         const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
      55           6 :         const template = cmpTemplateManager.GetTemplate(this.templateName);
      56           6 :         if (!template)
      57           0 :                 return false;
      58             : 
      59           6 :         const cmpPlayer = QueryOwnerInterface(this.trainer);
      60           6 :         if (!cmpPlayer)
      61           0 :                 return false;
      62           6 :         this.player = cmpPlayer.GetPlayerID();
      63             : 
      64           6 :         this.resources = {};
      65           6 :         const totalResources = {};
      66             : 
      67           6 :         for (const res in template.Cost.Resources)
      68             :         {
      69           6 :                 this.resources[res] = trainCostMultiplier[res] *
      70             :                         ApplyValueModificationsToTemplate(
      71             :                                 "Cost/Resources/" + res,
      72             :                                 +template.Cost.Resources[res],
      73             :                                 this.player,
      74             :                                 template);
      75             : 
      76           6 :                 totalResources[res] = Math.floor(this.count * this.resources[res]);
      77             :         }
      78             :         // TrySubtractResources should report error to player (they ran out of resources).
      79           6 :         if (!cmpPlayer.TrySubtractResources(totalResources))
      80           0 :                 return false;
      81             : 
      82           6 :         this.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, this.player, template);
      83             : 
      84           6 :         if (template.TrainingRestrictions)
      85             :         {
      86           6 :                 const unitCategory = template.TrainingRestrictions.Category;
      87           6 :                 const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
      88           6 :                 if (cmpPlayerEntityLimits)
      89             :                 {
      90           6 :                         if (!cmpPlayerEntityLimits.AllowedToTrain(unitCategory, this.count, this.templateName, template.TrainingRestrictions.MatchLimit))
      91             :                         // Already warned, return.
      92             :                         {
      93           1 :                                 cmpPlayer.RefundResources(totalResources);
      94           1 :                                 return false;
      95             :                         }
      96             :                         // ToDo: Should warn here v and return?
      97           5 :                         cmpPlayerEntityLimits.ChangeCount(unitCategory, this.count);
      98           5 :                         if (template.TrainingRestrictions.MatchLimit)
      99           5 :                                 cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, this.count);
     100             :                 }
     101             :         }
     102             : 
     103           5 :         const buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, this.player, template);
     104             : 
     105           5 :         const time = batchTimeMultiplier * trainCostMultiplier.time * buildTime * 1000;
     106           5 :         this.timeRemaining = time;
     107           5 :         this.timeTotal = time;
     108             : 
     109           5 :         const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
     110           5 :         cmpTrigger.CallEvent("OnTrainingQueued", {
     111             :                 "playerid": this.player,
     112             :                 "unitTemplate": this.templateName,
     113             :                 "count": this.count,
     114             :                 "metadata": this.metadata,
     115             :                 "trainerEntity": this.trainer
     116             :         });
     117             : 
     118           5 :         return true;
     119             : };
     120             : 
     121             : /**
     122             :  * Destroy cached entities, refund resources and free (population) limits.
     123             :  */
     124           1 : Trainer.prototype.Item.prototype.Stop = function()
     125             : {
     126             :         // Destroy any cached entities (those which didn't spawn for some reason).
     127           3 :         if (this.entities?.length)
     128             :         {
     129           1 :                 for (const ent of this.entities)
     130           2 :                         Engine.DestroyEntity(ent);
     131             : 
     132           1 :                 delete this.entities;
     133             :         }
     134             : 
     135           3 :         const cmpPlayer = QueryPlayerIDInterface(this.player);
     136             : 
     137           3 :         const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
     138           3 :         const template = cmpTemplateManager.GetTemplate(this.templateName);
     139           3 :         if (template.TrainingRestrictions)
     140             :         {
     141           3 :                 const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
     142           3 :                 if (cmpPlayerEntityLimits)
     143           3 :                         cmpPlayerEntityLimits.ChangeCount(template.TrainingRestrictions.Category, -this.count);
     144           3 :                 if (template.TrainingRestrictions.MatchLimit)
     145           3 :                         cmpPlayerEntityLimits.ChangeMatchCount(this.templateName, -this.count);
     146             :         }
     147             : 
     148           3 :         if (cmpPlayer)
     149             :         {
     150           3 :                 if (this.started)
     151           1 :                         cmpPlayer.UnReservePopulationSlots(this.population * this.count);
     152             : 
     153           3 :                 const totalCosts = {};
     154           3 :                 for (const resource in this.resources)
     155           3 :                         totalCosts[resource] = Math.floor(this.count * this.resources[resource]);
     156             : 
     157           3 :                 cmpPlayer.RefundResources(totalCosts);
     158           3 :                 cmpPlayer.UnBlockTraining();
     159             :         }
     160             : 
     161           3 :         delete this.resources;
     162             : };
     163             : 
     164             : /**
     165             :  * This starts the item, reserving population.
     166             :  * @return {boolean} - Whether the item was started successfully.
     167             :  */
     168           1 : Trainer.prototype.Item.prototype.Start = function()
     169             : {
     170           2 :         const cmpPlayer = QueryPlayerIDInterface(this.player);
     171           2 :         if (!cmpPlayer)
     172           0 :                 return false;
     173             : 
     174           2 :         const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(this.templateName);
     175           2 :         this.population = ApplyValueModificationsToTemplate(
     176             :                 "Cost/Population",
     177             :                 +template.Cost.Population,
     178             :                 this.player,
     179             :                 template);
     180             : 
     181           2 :         this.missingPopSpace = cmpPlayer.TryReservePopulationSlots(this.population * this.count);
     182           2 :         if (this.missingPopSpace)
     183             :         {
     184           0 :                 cmpPlayer.BlockTraining();
     185           0 :                 return false;
     186             :         }
     187           2 :         cmpPlayer.UnBlockTraining();
     188             : 
     189           2 :         Engine.PostMessage(this.trainer, MT_TrainingStarted, { "entity": this.trainer });
     190             : 
     191           2 :         this.started = true;
     192           2 :         return true;
     193             : };
     194             : 
     195           1 : Trainer.prototype.Item.prototype.Finish = function()
     196             : {
     197           2 :         this.Spawn();
     198           2 :         if (!this.count)
     199           1 :                 this.finished = true;
     200             : };
     201             : 
     202             : /**
     203             :  * @return {boolean} -
     204             :  */
     205           1 : Trainer.prototype.Item.prototype.IsFinished = function()
     206             : {
     207           3 :         return !!this.finished;
     208             : };
     209             : 
     210             : /*
     211             :  * This function creates the entities and places them in world if possible
     212             :  * (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
     213             :  */
     214           1 : Trainer.prototype.Item.prototype.Spawn = function()
     215             : {
     216           2 :         const createdEnts = [];
     217           2 :         const spawnedEnts = [];
     218             : 
     219             :         // We need entities to test spawning, but we don't want to waste resources,
     220             :         // so only create them once and use as needed.
     221           2 :         if (!this.entities)
     222             :         {
     223           2 :                 this.entities = [];
     224           2 :                 for (let i = 0; i < this.count; ++i)
     225           3 :                         this.entities.push(Engine.AddEntity(this.templateName));
     226             :         }
     227             : 
     228             :         let autoGarrison;
     229           2 :         const cmpRallyPoint = Engine.QueryInterface(this.trainer, IID_RallyPoint);
     230           2 :         if (cmpRallyPoint)
     231             :         {
     232           0 :                 const data = cmpRallyPoint.GetData()[0];
     233           0 :                 if (data?.target && data.target == this.trainer && data.command == "garrison")
     234           0 :                         autoGarrison = true;
     235             :         }
     236             : 
     237           2 :         const cmpFootprint = Engine.QueryInterface(this.trainer, IID_Footprint);
     238           2 :         const cmpPosition = Engine.QueryInterface(this.trainer, IID_Position);
     239           2 :         const positionTrainer = cmpPosition && cmpPosition.GetPosition();
     240             : 
     241           2 :         const cmpPlayerEntityLimits = QueryPlayerIDInterface(this.player, IID_EntityLimits);
     242           2 :         const cmpPlayerStatisticsTracker = QueryPlayerIDInterface(this.player, IID_StatisticsTracker);
     243           2 :         while (this.entities.length)
     244             :         {
     245           2 :                 const ent = this.entities[0];
     246           2 :                 const cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
     247           2 :                 let garrisoned = false;
     248             : 
     249           2 :                 if (autoGarrison)
     250             :                 {
     251           0 :                         const cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
     252           0 :                         if (cmpGarrisonable)
     253             :                         {
     254             :                                 // Temporary owner affectation needed for GarrisonHolder checks.
     255           0 :                                 cmpNewOwnership.SetOwnerQuiet(this.player);
     256           0 :                                 garrisoned = cmpGarrisonable.Garrison(this.trainer);
     257           0 :                                 cmpNewOwnership.SetOwnerQuiet(INVALID_PLAYER);
     258             :                         }
     259             :                 }
     260             : 
     261           2 :                 if (!garrisoned)
     262             :                 {
     263           2 :                         const pos = cmpFootprint.PickSpawnPoint(ent);
     264           2 :                         if (pos.y < 0)
     265           1 :                                 break;
     266             : 
     267           1 :                         const cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
     268           1 :                         cmpNewPosition.JumpTo(pos.x, pos.z);
     269             : 
     270           1 :                         if (positionTrainer)
     271           0 :                                 cmpNewPosition.SetYRotation(positionTrainer.horizAngleTo(pos));
     272             : 
     273           1 :                         spawnedEnts.push(ent);
     274             :                 }
     275             : 
     276             :                 // Decrement entity count in the EntityLimits component
     277             :                 // since it will be increased by EntityLimits.OnGlobalOwnershipChanged,
     278             :                 // i.e. we replace a 'trained' entity by 'alive' one.
     279             :                 // Must be done after spawn check so EntityLimits decrements only if unit spawns.
     280           1 :                 if (cmpPlayerEntityLimits)
     281             :                 {
     282           1 :                         const cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
     283           1 :                         if (cmpTrainingRestrictions)
     284           1 :                                 cmpPlayerEntityLimits.ChangeCount(cmpTrainingRestrictions.GetCategory(), -1);
     285             :                 }
     286           1 :                 cmpNewOwnership.SetOwner(this.player);
     287             : 
     288           1 :                 if (cmpPlayerStatisticsTracker)
     289           0 :                         cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
     290             : 
     291           1 :                 this.count--;
     292           1 :                 this.entities.shift();
     293           1 :                 createdEnts.push(ent);
     294             :         }
     295             : 
     296           2 :         if (spawnedEnts.length && cmpRallyPoint)
     297           0 :                 for (const com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts))
     298           0 :                         ProcessCommand(this.player, com);
     299             : 
     300           2 :         const cmpPlayer = QueryOwnerInterface(this.trainer);
     301           2 :         if (createdEnts.length)
     302             :         {
     303           1 :                 if (this.population)
     304           1 :                         cmpPlayer.UnReservePopulationSlots(this.population * createdEnts.length);
     305             :                 // Play a sound, but only for the first in the batch (to avoid nasty phasing effects).
     306           1 :                 PlaySound("trained", createdEnts[0]);
     307           1 :                 Engine.PostMessage(this.trainer, MT_TrainingFinished, {
     308             :                     "entities": createdEnts,
     309             :                     "owner": this.player,
     310             :                     "metadata": this.metadata
     311             :                 });
     312             :         }
     313           2 :         if (this.count)
     314             :         {
     315           1 :                 cmpPlayer.BlockTraining();
     316             : 
     317           1 :                 if (!this.spawnNotified)
     318             :                 {
     319           1 :                         Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).PushNotification({
     320             :                             "players": [cmpPlayer.GetPlayerID()],
     321             :                             "message": markForTranslation("Can't find free space to spawn trained units."),
     322             :                             "translateMessage": true
     323             :                         });
     324           1 :                         this.spawnNotified = true;
     325             :                 }
     326             :         }
     327             :         else
     328             :         {
     329           1 :                 cmpPlayer.UnBlockTraining();
     330           1 :                 delete this.spawnNotified;
     331             :         }
     332             : };
     333             : 
     334             : /**
     335             :  * @param {number} allocatedTime - The time allocated to this item.
     336             :  * @return {number} - The time used for this item.
     337             :  */
     338           1 : Trainer.prototype.Item.prototype.Progress = function(allocatedTime)
     339             : {
     340           3 :         if (this.paused)
     341           0 :                 this.Unpause();
     342             :         // We couldn't start this timeout, try again later.
     343           3 :         if (!this.started && !this.Start())
     344           0 :                 return allocatedTime;
     345             : 
     346           3 :         if (this.timeRemaining > allocatedTime)
     347             :         {
     348           1 :                 this.timeRemaining -= allocatedTime;
     349           1 :                 return allocatedTime;
     350             :         }
     351           2 :         this.Finish();
     352           2 :         return this.timeRemaining;
     353             : };
     354             : 
     355           1 : Trainer.prototype.Item.prototype.Pause = function()
     356             : {
     357           0 :         if (this.started)
     358           0 :                 this.paused = true;
     359           0 :         else if (this.missingPopSpace)
     360             :         {
     361           0 :                 delete this.missingPopSpace;
     362           0 :                 QueryOwnerInterface(this.trainer)?.UnBlockTraining();
     363             :         }
     364             : };
     365             : 
     366           1 : Trainer.prototype.Item.prototype.Unpause = function()
     367             : {
     368           0 :         delete this.paused;
     369             : };
     370             : 
     371             : /**
     372             :  * @return {Object} - Some basic information of this batch.
     373             :  */
     374           1 : Trainer.prototype.Item.prototype.GetBasicInfo = function()
     375             : {
     376           5 :         return {
     377             :                 "unitTemplate": this.templateName,
     378             :                 "count": this.count,
     379             :                 "neededSlots": this.missingPopSpace,
     380             :                 "progress": 1 - (this.timeRemaining / (this.timeTotal || 1)),
     381             :                 "timeRemaining": this.timeRemaining,
     382             :                 "paused": this.paused,
     383             :                 "metadata": this.metadata
     384             :         };
     385             : };
     386             : 
     387           1 : Trainer.prototype.Item.prototype.SerializableAttributes = [
     388             :         "count",
     389             :         "entities",
     390             :         "metadata",
     391             :         "missingPopSpace",
     392             :         "paused",
     393             :         "player",
     394             :         "population",
     395             :         "trainer",
     396             :         "resources",
     397             :         "started",
     398             :         "templateName",
     399             :         "timeRemaining",
     400             :         "timeTotal"
     401             : ];
     402             : 
     403           1 : Trainer.prototype.Item.prototype.Serialize = function(id)
     404             : {
     405           2 :         const result = {
     406             :                 "id": id
     407             :         };
     408           2 :         for (const att of this.SerializableAttributes)
     409          26 :                 if (this.hasOwnProperty(att))
     410          23 :                         result[att] = this[att];
     411           2 :         return result;
     412             : };
     413             : 
     414           1 : Trainer.prototype.Item.prototype.Deserialize = function(data)
     415             : {
     416           2 :         for (const att of this.SerializableAttributes)
     417          26 :                 if (att in data)
     418          23 :                         this[att] = data[att];
     419             : };
     420             : 
     421           1 : Trainer.prototype.Init = function()
     422             : {
     423           4 :         this.nextID = 1;
     424           4 :         this.queue = new Map();
     425           4 :         this.trainCostMultiplier = {};
     426             : };
     427             : 
     428           1 : Trainer.prototype.SerializableAttributes = [
     429             :         "entitiesMap",
     430             :         "nextID",
     431             :         "trainCostMultiplier"
     432             : ];
     433             : 
     434           1 : Trainer.prototype.Serialize = function()
     435             : {
     436           2 :         const queue = [];
     437           2 :         for (const [id, item] of this.queue)
     438           2 :                 queue.push(item.Serialize(id));
     439             : 
     440           2 :         const result = {
     441             :                 "queue": queue
     442             :         };
     443           2 :         for (const att of this.SerializableAttributes)
     444           6 :                 if (this.hasOwnProperty(att))
     445           6 :                         result[att] = this[att];
     446             : 
     447           2 :         return result;
     448             : };
     449             : 
     450           1 : Trainer.prototype.Deserialize = function(data)
     451             : {
     452           2 :         for (const att of this.SerializableAttributes)
     453           6 :                 if (att in data)
     454           6 :                         this[att] = data[att];
     455             : 
     456           2 :         this.queue = new Map();
     457           2 :         for (const item of data.queue)
     458             :         {
     459           2 :                 const newItem = new this.Item();
     460           2 :                 newItem.Deserialize(item);
     461           2 :                 this.queue.set(item.id, newItem);
     462             :         }
     463             : };
     464             : 
     465             : /*
     466             :  * Returns list of entities that can be trained by this entity.
     467             :  */
     468           1 : Trainer.prototype.GetEntitiesList = function()
     469             : {
     470           8 :         return Array.from(this.entitiesMap.values());
     471             : };
     472             : 
     473             : /**
     474             :  * Calculate the new list of producible entities
     475             :  * and update any entities currently being produced.
     476             :  */
     477           1 : Trainer.prototype.CalculateEntitiesMap = function()
     478             : {
     479             :         // Don't reset the map, it's used below to update entities.
     480           8 :         if (!this.entitiesMap)
     481           2 :                 this.entitiesMap = new Map();
     482             : 
     483           8 :         const string = this.template?.Entities?._string || "";
     484             :         // Tokens can be added -> process an empty list to get them.
     485           8 :         let addedTokens = ApplyValueModificationsToEntity("Trainer/Entities/_string", "", this.entity);
     486           8 :         if (!addedTokens && !string)
     487           0 :                 return;
     488             : 
     489           8 :         addedTokens = addedTokens == "" ? [] : addedTokens.split(/\s+/);
     490             : 
     491           8 :         const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
     492           8 :         const cmpPlayer = QueryOwnerInterface(this.entity);
     493             : 
     494           8 :         const disabledEntities = cmpPlayer ? cmpPlayer.GetDisabledTemplates() : {};
     495             : 
     496             :         /**
     497             :          * Process tokens:
     498             :          * - process token modifiers (this is a bit tricky).
     499             :          * - replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID
     500             :          * - remove disabled entities
     501             :          * - upgrade templates where necessary
     502             :          * This also updates currently queued production (it's more convenient to do it here).
     503             :          */
     504             : 
     505           8 :         const removeAllQueuedTemplate = (token) => {
     506           6 :                 const queue = clone(this.queue);
     507           6 :                 const template = this.entitiesMap.get(token);
     508           6 :                 for (const [id, item] of queue)
     509           2 :                         if (item.templateName == template)
     510           1 :                                 this.StopBatch(id);
     511             :         };
     512             : 
     513             :         // ToDo: Notice this doesn't account for entity limits changing due to the template change.
     514           8 :         const updateAllQueuedTemplate = (token, updateTo) => {
     515          18 :                 const template = this.entitiesMap.get(token);
     516          18 :                 for (const [id, item] of this.queue)
     517           4 :                         if (item.templateName === template)
     518           1 :                                 item.templateName = updateTo;
     519             :         };
     520             : 
     521           8 :         const toks = string.split(/\s+/);
     522           8 :         for (const tok of addedTokens)
     523           2 :                 toks.push(tok);
     524             : 
     525           8 :         const nativeCiv = Engine.QueryInterface(this.entity, IID_Identity)?.GetCiv();
     526           8 :         const playerCiv = QueryOwnerInterface(this.entity, IID_Identity)?.GetCiv();
     527             : 
     528           8 :         const addedDict = addedTokens.reduce((out, token) => { out[token] = true; return out; }, {});
     529           8 :         this.entitiesMap = toks.reduce((entMap, token) => {
     530          24 :                 const rawToken = token;
     531          24 :                 if (!(token in addedDict))
     532             :                 {
     533             :                         // This is a bit wasteful but I can't think of a simpler/better way.
     534             :                         // The list of token is unlikely to be a performance bottleneck anyways.
     535          22 :                         token = ApplyValueModificationsToEntity("Trainer/Entities/_string", token, this.entity);
     536          22 :                         token = token.split(/\s+/);
     537          22 :                         if (token.every(tok => addedTokens.indexOf(tok) !== -1))
     538             :                         {
     539           2 :                                 removeAllQueuedTemplate(rawToken);
     540           2 :                                 return entMap;
     541             :                         }
     542          20 :                         token = token[0];
     543             :                 }
     544             :                 // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID.
     545          22 :                 if (nativeCiv)
     546          22 :                         token = token.replace(/\{native\}/g, nativeCiv);
     547          22 :                 if (playerCiv)
     548          22 :                         token = token.replace(/\{civ\}/g, playerCiv);
     549             : 
     550             :                 // Filter out disabled and invalid entities.
     551          22 :                 if (disabledEntities[token] || !cmpTemplateManager.TemplateExists(token))
     552             :                 {
     553           4 :                         removeAllQueuedTemplate(rawToken);
     554           4 :                         return entMap;
     555             :                 }
     556             : 
     557          18 :                 token = this.GetUpgradedTemplate(token);
     558          18 :                 entMap.set(rawToken, token);
     559          18 :                 updateAllQueuedTemplate(rawToken, token);
     560          18 :                 return entMap;
     561             :         }, new Map());
     562             : 
     563           8 :         this.CalculateTrainCostMultiplier();
     564             : };
     565             : 
     566             : /*
     567             :  * Returns the upgraded template name if necessary.
     568             :  */
     569           1 : Trainer.prototype.GetUpgradedTemplate = function(templateName)
     570             : {
     571           4 :         const cmpPlayer = QueryOwnerInterface(this.entity);
     572           4 :         if (!cmpPlayer)
     573           0 :                 return templateName;
     574             : 
     575           4 :         const cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
     576           4 :         let template = cmpTemplateManager.GetTemplate(templateName);
     577           4 :         while (template && template.Promotion !== undefined)
     578             :         {
     579           0 :                 const requiredXp = ApplyValueModificationsToTemplate(
     580             :                     "Promotion/RequiredXp",
     581             :                     +template.Promotion.RequiredXp,
     582             :                     cmpPlayer.GetPlayerID(),
     583             :                     template);
     584           0 :                 if (requiredXp > 0)
     585           0 :                         break;
     586           0 :                 templateName = template.Promotion.Entity;
     587           0 :                 template = cmpTemplateManager.GetTemplate(templateName);
     588             :         }
     589           4 :         return templateName;
     590             : };
     591             : 
     592           1 : Trainer.prototype.CalculateTrainCostMultiplier = function()
     593             : {
     594           8 :         for (const res of Resources.GetCodes().concat(["time"]))
     595          16 :                 this.trainCostMultiplier[res] = ApplyValueModificationsToEntity(
     596             :                     "Trainer/TrainCostMultiplier/" + res,
     597             :                     +(this.template?.TrainCostMultiplier?.[res] || 1),
     598             :                     this.entity);
     599             : };
     600             : 
     601             : /**
     602             :  * @return {Object} - The multipliers to change the costs of any training activity with.
     603             :  */
     604           1 : Trainer.prototype.TrainCostMultiplier = function()
     605             : {
     606           6 :         return this.trainCostMultiplier;
     607             : };
     608             : 
     609             : /*
     610             :  * Returns batch build time.
     611             :  */
     612           1 : Trainer.prototype.GetBatchTime = function(batchSize)
     613             : {
     614             :         // TODO: work out what equation we should use here.
     615           6 :         return Math.pow(batchSize, ApplyValueModificationsToEntity(
     616             :             "Trainer/BatchTimeModifier",
     617             :             +(this.template?.BatchTimeModifier || 1),
     618             :             this.entity));
     619             : };
     620             : 
     621             : /**
     622             :  * @param {string} templateName - The template name to check.
     623             :  * @return {boolean} - Whether we can train this template.
     624             :  */
     625           1 : Trainer.prototype.CanTrain = function(templateName)
     626             : {
     627           0 :         return this.GetEntitiesList().includes(templateName);
     628             : };
     629             : 
     630             : /**
     631             :  * @param {string} templateName - The entity to queue.
     632             :  * @param {number} count - The batch size.
     633             :  * @param {string} metadata - Any metadata attached to the item.
     634             :  *
     635             :  * @return {number} - The ID of the item. -1 if the item could not be queued.
     636             :  */
     637           1 : Trainer.prototype.QueueBatch = function(templateName, count, metadata)
     638             : {
     639           6 :         const item = new this.Item(templateName, count, this.entity, metadata);
     640           6 :         if (!item.Queue(this.TrainCostMultiplier(), this.GetBatchTime(count)))
     641           1 :                 return -1;
     642             : 
     643           5 :         const id = this.nextID++;
     644           5 :         this.queue.set(id, item);
     645           5 :         return id;
     646             : };
     647             : 
     648             : /**
     649             :  * @param {number} id - The ID of the batch being trained here we need to stop.
     650             :  */
     651           1 : Trainer.prototype.StopBatch = function(id)
     652             : {
     653           3 :         this.queue.get(id).Stop();
     654           3 :         this.queue.delete(id);
     655             : };
     656             : 
     657             : /**
     658             :  * @param {number} id - The ID of the training.
     659             :  */
     660           1 : Trainer.prototype.PauseBatch = function(id)
     661             : {
     662           0 :         this.queue.get(id).Pause();
     663             : };
     664             : 
     665             : /**
     666             :  * @param {number} id - The ID of the batch to check.
     667             :  * @return {boolean} - Whether we are currently training the batch.
     668             :  */
     669           1 : Trainer.prototype.HasBatch = function(id)
     670             : {
     671           2 :         return this.queue.has(id);
     672             : };
     673             : 
     674             : /**
     675             :  * @parameter {number} id - The id of the training.
     676             :  * @return {Object} - Some basic information about the training.
     677             :  */
     678           1 : Trainer.prototype.GetBatch = function(id)
     679             : {
     680           6 :         const item = this.queue.get(id);
     681           6 :         return item?.GetBasicInfo();
     682             : };
     683             : 
     684             : /**
     685             :  * @param {number} id - The ID of the item we spent time on.
     686             :  * @param {number} allocatedTime - The time we spent on the given item.
     687             :  * @return {number} - The time we've actually used.
     688             :  */
     689           1 : Trainer.prototype.Progress = function(id, allocatedTime)
     690             : {
     691           3 :         const item = this.queue.get(id);
     692           3 :         const usedTime = item.Progress(allocatedTime);
     693           3 :         if (item.IsFinished())
     694           1 :                 this.queue.delete(id);
     695           3 :         return usedTime;
     696             : };
     697             : 
     698           1 : Trainer.prototype.OnOwnershipChanged = function(msg)
     699             : {
     700           0 :         if (msg.to != INVALID_PLAYER)
     701           0 :                 this.CalculateEntitiesMap();
     702             : };
     703             : 
     704           1 : Trainer.prototype.OnValueModification = function(msg)
     705             : {
     706             :         // If the promotion requirements of units is changed,
     707             :         // update the entities list so that automatically promoted units are shown
     708             :         // appropriately in the list.
     709           2 :         if (msg.component != "Promotion" && (msg.component != "Trainer" ||
     710           2 :                 !msg.valueNames.some(val => val.startsWith("Trainer/Entities/"))))
     711           0 :                 return;
     712             : 
     713           2 :         if (msg.entities.indexOf(this.entity) === -1)
     714           0 :                 return;
     715             : 
     716             :         // This also updates the queued production if necessary.
     717           2 :         this.CalculateEntitiesMap();
     718             : 
     719             :         // Inform the GUI that it'll need to recompute the selection panel.
     720             :         // TODO: it would be better to only send the message if something actually changing
     721             :         // for the current training queue.
     722           2 :         const cmpPlayer = QueryOwnerInterface(this.entity);
     723           2 :         if (cmpPlayer)
     724           2 :                 Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).SetSelectionDirty(cmpPlayer.GetPlayerID());
     725             : };
     726             : 
     727           1 : Trainer.prototype.OnDisabledTemplatesChanged = function(msg)
     728             : {
     729           0 :         this.CalculateEntitiesMap();
     730             : };
     731             : 
     732           1 : Engine.RegisterComponentType(IID_Trainer, "Trainer", Trainer);

Generated by: LCOV version 1.14