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's owner, while the string \"{native}\" will be automatically replaced by the entity'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);
|