diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index aec770f691..f2dc1407f8 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -119,27 +119,6 @@ namespace OpenRA.Mods.Common.AI [FieldLoader.LoadUsing("LoadBuildingCategories", true)] public readonly BuildingCategories BuildingCommonNames; - [Desc("Actor types that can capture other actors (via `Captures` or `ExternalCaptures`).", - "Leave this empty to disable capturing.")] - public HashSet CapturingActorTypes = new HashSet(); - - [Desc("Actor types that can be targeted for capturing.", - "Leave this empty to include all actors.")] - public HashSet CapturableActorTypes = new HashSet(); - - [Desc("Minimum delay (in ticks) between trying to capture with CapturingActorTypes.")] - public readonly int MinimumCaptureDelay = 375; - - [Desc("Maximum number of options to consider for capturing.", - "If a value less than 1 is given 1 will be used instead.")] - public readonly int MaximumCaptureTargetOptions = 10; - - [Desc("Should visibility (Shroud, Fog, Cloak, etc) be considered when searching for capturable targets?")] - public readonly bool CheckCaptureTargetsForVisibility = true; - - [Desc("Player stances that capturers should attempt to target.")] - public readonly Stance CapturableStances = Stance.Enemy | Stance.Neutral; - static object LoadUnitCategories(MiniYaml yaml) { var categories = yaml.Nodes.First(n => n.Key == "UnitsCommonNames"); @@ -206,8 +185,6 @@ namespace OpenRA.Mods.Common.AI int assignRolesTicks; int attackForceTicks; int minAttackForceDelayTicks; - int minCaptureDelayTicks; - readonly int maximumCaptureTargetOptions; public HackyAI(HackyAIInfo info, ActorInitializer init) { @@ -223,8 +200,6 @@ namespace OpenRA.Mods.Common.AI && unit.Info.HasTraitInfo(); unitCannotBeOrdered = a => a.Owner != Player || a.IsDead || !a.IsInWorld; - - maximumCaptureTargetOptions = Math.Max(1, Info.MaximumCaptureTargetOptions); } // Called by the host's player creation code @@ -246,7 +221,6 @@ namespace OpenRA.Mods.Common.AI assignRolesTicks = Random.Next(0, Info.AssignRolesInterval); attackForceTicks = Random.Next(0, Info.AttackForceInterval); minAttackForceDelayTicks = Random.Next(0, Info.MinimumAttackForceDelay); - minCaptureDelayTicks = Random.Next(0, Info.MinimumCaptureDelay); } void IBot.QueueOrder(Order order) @@ -459,96 +433,6 @@ namespace OpenRA.Mods.Common.AI minAttackForceDelayTicks = Info.MinimumAttackForceDelay; CreateAttackForce(); } - - if (--minCaptureDelayTicks <= 0) - { - minCaptureDelayTicks = Info.MinimumCaptureDelay; - QueueCaptureOrders(); - } - } - - IEnumerable GetVisibleActorsBelongingToPlayer(Player owner) - { - foreach (var actor in GetActorsThatCanBeOrderedByPlayer(owner)) - if (actor.CanBeViewedByPlayer(Player)) - yield return actor; - } - - IEnumerable GetActorsThatCanBeOrderedByPlayer(Player owner) - { - foreach (var actor in World.Actors) - if (actor.Owner == owner && !actor.IsDead && actor.IsInWorld) - yield return actor; - } - - void QueueCaptureOrders() - { - if (!Info.CapturingActorTypes.Any() || Player.WinState != WinState.Undefined) - return; - - var capturers = unitsHangingAroundTheBase - .Where(a => a.IsIdle && Info.CapturingActorTypes.Contains(a.Info.Name)) - .Select(a => new TraitPair(a, a.TraitOrDefault())) - .Where(tp => tp.Trait != null) - .ToArray(); - - if (capturers.Length == 0) - return; - - var randPlayer = World.Players.Where(p => !p.Spectating - && Info.CapturableStances.HasStance(Player.Stances[p])).Random(Random); - - var targetOptions = Info.CheckCaptureTargetsForVisibility - ? GetVisibleActorsBelongingToPlayer(randPlayer) - : GetActorsThatCanBeOrderedByPlayer(randPlayer); - - var capturableTargetOptions = targetOptions - .Select(a => new CaptureTarget(a, "CaptureActor")) - .Where(target => - { - if (target.Info == null) - return false; - - var captureManager = target.Actor.TraitOrDefault(); - if (captureManager == null) - return false; - - return capturers.Any(tp => captureManager.CanBeTargetedBy(target.Actor, tp.Actor, tp.Trait)); - }) - .OrderByDescending(target => target.Actor.GetSellValue()) - .Take(maximumCaptureTargetOptions); - - if (Info.CapturableActorTypes.Any()) - capturableTargetOptions = capturableTargetOptions.Where(target => Info.CapturableActorTypes.Contains(target.Actor.Info.Name.ToLowerInvariant())); - - if (!capturableTargetOptions.Any()) - return; - - var capturesCapturers = capturers.Where(tp => tp.Actor.Info.HasTraitInfo()); - foreach (var tp in capturesCapturers) - QueueCaptureOrderFor(tp.Actor, GetCapturerTargetClosestToOrDefault(tp.Actor, capturableTargetOptions)); - } - - void QueueCaptureOrderFor(Actor capturer, CaptureTarget target) where TTargetType : class, ITraitInfoInterface - { - if (capturer == null) - return; - - if (target == null) - return; - - if (target.Actor == null) - return; - - QueueOrder(new Order(target.OrderString, capturer, Target.FromActor(target.Actor), true)); - AIUtils.BotDebug("AI ({0}): Ordered {1} to capture {2}", Player.ClientIndex, capturer, target.Actor); - activeUnits.Remove(capturer); - } - - CaptureTarget GetCapturerTargetClosestToOrDefault(Actor capturer, IEnumerable> targets) - where TTargetType : class, ITraitInfoInterface - { - return targets.MinByOrDefault(target => (target.Actor.CenterPosition - capturer.CenterPosition).LengthSquared); } void FindNewUnits(Actor self) diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 9c6689b195..012fdd7bd8 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -128,6 +128,7 @@ + diff --git a/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs new file mode 100644 index 0000000000..34209f3aeb --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs @@ -0,0 +1,192 @@ +#region Copyright & License Information +/* + * Copyright 2007-2018 The OpenRA Developers (see AUTHORS) + * 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.Mods.Common.AI; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Manages AI capturing logic.")] + public class CaptureManagerBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that can capture other actors (via `Captures`).", + "Leave this empty to disable capturing.")] + public readonly HashSet CapturingActorTypes = new HashSet(); + + [Desc("Actor types that can be targeted for capturing.", + "Leave this empty to include all actors.")] + public readonly HashSet CapturableActorTypes = new HashSet(); + + [Desc("Minimum delay (in ticks) between trying to capture with CapturingActorTypes.")] + public readonly int MinimumCaptureDelay = 375; + + [Desc("Maximum number of options to consider for capturing.", + "If a value less than 1 is given 1 will be used instead.")] + public readonly int MaximumCaptureTargetOptions = 10; + + [Desc("Should visibility (Shroud, Fog, Cloak, etc) be considered when searching for capturable targets?")] + public readonly bool CheckCaptureTargetsForVisibility = true; + + [Desc("Player stances that capturers should attempt to target.")] + public readonly Stance CapturableStances = Stance.Enemy | Stance.Neutral; + + public override object Create(ActorInitializer init) { return new CaptureManagerBotModule(init.Self, this); } + } + + public class CaptureManagerBotModule : ConditionalTrait, IBotTick + { + readonly World world; + readonly Player player; + readonly Func isEnemyUnit; + readonly Predicate unitCannotBeOrdered; + readonly int maximumCaptureTargetOptions; + int minCaptureDelayTicks; + + // Units that the bot already knows about and has given a capture order. Any unit not on this list needs to be given a new order. + List activeCapturers = new List(); + + public CaptureManagerBotModule(Actor self, CaptureManagerBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + + if (world.Type == WorldType.Editor) + return; + + isEnemyUnit = unit => + player.Stances[unit.Owner] == Stance.Enemy + && !unit.Info.HasTraitInfo() + && unit.Info.HasTraitInfo(); + + unitCannotBeOrdered = a => a.Owner != player || a.IsDead || !a.IsInWorld; + + maximumCaptureTargetOptions = Math.Max(1, Info.MaximumCaptureTargetOptions); + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + minCaptureDelayTicks = world.LocalRandom.Next(0, Info.MinimumCaptureDelay); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minCaptureDelayTicks <= 0) + { + minCaptureDelayTicks = Info.MinimumCaptureDelay; + QueueCaptureOrders(bot); + } + } + + internal Actor FindClosestEnemy(WPos pos) + { + return world.Actors.Where(isEnemyUnit).ClosestTo(pos); + } + + internal Actor FindClosestEnemy(WPos pos, WDist radius) + { + return world.FindActorsInCircle(pos, radius).Where(isEnemyUnit).ClosestTo(pos); + } + + IEnumerable GetVisibleActorsBelongingToPlayer(Player owner) + { + foreach (var actor in GetActorsThatCanBeOrderedByPlayer(owner)) + if (actor.CanBeViewedByPlayer(player)) + yield return actor; + } + + IEnumerable GetActorsThatCanBeOrderedByPlayer(Player owner) + { + foreach (var actor in world.Actors) + if (actor.Owner == owner && !actor.IsDead && actor.IsInWorld) + yield return actor; + } + + void QueueCaptureOrders(IBot bot) + { + if (!Info.CapturingActorTypes.Any() || player.WinState != WinState.Undefined) + return; + + activeCapturers.RemoveAll(unitCannotBeOrdered); + + var newUnits = world.ActorsHavingTrait() + .Where(a => a.Owner == player && !activeCapturers.Contains(a)); + + var capturers = newUnits + .Where(a => a.IsIdle && Info.CapturingActorTypes.Contains(a.Info.Name)) + .Select(a => new TraitPair(a, a.TraitOrDefault())) + .Where(tp => tp.Trait != null) + .ToArray(); + + if (capturers.Length == 0) + return; + + var randPlayer = world.Players.Where(p => !p.Spectating + && Info.CapturableStances.HasStance(player.Stances[p])).Random(world.LocalRandom); + + var targetOptions = Info.CheckCaptureTargetsForVisibility + ? GetVisibleActorsBelongingToPlayer(randPlayer) + : GetActorsThatCanBeOrderedByPlayer(randPlayer); + + var capturableTargetOptions = targetOptions + .Select(a => new CaptureTarget(a, "CaptureActor")) + .Where(target => + { + if (target.Info == null) + return false; + + var captureManager = target.Actor.TraitOrDefault(); + if (captureManager == null) + return false; + + return capturers.Any(tp => captureManager.CanBeTargetedBy(target.Actor, tp.Actor, tp.Trait)); + }) + .OrderByDescending(target => target.Actor.GetSellValue()) + .Take(maximumCaptureTargetOptions); + + if (Info.CapturableActorTypes.Any()) + capturableTargetOptions = capturableTargetOptions.Where(target => Info.CapturableActorTypes.Contains(target.Actor.Info.Name.ToLowerInvariant())); + + if (!capturableTargetOptions.Any()) + return; + + var capturesCapturers = capturers.Where(tp => tp.Actor.Info.HasTraitInfo()); + foreach (var tp in capturesCapturers) + QueueCaptureOrderFor(bot, tp.Actor, GetCapturerTargetClosestToOrDefault(tp.Actor, capturableTargetOptions)); + } + + void QueueCaptureOrderFor(IBot bot, Actor capturer, CaptureTarget target) where TTargetType : class, ITraitInfoInterface + { + if (capturer == null) + return; + + if (target == null) + return; + + if (target.Actor == null) + return; + + bot.QueueOrder(new Order(target.OrderString, capturer, Target.FromActor(target.Actor), true)); + AIUtils.BotDebug("AI ({0}): Ordered {1} to capture {2}", player.ClientIndex, capturer, target.Actor); + activeCapturers.Add(capturer); + } + + CaptureTarget GetCapturerTargetClosestToOrDefault(Actor capturer, IEnumerable> targets) + where TTargetType : class, ITraitInfoInterface + { + return targets.MinByOrDefault(target => (target.Actor.CenterPosition - capturer.CenterPosition).LengthSquared); + } + } +} diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 3b3152df64..eb94e77c1d 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -453,6 +453,7 @@ namespace OpenRA.Mods.Common.Traits [RequireExplicitImplementation] public interface IBotTick { void BotTick(IBot bot); } + [RequireExplicitImplementation] public interface IBotRespondToAttack { void RespondToAttack(IBot bot, Actor self, AttackInfo e); } [RequireExplicitImplementation] diff --git a/OpenRA.Mods.Common/UpdateRules/Rules/20180923/ExtractHackyAIModules.cs b/OpenRA.Mods.Common/UpdateRules/Rules/20180923/ExtractHackyAIModules.cs index bc9433c069..b05146fae0 100644 --- a/OpenRA.Mods.Common/UpdateRules/Rules/20180923/ExtractHackyAIModules.cs +++ b/OpenRA.Mods.Common/UpdateRules/Rules/20180923/ExtractHackyAIModules.cs @@ -74,6 +74,16 @@ namespace OpenRA.Mods.Common.UpdateRules.Rules "MaxBaseRadius", }; + readonly string[] captureManagerFields = + { + "CapturingActorTypes", + "CapturableActorTypes", + "MinimumCaptureDelay", + "MaximumCaptureTargetOptions", + "CheckCaptureTargetsForVisibility", + "CapturableStances", + }; + public override IEnumerable AfterUpdate(ModData modData) { if (!messageShown) @@ -245,6 +255,21 @@ namespace OpenRA.Mods.Common.UpdateRules.Rules requiresConditionNode.ReplaceValue(oldValue + " || " + conditionString); } } + + if (captureManagerFields.Any(f => hackyAINode.ChildrenMatching(f).Any())) + { + var node = new MiniYamlNode("CaptureManagerBotModule@" + aiType, ""); + node.AddNode(new MiniYamlNode("RequiresCondition", conditionString)); + + foreach (var field in captureManagerFields) + { + var fieldNode = hackyAINode.LastChildMatching(field); + if (fieldNode != null) + fieldNode.MoveNode(hackyAINode, node); + } + + addNodes.Add(node); + } } // Only add module if any bot is using/enabling it.