function ProductionQueue() {} ProductionQueue.prototype.Schema = "Helps the building to train new units and research technologies." + ""; ProductionQueue.prototype.ProgressInterval = 1000; ProductionQueue.prototype.MaxQueueSize = 16; /** * This object represents an item in the queue. * * @param {number} producer - The entity ID of our producer. * @param {string} metadata - Optionally any metadata attached to us. */ ProductionQueue.prototype.Item = function(producer, metadata) { this.producer = producer; this.metadata = metadata; }; /** * @param {string} type - The type of queue to use. * @param {string} templateName - The template to queue. * @param {number} count - The amount of template to queue. Only applicable for type == "unit". * * @return {boolean} - Whether the item could be queued. */ ProductionQueue.prototype.Item.prototype.Queue = function(type, templateName, count) { if (type == "unit") return this.QueueEntity(templateName, count); if (type == "technology") return this.QueueTechnology(templateName); warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue (entity: " + this.producer + ")."); return false; }; /** * @param {string} templateName - The name of the entity to queue. * @param {number} count - The number of entities that should be produced. * @return {boolean} - Whether the batch was successfully created. */ ProductionQueue.prototype.Item.prototype.QueueEntity = function(templateName, count) { const cmpTrainer = Engine.QueryInterface(this.producer, IID_Trainer); if (!cmpTrainer) return false; this.entity = cmpTrainer.QueueBatch(templateName, count, this.metadata); if (this.entity == -1) return false; this.originalItem = { "templateName": templateName, "count": count, "metadata": this.metadata }; return true; }; /** * @param {string} templateName - The name of the technology to queue. * @return {boolean} - Whether the technology was successfully queued. */ ProductionQueue.prototype.Item.prototype.QueueTechnology = function(templateName) { const cmpResearcher = Engine.QueryInterface(this.producer, IID_Researcher); if (!cmpResearcher) return false; this.technology = cmpResearcher.QueueTechnology(templateName, this.metadata); return this.technology != -1; }; /** * @param {number} id - The id this item needs to get. */ ProductionQueue.prototype.Item.prototype.SetID = function(id) { this.id = id; }; ProductionQueue.prototype.Item.prototype.Stop = function() { if (this.entity > 0) Engine.QueryInterface(this.producer, IID_Trainer)?.StopBatch(this.entity); if (this.technology > 0) Engine.QueryInterface(this.producer, IID_Researcher)?.StopResearching(this.technology); }; /** * Called when the first work is performed. */ ProductionQueue.prototype.Item.prototype.Start = function() { this.started = true; }; /** * @return {boolean} - Whether there is work done on the item. */ ProductionQueue.prototype.Item.prototype.IsStarted = function() { return !!this.started; }; /** * @return {boolean} - Whether this item is finished. */ ProductionQueue.prototype.Item.prototype.IsFinished = function() { return !!this.finished; }; /** * @param {number} allocatedTime - The time allocated to this item. * @return {number} - The time used for this item. */ ProductionQueue.prototype.Item.prototype.Progress = function(allocatedTime) { if (this.paused) this.Unpause(); if (this.entity) { const cmpTrainer = Engine.QueryInterface(this.producer, IID_Trainer); allocatedTime -= cmpTrainer.Progress(this.entity, allocatedTime); if (!cmpTrainer.HasBatch(this.entity)) delete this.entity; } if (this.technology) { const cmpResearcher = Engine.QueryInterface(this.producer, IID_Researcher); allocatedTime -= cmpResearcher.Progress(this.technology, allocatedTime); if (!cmpResearcher.HasItem(this.technology)) delete this.technology; } if (!this.entity && !this.technology) this.finished = true; return allocatedTime; }; ProductionQueue.prototype.Item.prototype.Pause = function() { this.paused = true; if (this.entity) Engine.QueryInterface(this.producer, IID_Trainer).PauseBatch(this.entity); if (this.technology) Engine.QueryInterface(this.producer, IID_Researcher).PauseTechnology(this.technology); }; ProductionQueue.prototype.Item.prototype.Unpause = function() { delete this.paused; }; /** * @return {boolean} - Whether the item is currently paused. */ ProductionQueue.prototype.Item.prototype.IsPaused = function() { return !!this.paused; }; /** * @return {Object} - Some basic information of this item. */ ProductionQueue.prototype.Item.prototype.GetBasicInfo = function() { let result; if (this.technology) result = Engine.QueryInterface(this.producer, IID_Researcher).GetResearchingTechnology(this.technology); else if (this.entity) result = Engine.QueryInterface(this.producer, IID_Trainer).GetBatch(this.entity); result.id = this.id; result.paused = this.paused; return result; }; /** * @return {Object} - The originally queued item. */ ProductionQueue.prototype.Item.prototype.OriginalItem = function() { return this.originalItem; }; ProductionQueue.prototype.Item.prototype.SerializableAttributes = [ "entity", "id", "metadata", "originalItem", "paused", "producer", "started", "technology" ]; ProductionQueue.prototype.Item.prototype.Serialize = function() { const result = {}; for (const att of this.SerializableAttributes) if (this.hasOwnProperty(att)) result[att] = this[att]; return result; }; ProductionQueue.prototype.Item.prototype.Deserialize = function(data) { for (const att of this.SerializableAttributes) if (att in data) this[att] = data[att]; }; ProductionQueue.prototype.Init = function() { this.nextID = 1; this.queue = []; }; ProductionQueue.prototype.SerializableAttributes = [ "autoqueuing", "nextID", "paused", "timer" ]; ProductionQueue.prototype.Serialize = function() { const result = { "queue": [] }; for (const item of this.queue) result.queue.push(item.Serialize()); for (const att of this.SerializableAttributes) if (this.hasOwnProperty(att)) result[att] = this[att]; return result; }; ProductionQueue.prototype.Deserialize = function(data) { for (const att of this.SerializableAttributes) if (att in data) this[att] = data[att]; this.queue = []; for (const item of data.queue) { const newItem = new this.Item(); newItem.Deserialize(item); this.queue.push(newItem); } }; /** * @return {boolean} - Whether we are automatically queuing items. */ ProductionQueue.prototype.IsAutoQueueing = function() { return !!this.autoqueuing; }; /** * Turn on Auto-Queue. */ ProductionQueue.prototype.EnableAutoQueue = function() { this.autoqueuing = true; }; /** * Turn off Auto-Queue. */ ProductionQueue.prototype.DisableAutoQueue = function() { delete this.autoqueuing; }; /* * Adds a new batch of identical units to train or a technology to research to the production queue. * @param {string} templateName - The template to start production on. * @param {string} type - The type of production (i.e. "unit" or "technology"). * @param {number} count - The amount of units to be produced. Ignored for a tech. * @param {any} metadata - Optionally any metadata to be attached to the item. * @param {boolean} pushFront - Whether to push the item to the front of the queue and pause any item(s) currently in progress. * * @return {boolean} - Whether the addition of the item has succeeded. */ ProductionQueue.prototype.AddItem = function(templateName, type, count, metadata, pushFront = false) { // TODO: there should be a way for the GUI to determine whether it's going // to be possible to add a batch (based on resource costs and length limits). if (!this.queue.length) { const cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return false; const player = cmpPlayer.GetPlayerID(); const cmpUpgrade = Engine.QueryInterface(this.entity, IID_Upgrade); if (cmpUpgrade && cmpUpgrade.IsUpgrading()) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [player], "message": markForTranslation("Entity is being upgraded. Cannot start production."), "translateMessage": true }); return false; } } else if (this.queue.length >= this.MaxQueueSize) { const cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return false; const player = cmpPlayer.GetPlayerID(); const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [player], "message": markForTranslation("The production queue is full."), "translateMessage": true, }); return false; } const item = new this.Item(this.entity, metadata); if (!item.Queue(type, templateName, count)) return false; item.SetID(this.nextID++); if (pushFront) { this.queue[0]?.Pause(); this.queue.unshift(item); } else this.queue.push(item); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); if (!this.timer) this.StartTimer(); return true; }; /* * @param {number} - The ID of the item to remove from the queue. */ ProductionQueue.prototype.RemoveItem = function(id) { let itemIndex = this.queue.findIndex(item => item.id == id); if (itemIndex == -1) return; this.queue.splice(itemIndex, 1)[0].Stop(); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); if (!this.queue.length) this.StopTimer(); }; ProductionQueue.prototype.SetAnimation = function(name) { let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation(name, false, 1); }; /* * Returns basic data from all batches in the production queue. */ ProductionQueue.prototype.GetQueue = function() { return this.queue.map(item => item.GetBasicInfo()); }; /* * Removes all existing batches from the queue. */ ProductionQueue.prototype.ResetQueue = function() { while (this.queue.length) this.RemoveItem(this.queue[0].id); this.DisableAutoQueue(); }; /* * Increments progress on the first item in the production queue. * @param {Object} data - Unused in this case. * @param {number} lateness - The time passed since the expected time to fire the function. */ ProductionQueue.prototype.ProgressTimeout = function(data, lateness) { if (this.paused) return; // Allocate available time to as many queue items as it takes // until we've used up all the time (so that we work accurately // with items that take fractions of a second). let time = this.ProgressInterval + lateness; while (this.queue.length) { let item = this.queue[0]; if (!item.IsStarted()) { if (item.entity) this.SetAnimation("training"); if (item.technology) this.SetAnimation("researching"); item.Start(); } time -= item.Progress(time); if (!item.IsFinished()) { Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); return; } this.queue.shift(); Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); // If autoqueuing, push a new unit on the queue immediately, // but don't start right away. This 'wastes' some time, making // autoqueue slightly worse than regular queuing, and also ensures // that autoqueue doesn't train more than one item per turn, // if the units would take fewer than ProgressInterval ms to train. if (this.autoqueuing) { const autoqueueData = item.OriginalItem(); if (!autoqueueData) continue; if (!this.AddItem(autoqueueData.templateName, "unit", autoqueueData.count, autoqueueData.metadata)) { this.DisableAutoQueue(); const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [QueryOwnerInterface(this.entity).GetPlayerID()], "message": markForTranslation("Could not auto-queue unit, de-activating."), "translateMessage": true }); } break; } } if (!this.queue.length) this.StopTimer(); }; ProductionQueue.prototype.PauseProduction = function() { this.StopTimer(); this.paused = true; this.queue[0]?.Pause(); }; ProductionQueue.prototype.UnpauseProduction = function() { delete this.paused; this.StartTimer(); }; ProductionQueue.prototype.StartTimer = function() { if (this.timer) return; this.timer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetInterval( this.entity, IID_ProductionQueue, "ProgressTimeout", this.ProgressInterval, this.ProgressInterval, null ); }; ProductionQueue.prototype.StopTimer = function() { if (!this.timer) return; this.SetAnimation("idle"); Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).CancelTimer(this.timer); delete this.timer; }; /** * @return {boolean} - Whether this entity is currently producing. */ ProductionQueue.prototype.HasQueuedProduction = function() { return this.queue.length > 0; }; ProductionQueue.prototype.OnOwnershipChanged = function(msg) { // Reset the production queue whenever the owner changes. // (This should prevent players getting surprised when they capture // an enemy building, and then loads of the enemy's civ's soldiers get // created from it. Also it means we don't have to worry about // updating the reserved pop slots.) this.ResetQueue(); }; ProductionQueue.prototype.OnGarrisonedStateChanged = function(msg) { if (msg.holderID != INVALID_ENTITY) this.PauseProduction(); else this.UnpauseProduction(); }; Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue);