1
0
forked from 0ad/0ad

Improvement to autoqueue usability

The graphical interface will show autoqueued units as 'ghost' units so
the player gets immediate feedback on what's happening.
The autoqueue disengages if it runs out of resources or entity limits:
this reflects that the autoqueue cannot resume on its own.

By changing the spot where the autoqueue pushes items, the autoqueue no
longer 'buffers' one unit in advance, but remains very slightly less
efficient than manual queuing. This also prevents cheats from leading to
a ton of units accidentally being created.

Accepted By: Freagarach
Refs #6213

Differential Revision: https://code.wildfiregames.com/D4144
This was SVN commit r25779.
This commit is contained in:
wraitii 2021-06-12 09:43:57 +00:00
parent 42d6893eb8
commit 956b3f96db
3 changed files with 62 additions and 22 deletions

View File

@ -493,22 +493,37 @@ g_SelectionPanels.Queue = {
*/
"getItems": function(unitEntStates)
{
let queue = [];
const queue = [];
let foundNew = true;
for (let i = 0; foundNew; ++i)
{
foundNew = false;
for (let state of unitEntStates)
for (const state of unitEntStates)
{
if (!state.production || !state.production.queue[i])
continue;
queue.push({
"producingEnt": state.id,
"queuedItem": state.production.queue[i]
"queuedItem": state.production.queue[i],
"autoqueue": state.production.autoqueue && state.production.queue[i].unitTemplate,
});
foundNew = true;
}
}
if (!queue.length)
return queue;
// Add 'ghost' items to show autoqueues.
const repeat = [];
for (const item of queue)
if (item.autoqueue)
{
const ghostItem = clone(item);
ghostItem.ghost = true;
repeat.push(ghostItem);
}
if (repeat.length)
for (let i = 0; queue.length < g_SelectionPanels.Queue.getMaxNumberOfItems(); ++i)
queue.push(repeat[i % repeat.length]);
return queue;
},
"resizePanel": function(numberOfItems, rowLength)
@ -536,10 +551,11 @@ g_SelectionPanels.Queue = {
warning("Unknown production queue template " + uneval(queuedItem));
return false;
}
data.button.onPress = function() { removeFromProductionQueue(data.item.producingEnt, queuedItem.id); };
const tooltips = [getEntityNames(template)];
if (data.item.ghost)
tooltips.push(translate("The auto-queue will try to train this item later."));
if (queuedItem.neededSlots)
{
tooltips.push(coloredText(translate("Insufficient population capacity:"), "red"));
@ -553,22 +569,32 @@ g_SelectionPanels.Queue = {
data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : "";
// Show the time remaining to finish the first item
if (data.i == 0)
Engine.GetGUIObjectByName("queueTimeRemaining").caption =
Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss"));
if (data.item.ghost)
{
data.button.enabled = false;
Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]").sprite="color:0 150 250 50";
}
else
{
// Show the time remaining to finish the first item
if (data.i == 0)
Engine.GetGUIObjectByName("queueTimeRemaining").caption =
Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss"));
let guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]");
let size = guiObject.size;
const guiObject = Engine.GetGUIObjectByName("unitQueueProgressSlider[" + data.i + "]");
guiObject.sprite = "queueProgressSlider";
const size = guiObject.size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(queuedItem.progress * (size.right - size.left));
guiObject.size = size;
// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
size.top = size.left + Math.round(queuedItem.progress * (size.right - size.left));
guiObject.size = size;
data.button.enabled = controlsPlayer(data.player);
}
if (template.icon)
data.icon.sprite = "stretched:session/portraits/" + template.icon;
data.icon.sprite = (data.item.ghost ? "grayscale:" : "") + "stretched:session/portraits/" + template.icon;
data.button.enabled = controlsPlayer(data.player);
const showTemplateFunc = () => { showTemplateDetails(data.item.queuedItem.unitTemplate || data.item.queuedItem.technologyTemplate, data.playerState.civ); };
data.button.onPressRight = showTemplateFunc;

View File

@ -809,12 +809,6 @@ ProductionQueue.prototype.ProgressTimeout = function(data, lateness)
cmpPlayer.UnBlockTraining();
// AutoQueue: We add the second batch after starting the first.
// (As opposed to when the first batch finishes.)
// This is to make the feature not infinitely better than good micro.
if (this.autoqueuing)
this.AddItem(item.unitTemplate, "unit", item.count, item.metadata);
Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity });
}
if (item.technologyTemplate)
@ -891,6 +885,26 @@ ProductionQueue.prototype.ProgressTimeout = function(data, lateness)
time -= item.timeRemaining;
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 && item.unitTemplate)
{
if (!this.AddItem(item.unitTemplate, "unit", item.count, item.metadata))
{
this.DisableAutoQueue();
const cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification({
"players": [cmpPlayer.GetPlayerID()],
"message": markForTranslation("Could not auto-queue unit, de-activating."),
"translateMessage": true
});
}
break;
}
}
if (!this.queue.length)

View File

@ -615,7 +615,7 @@ function test_auto_queue()
cmpProdQueue.AddItem("some_template", "unit", 3);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
cmpProdQueue.ProgressTimeout(null, 0);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2);
TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1);
}
testEntitiesList();