From f7e1b4259c3e2cbd886f35b27067dfd15f6312a1 Mon Sep 17 00:00:00 2001 From: dnqbob Date: Thu, 11 Sep 2025 23:42:20 +0800 Subject: [PATCH] Add PowerDownBotModule --- .../Traits/BotModules/PowerDownBotManager.cs | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 OpenRA.Mods.Common/Traits/BotModules/PowerDownBotManager.cs diff --git a/OpenRA.Mods.Common/Traits/BotModules/PowerDownBotManager.cs b/OpenRA.Mods.Common/Traits/BotModules/PowerDownBotManager.cs new file mode 100644 index 0000000000..1437aea9cf --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/PowerDownBotManager.cs @@ -0,0 +1,203 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI powerdown.", + "You need to use PowerMultiplier on toggle control only on related buildings, for calculation of this bot module")] + public class PowerDownBotModuleInfo : ConditionalTraitInfo + { + [Desc("Delay (in ticks) between two action on toggling powerdown.")] + public readonly int Interval = 150; + + [Desc("Actors that allow this module to toggle")] + public readonly HashSet PowerDownTypes = []; + + [Desc("Order used by " + nameof(ToggleConditionOnOrderInfo) + " for powerdown on toggled actor.")] + public readonly string PowerDownOrder = "PowerDown"; + + public override object Create(ActorInitializer init) { return new PowerDownBotModule(init.Self, this); } + } + + public class PowerDownBotModule : ConditionalTrait, IBotTick, IGameSaveTraitData, INotifyActorDisposing + { + readonly World world; + readonly Player player; + readonly ActorIndex.OwnerAndNamesAndTrait togglable; + readonly Func isTogglableValid; + + // We keep a list to track toggled buildings for performance. + readonly List toggled = []; + + PowerManager playerPower; + int toggleTick; + + sealed class ToggledPowerWrapper + { + public int ExpectedPowerChanging; + public Actor Actor; + + public ToggledPowerWrapper(Actor a, int p) + { + Actor = a; + ExpectedPowerChanging = p; + } + } + + public PowerDownBotModule(Actor self, PowerDownBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + togglable = new ActorIndex.OwnerAndNamesAndTrait(world, info.PowerDownTypes, player); + + isTogglableValid = a => a != null && a.Owner == self.Owner && !a.IsDead && a.IsInWorld; + } + + protected override void Created(Actor self) + { + playerPower = self.Owner.PlayerActor.TraitOrDefault(); + } + + protected override void TraitEnabled(Actor self) + { + toggleTick = world.LocalRandom.Next(Info.Interval); + } + + // We calculate the approximate power changing if toggled on. Since we have no idea on the conditions set by users + // Calculation here need user to use PowerMultiplier on toggle control only on related buildings, for calculation of this bot module. + static int GetTogglePowerChanging(Actor a) + { + var powerChangingIfToggled = 0; + var power = a.TraitsImplementing().Where(t => !t.IsTraitDisabled).Sum(t => t.Info.Amount); + if (power != 0) + { + var powerMulTraits = a.TraitsImplementing().ToArray(); + powerChangingIfToggled = power * (powerMulTraits.Sum(p => p.Info.Modifier) - 100) / 100; + if (Array.Exists(powerMulTraits, t => !t.IsTraitDisabled)) + powerChangingIfToggled = -powerChangingIfToggled; + } + + return powerChangingIfToggled; + } + + IEnumerable GetOnlineBuildings() + { + var toggleableBuildings = new List(); + + foreach (var a in togglable.Actors.Where(a => !a.IsDead && a.Info.HasTraitInfo())) + { + // Note: it is OK if GetTogglePowerChanging is not accurate, when player is still in lowpower. + // The bot will try to toggle off more buildings next bot tick. + var powerChanging = GetTogglePowerChanging(a); + if (powerChanging > 0) + toggleableBuildings.Add(new ToggledPowerWrapper(a, powerChanging)); + } + + return toggleableBuildings.OrderBy(bpw => bpw.ExpectedPowerChanging); + } + + void IBotTick.BotTick(IBot bot) + { + if (toggleTick > 0 || playerPower == null) + { + toggleTick--; + return; + } + + var power = playerPower.ExcessPower; + var togglingBuildings = new List(); + + // When there is extra power, check if AI can toggle on + // TODO: captured disabled actors will never be toggled on + if (power > 0 && toggled.Count > 0) + { + toggled.RemoveAll(bpw => !isTogglableValid(bpw.Actor)); + toggled.Sort((bpw1, bpw2) => bpw2.ExpectedPowerChanging.CompareTo(bpw1.ExpectedPowerChanging)); + for (var i = 0; i < toggled.Count; i++) + { + var bpw = toggled[i]; + if (power + bpw.ExpectedPowerChanging < 0) + continue; + + togglingBuildings.Add(bpw.Actor); + power += bpw.ExpectedPowerChanging; + toggled.RemoveAt(i); + } + } + + // When there is no power, check if AI can toggle off + // and add those toggled to list for toggling on + else if (power < 0) + { + foreach (var bpw in GetOnlineBuildings()) + { + if (power > 0) + break; + + togglingBuildings.Add(bpw.Actor); + toggled.Add(new ToggledPowerWrapper(bpw.Actor, -bpw.ExpectedPowerChanging)); + power += bpw.ExpectedPowerChanging; + } + } + + if (togglingBuildings.Count > 0) + bot.QueueOrder(new Order(Info.PowerDownOrder, null, false, groupedActors: togglingBuildings.ToArray())); + + toggleTick = Info.Interval; + } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + var data = new List(); + foreach (var tb in toggled.Where(td => isTogglableValid(td.Actor))) + data.Add(new MiniYamlNode(FieldSaver.FormatValue(tb.Actor.ActorID), FieldSaver.FormatValue(tb.ExpectedPowerChanging))); + + return + [ + new("ToggledBuildings", new MiniYaml("", data)) + ]; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var nodes = data.ToDictionary(); + + if (nodes.TryGetValue("ToggledBuildings", out var toggledBuildingsNode)) + { + foreach (var n in toggledBuildingsNode.Nodes) + { + var a = self.World.GetActorById(FieldLoader.GetValue(n.Key, n.Key)); + + if (isTogglableValid(a)) + toggled.Add(new ToggledPowerWrapper(a, FieldLoader.GetValue(n.Key, n.Value.Value))); + } + } + } + + void INotifyActorDisposing.Disposing(Actor self) + { + togglable.Dispose(); + } + } +}