From 89ac38bf4194dc1ef12ada5fafe88d20de47c0c1 Mon Sep 17 00:00:00 2001 From: Paul Schultz Date: Wed, 13 May 2026 17:49:53 -0500 Subject: [PATCH] Add idle factory alert Adds a visual alert for idle unit-producing factories. When a factory's production queue is empty or on hold for 10 seconds, the tab number and sidebar icon blink to draw attention. --- .../Logic/Ingame/ProductionTabsLogicCA.cs | 18 ++++++ .../Widgets/ProductionTabsCAWidget.cs | 59 ++++++++++++++++++- mods/ca/chrome/ingame-player.yaml | 3 + mods/ca/chrome/settings-display.yaml | 11 ++++ 4 files changed, 90 insertions(+), 1 deletion(-) diff --git a/OpenRA.Mods.CA/Widgets/Logic/Ingame/ProductionTabsLogicCA.cs b/OpenRA.Mods.CA/Widgets/Logic/Ingame/ProductionTabsLogicCA.cs index e2562e718f..58a004ff1a 100644 --- a/OpenRA.Mods.CA/Widgets/Logic/Ingame/ProductionTabsLogicCA.cs +++ b/OpenRA.Mods.CA/Widgets/Logic/Ingame/ProductionTabsLogicCA.cs @@ -45,6 +45,24 @@ void SetupProductionGroupButton(ProductionTypeButtonWidget button) var icon = button.Get("ICON"); icon.GetImageName = () => button.IsDisabled() ? chromeName + "-disabled" : tabs.Groups[button.ProductionGroup].Alert ? chromeName + "-alert" : chromeName; + + var defaultIsVisible = icon.IsVisible; + icon.IsVisible = () => + { + if (!defaultIsVisible()) + return false; + + var group = tabs.Groups[button.ProductionGroup]; + if (!group.Alert && group.HasIdleFactories + && tabs.QueueGroup != button.ProductionGroup + && Game.Settings.Game.IdleFactoryAlert) + { + // Blink by toggling visibility using sine wave + return Math.Sin(Game.LocalTick * tabs.IdleAlertBlinkRate * 2 * Math.PI) > -0.3; + } + + return true; + }; } [ObjectCreator.UseCtor] diff --git a/OpenRA.Mods.CA/Widgets/ProductionTabsCAWidget.cs b/OpenRA.Mods.CA/Widgets/ProductionTabsCAWidget.cs index e76fb91d44..188c13ab36 100644 --- a/OpenRA.Mods.CA/Widgets/ProductionTabsCAWidget.cs +++ b/OpenRA.Mods.CA/Widgets/ProductionTabsCAWidget.cs @@ -25,6 +25,8 @@ public class ProductionTabCA public string Name; public ProductionQueue Queue; public Actor Actor; + public int IdleTicks; + public bool IsIdle; } public class ProductionTabGroupCA @@ -32,6 +34,7 @@ public class ProductionTabGroupCA public List Tabs = new List(); public string Group; public bool Alert { get { return Tabs.Any(t => t.Queue.AllQueued().Any(i => i.Done)); } } + public bool HasIdleFactories { get { return Tabs.Any(t => t.IsIdle); } } public void Update(IEnumerable allQueues) { @@ -96,6 +99,12 @@ public class ProductionTabsCAWidget : Widget public readonly Color TabColor = Color.White; public readonly Color TabColorDone = Color.Gold; + public readonly int IdleAlertDelay = 250; + + public readonly HashSet IdleAlertGroups = new() { "Infantry", "Vehicle", "Aircraft", "Ship" }; + + public readonly float IdleAlertBlinkRate = 0.08f; + int contentWidth = 0; bool leftPressed = false; bool rightPressed = false; @@ -257,7 +266,21 @@ public override void Draw() // Draw number label var textSize = font.Measure(tab.Name); var position = new int2(rect.X + (rect.Width - textSize.X) / 2, (rect.Y + (rect.Height - textSize.Y) / 2) - 1); - font.DrawText(tab.Name, position, tab.Queue.AllQueued().Any(i => i.Done) ? TabColorDone : TabColor); + + Color tabTextColor; + var showText = true; + if (tab.Queue.AllQueued().Any(i => i.Done)) + tabTextColor = TabColorDone; + else if (tab.IsIdle && !highlighted) + { + tabTextColor = TabColor; + showText = Math.Sin(Game.LocalTick * IdleAlertBlinkRate * 2 * Math.PI) > -0.3; + } + else + tabTextColor = TabColor; + + if (showText) + font.DrawText(tab.Name, position, tabTextColor); tabsShown++; } @@ -348,6 +371,40 @@ public override void Tick() if (shouldUpdateQueues) foreach (var g in Groups.Values) g.Update(cachedProductionQueueEnabledStates.Select(t => t.Queue)); + + // Track idle ticks for each tab in groups that support idle alerts + if (Game.Settings.Game.IdleFactoryAlert) + { + foreach (var g in Groups.Values) + { + if (!IdleAlertGroups.Contains(g.Group)) + continue; + + foreach (var tab in g.Tabs) + { + var currentItem = tab.Queue.CurrentItem(); + if (currentItem == null || currentItem.Paused) + { + tab.IdleTicks++; + tab.IsIdle = tab.IdleTicks >= IdleAlertDelay; + } + else + { + tab.IdleTicks = 0; + tab.IsIdle = false; + } + } + } + } + else + { + foreach (var g in Groups.Values) + foreach (var tab in g.Tabs) + { + tab.IdleTicks = 0; + tab.IsIdle = false; + } + } } public override bool YieldMouseFocus(MouseInput mi) diff --git a/mods/ca/chrome/ingame-player.yaml b/mods/ca/chrome/ingame-player.yaml index e8dba99c45..dafcf30b57 100644 --- a/mods/ca/chrome/ingame-player.yaml +++ b/mods/ca/chrome/ingame-player.yaml @@ -735,6 +735,9 @@ Container@PLAYER_WIDGETS: TabWidth: 31 TabSpacing: 3 ArrowWidth: 17 + IdleAlertDelay: 250 + IdleAlertGroups: Infantry, Vehicle, Aircraft, Ship + IdleAlertBlinkRate: 0.08 Logic: AddFactionSuffixLogicCA, ProductionTabsLogicCA PaletteWidget: PRODUCTION_PALETTE TypesContainer: PRODUCTION_TYPES diff --git a/mods/ca/chrome/settings-display.yaml b/mods/ca/chrome/settings-display.yaml index d3230f7d05..c137bb53f7 100644 --- a/mods/ca/chrome/settings-display.yaml +++ b/mods/ca/chrome/settings-display.yaml @@ -225,6 +225,17 @@ Container@DISPLAY_PANEL: Text: Selected Unit Tooltip TooltipText: When a single unit/structure is selected, show info about it in bottom right corner TooltipContainer: SETTINGS_TOOLTIP_CONTAINER + Container@IDLE_FACTORY_ALERT_CHECKBOX_CONTAINER: + X: PARENT_WIDTH / 2 + 10 + Width: PARENT_WIDTH / 2 - 10 + Children: + Checkbox@IDLE_FACTORY_ALERT_CHECKBOX: + Width: PARENT_WIDTH + Height: 20 + Font: Regular + Text: Idle Factory Alert + TooltipText: Flash production tabs and sidebar icons when a factory is idle + TooltipContainer: SETTINGS_TOOLTIP_CONTAINER Container@SPACER: Background@VIDEO_SECTION_HEADER: X: 5