From 956b3f96db29a092dff9e598388307e71377d408 Mon Sep 17 00:00:00 2001 From: wraitii Date: Sat, 12 Jun 2021 09:43:57 +0000 Subject: [PATCH] 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. --- .../public/gui/session/selection_panels.js | 56 ++++++++++++++----- .../simulation/components/ProductionQueue.js | 26 +++++++-- .../components/tests/test_ProductionQueue.js | 2 +- 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/binaries/data/mods/public/gui/session/selection_panels.js b/binaries/data/mods/public/gui/session/selection_panels.js index 8cb8aa0263..07beb52afc 100644 --- a/binaries/data/mods/public/gui/session/selection_panels.js +++ b/binaries/data/mods/public/gui/session/selection_panels.js @@ -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; diff --git a/binaries/data/mods/public/simulation/components/ProductionQueue.js b/binaries/data/mods/public/simulation/components/ProductionQueue.js index 32ce67e4dc..0d1de95de9 100644 --- a/binaries/data/mods/public/simulation/components/ProductionQueue.js +++ b/binaries/data/mods/public/simulation/components/ProductionQueue.js @@ -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) diff --git a/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js b/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js index d6030f2988..b084e23fad 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js +++ b/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js @@ -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();