diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index edf167b27c..4be5e07b41 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -16,12 +16,28 @@ using System.Linq; using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Pathfinder; using OpenRA.Mods.Common.Traits; -using OpenRA.Primitives; using OpenRA.Support; using OpenRA.Traits; namespace OpenRA.Mods.Common.AI { + class CaptureTarget where TInfoType : class, ITraitInfoInterface + { + internal readonly Actor Actor; + internal readonly TInfoType Info; + + /// The order string given to the capturer so they can capture this actor. + /// ExternalCaptureActor + internal readonly string OrderString; + + internal CaptureTarget(Actor actor, string orderString) + { + Actor = actor; + Info = actor.Info.TraitInfoOrDefault(); + OrderString = orderString; + } + } + public sealed class HackyAIInfo : IBotInfo, ITraitInfo { public class UnitCategories @@ -174,6 +190,27 @@ namespace OpenRA.Mods.Common.AI [FieldLoader.LoadUsing("LoadDecisions")] public readonly List PowerDecisions = new List(); + [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"); @@ -254,6 +291,8 @@ namespace OpenRA.Mods.Common.AI int assignRolesTicks; int attackForceTicks; int minAttackForceDelayTicks; + int minCaptureDelayTicks; + readonly int maximumCaptureTargetOptions; readonly Queue orders = new Queue(); @@ -279,6 +318,8 @@ namespace OpenRA.Mods.Common.AI foreach (var decision in info.PowerDecisions) powerDecisions.Add(decision.OrderName, decision); + + maximumCaptureTargetOptions = Math.Max(1, Info.MaximumCaptureTargetOptions); } public static void BotDebug(string s, params object[] args) @@ -311,6 +352,7 @@ 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); var tileset = World.Map.Rules.TileSet; resourceTypeIndices = new BitArray(tileset.TerrainInfo.Length); // Big enough @@ -631,6 +673,95 @@ namespace OpenRA.Mods.Common.AI } FindAndDeployBackupMcv(self); + + if (--minCaptureDelayTicks <= 0) + { + minCaptureDelayTicks = Info.MinimumCaptureDelay; + QueueCaptureOrders(); + } + } + + IEnumerable GetVisibleActorsBelongingToPlayer(Player owner) + { + foreach (var actor in GetActorsThatCanBeOrderedByPlayer(owner)) + if (actor.CanBeViewedByPlayer(owner)) + 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)).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 => target.Info != null && capturers.Any(capturer => target.Info.CanBeTargetedBy(capturer, target.Actor.Owner))) + .OrderByDescending(target => target.Actor.GetSellValue()) + .Take(maximumCaptureTargetOptions); + + var externalCapturableTargetOptions = targetOptions + .Select(a => new CaptureTarget(a, "ExternalCaptureActor")) + .Where(target => target.Info != null && capturers.Any(capturer => target.Info.CanBeTargetedBy(capturer, target.Actor.Owner))) + .OrderByDescending(target => target.Actor.GetSellValue()) + .Take(maximumCaptureTargetOptions); + + if (Info.CapturableActorTypes.Any()) + { + capturableTargetOptions = capturableTargetOptions.Where(target => Info.CapturableActorTypes.Contains(target.Actor.Info.Name.ToLowerInvariant())); + externalCapturableTargetOptions = externalCapturableTargetOptions.Where(target => Info.CapturableActorTypes.Contains(target.Actor.Info.Name.ToLowerInvariant())); + } + + if (!capturableTargetOptions.Any() && !externalCapturableTargetOptions.Any()) + return; + + var capturesCapturers = capturers.Where(a => a.Info.HasTraitInfo()); + var externalCapturers = capturers.Except(capturesCapturers).Where(a => a.Info.HasTraitInfo()); + + foreach (var capturer in capturesCapturers) + QueueCaptureOrderFor(capturer, GetCapturerTargetClosestToOrDefault(capturer, capturableTargetOptions)); + + foreach (var capturer in externalCapturers) + QueueCaptureOrderFor(capturer, GetCapturerTargetClosestToOrDefault(capturer, externalCapturableTargetOptions)); + } + + 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, true) { TargetActor = target.Actor }); + 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); } CPos FindNextResource(Actor harvester)