0ad/binaries/data/mods/public/simulation/components/ProductionQueue.js
Ralph Sennhauser 0baafb5375
Don't set animation for production with UnitAi
Entities with a production queue when queueing or unqueueing items for
will set an appropriate animation which is desired for structures, like
the forge producing smoke, but not so for units as it interferes with
UnitAi animation state.

Units don't have animations for training or researching so the idle
animation will be set in that case instead. If such a unit is in motion
this results in the unit gliding. To avoid this just skip setting an
animation for entities having a UnitAI.

Reported-by: wowgetoffyourcellphone
Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
2025-10-07 19:25:21 +02:00

531 lines
14 KiB
JavaScript

function ProductionQueue() {}
ProductionQueue.prototype.Schema =
"<a:help>Helps the building to train new units and research technologies.</a:help>" +
"<empty/>";
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 (Object.hasOwn(this, 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 (Object.hasOwn(this, 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())
{
const 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)
{
const 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)
{
// In case the entity has a UnitAI discard the attempted change of
// animation as it would interfere with animation logic in UnitAI.
if (Engine.QueryInterface(this.entity, IID_UnitAI))
return;
const 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)
{
const 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);