From 7d6a0497cfec698c25c4153e6ecff775ed08bf86 Mon Sep 17 00:00:00 2001 From: ottelo <33122175+ottelo9@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:02:15 +0200 Subject: [PATCH] Add files via upload --- .../Traits/BotModules/BaseBotModule.cs | 95 +++++++ .../Traits/BotModules/BuilderBotModule.cs | 167 +++++++++++ .../Traits/BotModules/CargoBotModule.cs | 144 ++++++++++ .../Traits/BotModules/CubePickupBotModule.cs | 144 ++++++++++ .../Traits/BotModules/DeployActorBotModule.cs | 100 +++++++ .../ExternalConditionPowerBotModule.cs | 83 ++++++ .../Traits/BotModules/PowerDownBotModule.cs | 191 +++++++++++++ .../PriorityCaptureManagerBotModule.cs | 261 ++++++++++++++++++ .../Traits/BotModules/ScoutBotModule.cs | 101 +++++++ .../BotModules/SendUnitToAttackBotModule.cs | 185 +++++++++++++ 10 files changed, 1471 insertions(+) create mode 100644 OpenRA.Mods.Common/Traits/BotModules/BaseBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/BuilderBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/CargoBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/CubePickupBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/DeployActorBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/ExternalConditionPowerBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/PowerDownBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/PriorityCaptureManagerBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/ScoutBotModule.cs create mode 100644 OpenRA.Mods.Common/Traits/BotModules/SendUnitToAttackBotModule.cs diff --git a/OpenRA.Mods.Common/Traits/BotModules/BaseBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BaseBotModule.cs new file mode 100644 index 000000000000..474b9b4177ee --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/BaseBotModule.cs @@ -0,0 +1,95 @@ +#region Copyright & License Information +/* + * Copyright 2007-2024 The OpenHV Developers (see AUTHORS) + * This file is part of OpenHV, 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.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [Desc("Manages the initial base.")] + public class BaseBotModuleInfo : ConditionalTraitInfo + { + public override object Create(ActorInitializer init) { return new BaseBotModule(init.Self, this); } + } + + public class BaseBotModule : ConditionalTrait, IBotTick, IBotPositionsUpdated, IGameSaveTraitData + { + readonly World world; + readonly Player player; + + IBotPositionsUpdated[] notifyPositionsUpdated; + + CPos initialBaseCenter; + bool initialized; + + public BaseBotModule(Actor self, BaseBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + } + + protected override void Created(Actor self) + { + notifyPositionsUpdated = self.Owner.PlayerActor.TraitsImplementing().ToArray(); + } + + void IBotTick.BotTick(IBot bot) + { + if (!initialized) + { + foreach (var baseBuilding in world.ActorsHavingTrait().Where(a => a.Owner == player)) + SetCenter(baseBuilding.Location); + + initialized = true; + } + } + + void SetCenter(CPos location) + { + foreach (var notify in notifyPositionsUpdated) + { + notify.UpdatedBaseCenter(location); + notify.UpdatedDefenseCenter(location); + } + } + + void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) + { + initialBaseCenter = newLocation; + } + + void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + return new List() + { + new("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)) + }; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var initialBaseCenterNode = data.NodeWithKeyOrDefault("InitialBaseCenter"); + if (initialBaseCenterNode != null) + initialBaseCenter = FieldLoader.GetValue("InitialBaseCenter", initialBaseCenterNode.Value.Value); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/BuilderBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/BuilderBotModule.cs new file mode 100644 index 000000000000..fe5c36bb1b8a --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/BuilderBotModule.cs @@ -0,0 +1,167 @@ +#region Copyright & License Information +/* + * Copyright 2022-2023 The OpenHV Developers (see AUTHORS) + * This file is part of OpenHV, 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.Collections.Generic; +using System.Linq; +using OpenRA.Mods.Common; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [Desc("Manages AI builders.")] + public class BuilderBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that can deploy into outposts.")] + public readonly HashSet BuilderTypes = new(); + + [Desc("Delay (in ticks) between looking for and giving out orders to new builders.")] + public readonly int ScanForNewBuilderInterval = 20; + + [Desc("Minimum distance in cells from center of the base when checking for builder deployment location.")] + public readonly int MinimumBaseRadius = 0; + + [Desc("Maximum distance in cells from center of the base when checking for builder deployment location.")] + public readonly int MaximumBaseRadius = 50; + + public override object Create(ActorInitializer init) { return new BuilderBotModule(init.Self, this); } + } + + public class BuilderBotModule : ConditionalTrait, IBotTick, IBotPositionsUpdated, IGameSaveTraitData + { + public CPos GetRandomBaseCenter() + { + var randomOutpost = world.Actors.Where(a => a.Owner == player && + (a.TraitOrDefault() != null)) + .RandomOrDefault(world.LocalRandom); + + return randomOutpost?.Location ?? initialBaseCenter; + } + + readonly World world; + readonly Player player; + + CPos initialBaseCenter; + int scanInterval; + bool firstTick = true; + + public BuilderBotModule(Actor self, BuilderBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + scanInterval = world.LocalRandom.Next(Info.ScanForNewBuilderInterval, Info.ScanForNewBuilderInterval * 2); + } + + void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) + { + initialBaseCenter = newLocation; + } + + void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } + + void IBotTick.BotTick(IBot bot) + { + if (firstTick) + { + DeployBuilders(bot, false); + firstTick = false; + } + + if (--scanInterval <= 0) + { + scanInterval = Info.ScanForNewBuilderInterval; + DeployBuilders(bot, true); + } + } + + void DeployBuilders(IBot bot, bool chooseLocation) + { + var newBuilders = world.ActorsHavingTrait() + .Where(a => a.Owner == player && a.IsIdle && Info.BuilderTypes.Contains(a.Info.Name)); + + foreach (var builder in newBuilders) + DeployBuilder(bot, builder, chooseLocation); + } + + // Find any builder and deploy at a sensible location. + void DeployBuilder(IBot bot, Actor mcv, bool move) + { + if (move) + { + var transformsInfo = mcv.Info.TraitInfo(); + var desiredLocation = ChooseDeployLocation(transformsInfo.IntoActor, transformsInfo.Offset); + if (desiredLocation == null) + return; + + bot.QueueOrder(new Order("Move", mcv, Target.FromCell(world, desiredLocation.Value), true)); + } + + bot.QueueOrder(new Order("DeployTransform", mcv, true)); + } + + CPos? ChooseDeployLocation(string actorType, CVec offset) + { + var actorInfo = world.Map.Rules.Actors[actorType]; + var bi = actorInfo.TraitInfoOrDefault(); + if (bi == null) + return null; + + // Find the buildable cell that is closest to pos and centered around center + CPos? FindPos(CPos center, CPos target, int minRange, int maxRange) + { + var cells = world.Map.FindTilesInAnnulus(center, minRange, maxRange); + + // Sort by distance to target if we have one + if (center != target) + cells = cells.OrderBy(c => (c - target).LengthSquared); + else + cells = cells.Shuffle(world.LocalRandom); + + foreach (var cell in cells) + if (world.CanPlaceBuilding(cell + offset, actorInfo, bi, null)) + return cell; + + return null; + } + + var baseCenter = GetRandomBaseCenter(); + + return FindPos(baseCenter, baseCenter, Info.MinimumBaseRadius, Info.MaximumBaseRadius); + } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + return new List() + { + new("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)) + }; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var initialBaseCenterNode = data.NodeWithKeyOrDefault("InitialBaseCenter"); + if (initialBaseCenterNode != null) + initialBaseCenter = FieldLoader.GetValue("InitialBaseCenter", initialBaseCenterNode.Value.Value); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/CargoBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/CargoBotModule.cs new file mode 100644 index 000000000000..eb38aa97540e --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/CargoBotModule.cs @@ -0,0 +1,144 @@ +#region Copyright & License Information +/* + * Copyright 2023-2024 The OpenHV Developers (see CREDITS) + * This file is part of OpenHV, 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.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Manages AI load unit related with " + nameof(Cargo) + " and " + nameof(Passenger) + " traits.")] + public class CargoBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that can be targeted for load, must have " + nameof(Cargo) + ".")] + public readonly HashSet TransportTypes = default; + + [Desc("Actor types that used for loading, must have " + nameof(Passenger) + ".")] + public readonly HashSet PassengerTypes = default; + + [Desc("Allow enter allied transport.")] + public readonly bool OnlyEnterOwnerPlayer = true; + + [Desc("Scan suitable actors and target in this interval.")] + public readonly int ScanTick = 317; + + [Desc("Don't load passengers to this actor if damage state is worse than this.")] + public readonly DamageState ValidDamageState = DamageState.Heavy; + + [Desc("Unload passengers to this actor if damage state is worse than this.")] + public readonly DamageState UnloadDamageState = DamageState.Heavy; + + [Desc("Don't load passengers that are further than this distance to this actor.")] + public readonly WDist MaxDistance = WDist.FromCells(20); + + public override object Create(ActorInitializer init) { return new CargoBotModule(init.Self, this); } + } + + public class CargoBotModule : ConditionalTrait, IBotTick, IBotRespondToAttack + { + readonly World world; + readonly Player player; + readonly Predicate unitCannotBeOrdered; + readonly Predicate unitCannotBeOrderedOrIsBusy; + readonly Predicate invalidTransport; + + int minAssignRoleDelayTicks; + + public CargoBotModule(Actor self, CargoBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + + if (info.OnlyEnterOwnerPlayer) + invalidTransport = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; + else + invalidTransport = a => a == null || a.IsDead || !a.IsInWorld || a.Owner.RelationshipWith(player) != PlayerRelationship.Ally; + + unitCannotBeOrdered = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; + unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || (!a.IsIdle && a.CurrentActivity is not FlyIdle); + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + minAssignRoleDelayTicks = world.LocalRandom.Next(0, Info.ScanTick); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minAssignRoleDelayTicks <= 0) + { + minAssignRoleDelayTicks = Info.ScanTick; + + var transporters = world.ActorsWithTrait().Where(at => + { + var health = at.Actor.TraitOrDefault()?.DamageState; + return Info.TransportTypes.Contains(at.Actor.Info.Name) && !invalidTransport(at.Actor) + && at.Trait.HasSpace(1) && (health == null || health < Info.ValidDamageState); + }).ToArray(); + + if (transporters.Length == 0) + return; + + var transporter = transporters.Random(world.LocalRandom); + var cargo = transporter.Trait; + var transport = transporter.Actor; + var spaceTaken = 0; + + var passengers = world.ActorsWithTrait().Where(at => !unitCannotBeOrderedOrIsBusy(at.Actor) && Info.PassengerTypes.Contains(at.Actor.Info.Name) && cargo.HasSpace(at.Trait.Info.Weight) && (at.Actor.CenterPosition - transport.CenterPosition).HorizontalLengthSquared <= Info.MaxDistance.LengthSquared) + .OrderBy(at => (at.Actor.CenterPosition - transport.CenterPosition).HorizontalLengthSquared); + + var orderedActors = new List(); + + foreach (var passenger in passengers) + { + var mobile = passenger.Actor.TraitOrDefault(); + if (mobile == null || !mobile.PathFinder.PathExistsForLocomotor(mobile.Locomotor, passenger.Actor.Location, transport.Location)) + continue; + + if (cargo.HasSpace(spaceTaken + passenger.Trait.Info.Weight)) + { + spaceTaken += passenger.Trait.Info.Weight; + orderedActors.Add(passenger.Actor); + } + + if (!cargo.HasSpace(spaceTaken + 1)) + break; + } + + if (orderedActors.Count > 0) + bot.QueueOrder(new Order("EnterTransport", null, Target.FromActor(transport), false, groupedActors: orderedActors.ToArray())); + } + } + + void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) + { + if (!Info.TransportTypes.Contains(self.Info.Name)) + return; + + if (unitCannotBeOrdered(self)) + return; + + var damageState = self.TraitOrDefault()?.DamageState; + if (damageState == null || damageState >= Info.UnloadDamageState) + { + var cargo = self.TraitOrDefault(); + if (cargo != null && !cargo.IsEmpty()) + bot.QueueOrder(new Order("Unload", self, false)); + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/CubePickupBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/CubePickupBotModule.cs new file mode 100644 index 000000000000..c0ee02a28e17 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/CubePickupBotModule.cs @@ -0,0 +1,144 @@ +#region Copyright & License Information +/* + * Copyright 2021-2022 The OpenHV Developers (see AUTHORS) + * This file is part of OpenHV, 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; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [TraitLocation(SystemActors.Player)] + [Desc("Put this on the Player actor. Manages cube collection.")] + public class CubePickupBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actor types that should not start hunting for cubes.")] + public readonly HashSet ExcludedUnitTypes = new(); + + [Desc("Only these actor types should start hunting for cubes.")] + public readonly HashSet IncludedUnitTypes = new(); + + [Desc("Interval (in ticks) between giving out orders to idle units.")] + public readonly int ScanForCubesInterval = 50; + + [Desc("Only move this far away from base. Disabled if set to zero.")] + public readonly int MaxProximityRadius = 0; + + [Desc("Avoid enemy actors nearby when searching for cubes. Should be somewhere near the max weapon range.")] + public readonly WDist EnemyAvoidanceRadius = WDist.FromCells(8); + + [Desc("Should visibility (Shroud, Fog, Cloak, etc) be considered when searching for cubes?")] + public readonly bool CheckTargetsForVisibility = true; + + public override object Create(ActorInitializer init) { return new CubePickupBotModule(init.Self, this); } + } + + public class CubePickupBotModule : ConditionalTrait, IBotTick + { + readonly World world; + readonly Player player; + readonly int maxProximity; + + CubeSpawner cubeSpawner; + + int scanForcubesTicks; + + readonly List alreadyPursuitcubes = new(); + + public CubePickupBotModule(Actor self, CubePickupBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + + maxProximity = Info.MaxProximityRadius > 0 ? info.MaxProximityRadius : world.Map.Grid.MaximumTileSearchRange; + } + + protected override void Created(Actor self) + { + cubeSpawner = self.Owner.World.WorldActor.TraitOrDefault(); + } + + protected override void TraitEnabled(Actor self) + { + scanForcubesTicks = Info.ScanForCubesInterval; + } + + void IBotTick.BotTick(IBot bot) + { + if (cubeSpawner == null || !cubeSpawner.IsTraitEnabled() || !cubeSpawner.Enabled) + return; + + if (--scanForcubesTicks > 0) + return; + + scanForcubesTicks = Info.ScanForCubesInterval; + + var cubes = world.ActorsHavingTrait().ToList(); + if (cubes.Count < 1) + return; + + if (Info.CheckTargetsForVisibility) + cubes.RemoveAll(c => !c.CanBeViewedByPlayer(player)); + + var idleUnits = world.ActorsHavingTrait().Where(a => a.Owner == player && a.IsIdle + && (Info.IncludedUnitTypes.Contains(a.Info.Name) || (Info.IncludedUnitTypes.Count < 1 && !Info.ExcludedUnitTypes.Contains(a.Info.Name)))).ToList(); + + if (idleUnits.Count < 1) + return; + + foreach (var cube in cubes) + { + if (alreadyPursuitcubes.Contains(cube)) + continue; + + if (!cube.IsAtGroundLevel()) + continue; + + var cubeCollector = idleUnits.ClosestToIgnoringPath(cube); + if (cubeCollector == null) + continue; + + if ((cube.Location - cubeCollector.Location).Length > maxProximity) + continue; + + idleUnits.Remove(cubeCollector); + + var target = PathToNextcube(cubeCollector, cube); + if (target.Type == TargetType.Invalid) + continue; + + var cell = world.Map.CellContaining(target.CenterPosition); + AIUtils.BotDebug($"{bot.Player}: Ordering {cubeCollector} to {cell} for cube pick up."); + bot.QueueOrder(new Order("Move", cubeCollector, target, true)); + alreadyPursuitcubes.Add(cube); + } + } + + Target PathToNextcube(Actor collector, Actor cube) + { + var mobile = collector.Trait(); + var path = mobile.PathFinder.FindPathToTargetCell( + collector, new[] { collector.Location }, cube.Location, BlockedByActor.Stationary, + location => world.FindActorsInCircle(world.Map.CenterOfCell(location), Info.EnemyAvoidanceRadius) + .Where(u => !u.IsDead && collector.Owner.RelationshipWith(u.Owner) == PlayerRelationship.Enemy) + .Sum(u => Math.Max(WDist.Zero.Length, Info.EnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(location) - u.CenterPosition).Length))); + + if (path.Count == 0) + return Target.Invalid; + + // Don't use the actor to avoid invalid targets when the cube disappears midway. + return Target.FromCell(world, cube.Location); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/DeployActorBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/DeployActorBotModule.cs new file mode 100644 index 000000000000..f1ec958f61b8 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/DeployActorBotModule.cs @@ -0,0 +1,100 @@ +#region Copyright & License Information +/* + * Copyright 2019-2022 The OpenHV Developers (see CREDITS) + * This file is part of OpenHV, 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.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [Desc("Deploys units when idle.")] + public class DeployActorBotModuleInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [ActorReference] + [Desc("Actor types that can deploy.")] + public readonly HashSet DeployableActorTypes = new(); + + [Desc("Minimum delay (in ticks) between trying to deploy with DeployableActorTypes.")] + public readonly int MinimumScanDelay = 100; + + public override object Create(ActorInitializer init) { return new DeployActorBotModule(init.Self, this); } + } + + public class DeployActorBotModule : ConditionalTrait, IBotTick + { + readonly World world; + readonly Player player; + + readonly Func unitCannotBeOrdered; + + int scanForIdleDetectorsTicks; + + sealed class ActorTraitWrapper + { + public readonly Actor Actor; + public readonly GrantConditionOnDeploy GrantConditionOnDeploy; + + public ActorTraitWrapper(Actor actor) + { + Actor = actor; + GrantConditionOnDeploy = actor.Trait(); + } + } + + readonly Dictionary actors = new(); + + public DeployActorBotModule(Actor self, DeployActorBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + + unitCannotBeOrdered = a => a.Owner != self.Owner || a.IsDead || !a.IsInWorld; + } + + protected override void TraitEnabled(Actor self) + { + // PERF: Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + scanForIdleDetectorsTicks = world.LocalRandom.Next(0, Info.MinimumScanDelay); + } + + void IBotTick.BotTick(IBot bot) + { + if (--scanForIdleDetectorsTicks > 0) + return; + + scanForIdleDetectorsTicks = Info.MinimumScanDelay; + + var toRemove = actors.Keys.Where(unitCannotBeOrdered).ToList(); + foreach (var a in toRemove) + actors.Remove(a); + + // TODO: Look for a more performance friendly way to update this list + var newActors = world.Actors.Where(a => Info.DeployableActorTypes.Contains(a.Info.Name) && a.Owner == player && !actors.ContainsKey(a)); + foreach (var a in newActors) + actors[a] = new ActorTraitWrapper(a); + + foreach (var actor in actors) + { + if (actor.Value.GrantConditionOnDeploy.DeployState != DeployState.Undeployed) + continue; + + if (!actor.Key.IsIdle) + continue; + + bot.QueueOrder(new Order("GrantConditionOnDeploy", actor.Key, true)); + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/ExternalConditionPowerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/ExternalConditionPowerBotModule.cs new file mode 100644 index 000000000000..5d1b7d6c28d8 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/ExternalConditionPowerBotModule.cs @@ -0,0 +1,83 @@ +#region Copyright & License Information +/* + * Copyright 2022 The OpenHV Developers (see AUTHORS) + * This file is part of OpenHV, 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.Linq; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [Desc("Manages bot defensive support power handling.")] + public class ExternalConditionPowerBotModuleInfo : ConditionalTraitInfo, Requires + { + [FieldLoader.Require] + [Desc("Which support power to use.")] + public readonly string OrderName = null; + + [Desc("How many friendlies should at least be affected?")] + public readonly int MinimumTargets = 4; + + public override object Create(ActorInitializer init) { return new ExternalConditionPowerBotModule(init.Self, this); } + } + + public class ExternalConditionPowerBotModule : ConditionalTrait, IBotRespondToAttack + { + readonly World world; + readonly Player player; + readonly ExternalConditionPowerBotModuleInfo info; + + SupportPowerManager supportPowerManager; + + public ExternalConditionPowerBotModule(Actor self, ExternalConditionPowerBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + this.info = info; + } + + protected override void Created(Actor self) + { + supportPowerManager = player.PlayerActor.Trait(); + } + + void IBotRespondToAttack.RespondToAttack(IBot bot, Actor self, AttackInfo e) + { + if (e.Attacker == null || e.Attacker.Disposed) + return; + + if (e.Attacker.Owner.RelationshipWith(self.Owner) != PlayerRelationship.Enemy) + return; + + foreach (var sp in supportPowerManager.Powers.Values) + { + if (sp.Disabled) + continue; + + if (info.OrderName != sp.Info.OrderName) + continue; + + if (!sp.Ready) + continue; + + var externalConditionPower = sp.Instances[0] as GrantExternalConditionPower; + var possibleTargets = externalConditionPower.UnitsInRange(self.Location); + if (possibleTargets.Any(p => !p.Owner.IsAlliedWith(player))) + continue; + + if (possibleTargets.Count() < info.MinimumTargets) + continue; + + bot.QueueOrder(new Order(sp.Key, supportPowerManager.Self, Target.FromCell(world, self.Location), false) { SuppressVisualFeedback = true }); + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/PowerDownBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/PowerDownBotModule.cs new file mode 100644 index 000000000000..ce48da2fc566 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/PowerDownBotModule.cs @@ -0,0 +1,191 @@ +#region Copyright & License Information +/* + * Copyright 2019-2023 The OpenHV Developers (see CREDITS) + * This file is part of OpenHV, 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.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [Desc("Manages AI temporary power shutdowns.")] + public class PowerDownBotModuleInfo : ConditionalTraitInfo + { + [Desc("Delay (in ticks) between toggling powerdown.")] + public readonly int Interval = 150; + + public override object Create(ActorInitializer init) { return new PowerDownBotModule(init.Self, this); } + } + + public class PowerDownBotModule : ConditionalTrait, IBotTick, IGameSaveTraitData + { + readonly World world; + readonly Player player; + + PowerManager playerPower; + int toggleTick; + + readonly Func isToggledBuildingsValid; + + // We keep a list to track toggled buildings for performance. + List toggledBuildings = new(); + + sealed class BuildingPowerWrapper + { + public int ExpectedPowerChanging; + public Actor Actor; + + public BuildingPowerWrapper(Actor actor, int powerChanging) + { + Actor = actor; + ExpectedPowerChanging = powerChanging; + } + } + + public PowerDownBotModule(Actor self, PowerDownBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + + isToggledBuildingsValid = 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); + } + + static int GetTogglePowerChanging(Actor actor) + { + var powerChangingIfToggled = 0; + var powerTraits = actor.TraitsImplementing().Where(t => !t.IsTraitDisabled).ToArray(); + if (powerTraits.Length > 0) + { + var powerMultipliers = actor.TraitsImplementing().ToArray(); + powerChangingIfToggled = powerTraits.Sum(p => p.Info.Amount) * (powerMultipliers.Sum(p => p.Info.Modifier) - 100) / 100; + if (powerMultipliers.Any(t => !t.IsTraitDisabled)) + powerChangingIfToggled = -powerChangingIfToggled; + } + + return powerChangingIfToggled; + } + + IEnumerable GetToggleableBuildings(IBot bot) + { + var toggleable = bot.Player.World.ActorsHavingTrait(t => !t.IsTraitDisabled && !t.IsTraitPaused) + .Where(a => a != null && !a.IsDead && a.Owner == player && a.Info.HasTraitInfo() && a.Info.HasTraitInfo() && a.Info.HasTraitInfo()); + + return toggleable; + } + + IEnumerable GetOnlineBuildings(IBot bot) + { + var toggleableBuildings = new List(); + + foreach (var actor in GetToggleableBuildings(bot)) + { + var powerChanging = GetTogglePowerChanging(actor); + if (powerChanging > 0) + toggleableBuildings.Add(new BuildingPowerWrapper(actor, 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 + if (power > 0) + { + toggledBuildings = toggledBuildings.Where(bpw => isToggledBuildingsValid(bpw.Actor)).OrderByDescending(bpw => bpw.ExpectedPowerChanging).ToList(); + for (var i = 0; i < toggledBuildings.Count; i++) + { + var building = toggledBuildings[i]; + if (power + building.ExpectedPowerChanging < 0) + continue; + + togglingBuildings.Add(building.Actor); + power += building.ExpectedPowerChanging; + toggledBuildings.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) + { + var buildingsCanBeOff = GetOnlineBuildings(bot); + foreach (var building in buildingsCanBeOff) + { + if (power > 0) + break; + + togglingBuildings.Add(building.Actor); + toggledBuildings.Add(new BuildingPowerWrapper(building.Actor, -building.ExpectedPowerChanging)); + power += building.ExpectedPowerChanging; + } + } + + if (togglingBuildings.Count > 0) + bot.QueueOrder(new Order("PowerDown", null, false, groupedActors: togglingBuildings.ToArray())); + + toggleTick = Info.Interval; + } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + var data = new List(); + foreach (var building in toggledBuildings.Where(bpw => isToggledBuildingsValid(bpw.Actor))) + data.Add(new MiniYamlNode(FieldSaver.FormatValue(building.Actor.ActorID), FieldSaver.FormatValue(building.ExpectedPowerChanging))); + + return new List() + { + new("ToggledBuildings", new MiniYaml("", data)) + }; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var toggledBuildingsNode = data.NodeWithKeyOrDefault("ToggledBuildings"); + if (toggledBuildingsNode != null) + { + foreach (var node in toggledBuildingsNode.Value.Nodes) + { + var actor = self.World.GetActorById(FieldLoader.GetValue(node.Key, node.Key)); + if (isToggledBuildingsValid(actor)) + toggledBuildings.Add(new BuildingPowerWrapper(actor, FieldLoader.GetValue(node.Key, node.Value.Value))); + } + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/PriorityCaptureManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/PriorityCaptureManagerBotModule.cs new file mode 100644 index 000000000000..183c0c98c897 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/PriorityCaptureManagerBotModule.cs @@ -0,0 +1,261 @@ +#region Copyright & License Information +/* + * Copyright 2019-2022 The OpenHV Developers (see CREDITS) + * This file is part of OpenHV, 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; +using OpenRA.Mods.Common.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [Desc("Manages AI capturing logic.")] + public class PriorityCaptureManagerBotModuleInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [Desc("Actor types that can capture other actors (via `Captures`).")] + public readonly HashSet CapturingActorTypes = new(); + + [Desc("Percentage chance of trying a priority capture.")] + public readonly int PriorityCaptureChance = 75; + + [Desc("Actor types that should be priorizited to be captured.", + "Leave this empty to include all actors.")] + public readonly HashSet PriorityCapturableActorTypes = new(); + + [Desc("Actor types that can be targeted for capturing.", + "Leave this empty to include all actors.")] + public readonly HashSet CapturableActorTypes = new(); + + [Desc("Avoid enemy actors nearby when searching for capture opportunities. Should be somewhere near the max weapon range.")] + public readonly WDist EnemyAvoidanceRadius = WDist.FromCells(8); + + [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 PlayerRelationship CapturableStances = PlayerRelationship.Enemy | PlayerRelationship.Neutral; + + public override object Create(ActorInitializer init) { return new PriorityCaptureManagerBotModule(init.Self, this); } + } + + public class PriorityCaptureManagerBotModule : ConditionalTrait, IBotTick, IBotPositionsUpdated, IGameSaveTraitData + { + readonly World world; + readonly Player player; + readonly int maximumCaptureTargetOptions; + + int minCaptureDelayTicks; + CPos initialBaseCenter; + + public PriorityCaptureManagerBotModule(Actor self, PriorityCaptureManagerBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + + if (world.Type == WorldType.Editor) + return; + + maximumCaptureTargetOptions = Math.Max(1, Info.MaximumCaptureTargetOptions); + } + + void IBotPositionsUpdated.UpdatedBaseCenter(CPos newLocation) + { + initialBaseCenter = newLocation; + } + + void IBotPositionsUpdated.UpdatedDefenseCenter(CPos newLocation) { } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + minCaptureDelayTicks = world.LocalRandom.Next(Info.MinimumCaptureDelay); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minCaptureDelayTicks <= 0) + { + minCaptureDelayTicks = Info.MinimumCaptureDelay; + QueueCaptureOrders(bot); + } + } + + 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 (player.WinState != WinState.Undefined) + return; + + var newUnits = world.ActorsHavingTrait() + .Where(a => a.Owner == player && !a.IsDead && a.IsInWorld); + + if (!newUnits.Any()) + return; + + 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); + + if (!capturers.Any()) + return; + + var baseCenter = world.Map.CenterOfCell(initialBaseCenter); + + if (world.LocalRandom.Next(100) < Info.PriorityCaptureChance) + { + var priorityTargets = world.Actors.Where(a => + !a.IsDead && a.IsInWorld && Info.CapturableStances.HasRelationship(player.RelationshipWith(a.Owner)) + && Info.PriorityCapturableActorTypes.Contains(a.Info.Name.ToLowerInvariant())); + + if (Info.CheckCaptureTargetsForVisibility) + priorityTargets = priorityTargets.Where(a => a.CanBeViewedByPlayer(player)); + + if (priorityTargets.Any()) + { + priorityTargets = priorityTargets.OrderBy(a => (a.CenterPosition - baseCenter).LengthSquared); + + var priorityCaptures = Math.Min(capturers.Count(), priorityTargets.Count()); + + for (var i = 0; i < priorityCaptures; i++) + { + var capturer = capturers.First(); + + var priorityTarget = priorityTargets.First(); + + var captureManager = priorityTarget.TraitOrDefault(); + if (captureManager != null && capturer.Trait.CanTarget(captureManager)) + { + var safeTarget = SafePath(capturer.Actor, priorityTarget); + if (safeTarget.Type == TargetType.Invalid) + { + priorityTargets = priorityTargets.Skip(1); + capturers = capturers.Skip(1); + continue; + } + + bot.QueueOrder(new Order("CaptureActor", capturer.Actor, safeTarget, true)); + AIUtils.BotDebug($"{player}: Ordered {capturer.Actor} {capturer.Actor.ActorID} to capture {priorityTarget} {priorityTarget.ActorID} in priority mode."); + } + + priorityTargets = priorityTargets.Skip(1); + capturers = capturers.Skip(1); + } + + if (!capturers.Any()) + return; + } + } + + var randomPlayer = world.Players.Where(p => !p.Spectating + && Info.CapturableStances.HasRelationship(player.RelationshipWith(p))).Random(world.LocalRandom); + + var targetOptions = Info.CheckCaptureTargetsForVisibility + ? GetVisibleActorsBelongingToPlayer(randomPlayer) + : GetActorsThatCanBeOrderedByPlayer(randomPlayer); + + var capturableTargetOptions = targetOptions + .Where(target => + { + var captureManager = target.TraitOrDefault(); + if (captureManager == null) + return false; + + return capturers.Any(tp => tp.Trait.CanTarget(captureManager)); + }) + .OrderBy(target => (target.CenterPosition - baseCenter).LengthSquared) + .Take(maximumCaptureTargetOptions); + + if (Info.CapturableActorTypes.Count > 0) + capturableTargetOptions = capturableTargetOptions.Where(target => Info.CapturableActorTypes.Contains(target.Info.Name.ToLowerInvariant())); + + if (!capturableTargetOptions.Any()) + return; + + foreach (var capturer in capturers) + { + var nearestTargetActors = capturableTargetOptions.OrderBy(target => (target.CenterPosition - capturer.Actor.CenterPosition).LengthSquared); + foreach (var nearestTargetActor in nearestTargetActors) + { + var attack = nearestTargetActor.TraitOrDefault(); + if (attack != null && attack.HasAnyValidWeapons(Target.FromActor(capturer.Actor))) + continue; + + var safeTarget = SafePath(capturer.Actor, nearestTargetActor); + if (safeTarget.Type == TargetType.Invalid) + continue; + + bot.QueueOrder(new Order("CaptureActor", capturer.Actor, safeTarget, true)); + AIUtils.BotDebug($"{capturer.Actor.Owner}: Ordered {capturer.Actor} to capture {nearestTargetActor}"); + break; + } + } + } + + Target SafePath(Actor capturer, Actor target) + { + var mobile = capturer.Trait(); + var path = mobile.PathFinder.FindPathToTargetCell(capturer, new[] { mobile.ToCell }, target.Location, BlockedByActor.Stationary, + location => world.FindActorsInCircle(world.Map.CenterOfCell(location), Info.EnemyAvoidanceRadius) + .Where(u => !u.IsDead && capturer.Owner.RelationshipWith(u.Owner) == PlayerRelationship.Enemy && capturer.IsTargetableBy(u)) + .Sum(u => Math.Max(WDist.Zero.Length, Info.EnemyAvoidanceRadius.Length - (world.Map.CenterOfCell(location) - u.CenterPosition).Length))); + + if (path.Count == 0) + return Target.Invalid; + + return Target.FromActor(target); + } + + List IGameSaveTraitData.IssueTraitData(Actor self) + { + if (IsTraitDisabled) + return null; + + return new List() + { + new("InitialBaseCenter", FieldSaver.FormatValue(initialBaseCenter)) + }; + } + + void IGameSaveTraitData.ResolveTraitData(Actor self, MiniYaml data) + { + if (self.World.IsReplay) + return; + + var initialBaseCenterNode = data.NodeWithKeyOrDefault("InitialBaseCenter"); + if (initialBaseCenterNode != null) + initialBaseCenter = FieldLoader.GetValue("InitialBaseCenter", initialBaseCenterNode.Value.Value); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/ScoutBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/ScoutBotModule.cs new file mode 100644 index 000000000000..3447b67c385c --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/ScoutBotModule.cs @@ -0,0 +1,101 @@ +#region Copyright & License Information +/* + * Copyright 2019-2021 The OpenHV Developers (see CREDITS) + * This file is part of OpenHV, 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.Traits; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [Desc("Manages AI random scouting. Exclude from `SquadManagerBotModule`.")] + public class ScoutBotModuleInfo : ConditionalTraitInfo + { + [FieldLoader.Require] + [Desc("Actor types that are sent around the map.")] + public readonly HashSet ScoutActorTypes = new(); + + [Desc("Minimum delay (in ticks) between searching for ScoutActorTypes.")] + public readonly int MinimumScanDelay = 200; + + [Desc("How far away to move from current location.")] + public readonly int MoveRadius = 1; + + public override object Create(ActorInitializer init) { return new ScoutBotModule(init.Self, this); } + } + + public class ScoutBotModule : ConditionalTrait, IBotTick + { + readonly List scouts = new(); + + readonly World world; + readonly Player player; + readonly ScoutBotModuleInfo info; + + readonly Func unitCannotBeOrdered; + + int scanForIdleScoutsTicks; + + public ScoutBotModule(Actor self, ScoutBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + this.info = info; + + unitCannotBeOrdered = a => a.Owner != self.Owner || a.IsDead || !a.IsInWorld; + } + + protected override void TraitEnabled(Actor self) + { + // PERF: Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + scanForIdleScoutsTicks = world.LocalRandom.Next(0, Info.MinimumScanDelay); + } + + void IBotTick.BotTick(IBot bot) + { + if (--scanForIdleScoutsTicks > 0) + return; + + scanForIdleScoutsTicks = Info.MinimumScanDelay; + + var toRemove = scouts.Where(unitCannotBeOrdered).ToList(); + foreach (var a in toRemove) + scouts.Remove(a); + + // TODO: Look for a more performance friendly way to update this list + var newScouts = world.Actors.Where(a => Info.ScoutActorTypes.Contains(a.Info.Name) && a.Owner == player && !scouts.Contains(a)); + scouts.AddRange(newScouts); + + foreach (var scout in scouts) + { + var target = PickTargetLocation(scout); + if (target.Type == TargetType.Invalid) + continue; + + bot.QueueOrder(new Order("Move", scout, target, false)); + } + } + + Target PickTargetLocation(Actor scout) + { + var targetPosition = scout.CenterPosition + new WVec(0, -1024 * info.MoveRadius, 0).Rotate(WRot.FromFacing(world.LocalRandom.Next(255))); + var targetCell = world.Map.CellContaining(targetPosition); + + if (!world.Map.Contains(targetCell)) + return Target.Invalid; + + var target = Target.FromCell(world, targetCell); + return target; + } + } +} diff --git a/OpenRA.Mods.Common/Traits/BotModules/SendUnitToAttackBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/SendUnitToAttackBotModule.cs new file mode 100644 index 000000000000..6a7007ce60b1 --- /dev/null +++ b/OpenRA.Mods.Common/Traits/BotModules/SendUnitToAttackBotModule.cs @@ -0,0 +1,185 @@ +#region Copyright & License Information +/* + * Copyright 2007-2022 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; +using OpenRA.Mods.Common.Activities; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.HV.Traits +{ + [Flags] + public enum AttackDistance + { + Closest = 0, + Furthest = 1, + Random = 2 + } + + [TraitLocation(SystemActors.Player)] + [Desc("Bot logic for units that should not be sent with a regular squad, like suicide or subterranean units.")] + public class SendUnitToAttackBotModuleInfo : ConditionalTraitInfo + { + [Desc("Actors used for attack, and their base desire provided for attack desire.", + "When desire reach 100, AI will send them to attack.")] + public readonly Dictionary ActorTypesAndAttackDesire = default; + + [Desc("Target types that can be targeted.")] + public readonly BitSet ValidTargets = new("Structure"); + + [Desc("Target types that can't be targeted.")] + public readonly BitSet InvalidTargets; + + [Desc("Should attack the furthest or closest target. Possible values are Closest, Furthest, Random")] + public readonly AttackDistance AttackDistance = AttackDistance.Closest; + + [Desc("Attack order name.")] + public readonly string AttackOrderName = "Attack"; + + [Desc("Find target and try attack target in this interval.")] + public readonly int ScanTick = 463; + + [Desc("The total attack desire increases by this amount per scan", + "Note: When there is no attack unit, the total attack desire will return to 0.")] + public readonly int AttackDesireIncreasedPerScan = 10; + + public override object Create(ActorInitializer init) { return new SendUnitToAttackBotModule(init.Self, this); } + } + + public class SendUnitToAttackBotModule : ConditionalTrait, IBotTick + { + readonly World world; + readonly Player player; + readonly Predicate unitCannotBeOrdered; + readonly Predicate unitCannotBeOrderedOrIsBusy; + readonly Predicate isInvalidActor; + int minAssignRoleDelayTicks; + Player targetPlayer; + int desireIncreased; + + public SendUnitToAttackBotModule(Actor self, SendUnitToAttackBotModuleInfo info) + : base(info) + { + world = self.World; + player = self.Owner; + isInvalidActor = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != targetPlayer; + unitCannotBeOrdered = a => a == null || a.IsDead || !a.IsInWorld || a.Owner != player; + unitCannotBeOrderedOrIsBusy = a => unitCannotBeOrdered(a) || (!a.IsIdle && a.CurrentActivity is not FlyIdle); + desireIncreased = 0; + } + + protected override void TraitEnabled(Actor self) + { + // Avoid all AIs reevaluating assignments on the same tick, randomize their initial evaluation delay. + minAssignRoleDelayTicks = world.LocalRandom.Next(0, Info.ScanTick); + } + + void IBotTick.BotTick(IBot bot) + { + if (--minAssignRoleDelayTicks <= 0) + { + minAssignRoleDelayTicks = Info.ScanTick; + + var attackdesire = 0; + var actors = world.ActorsWithTrait().Select(at => at.Actor).Where(a => + { + if (Info.ActorTypesAndAttackDesire.ContainsKey(a.Info.Name) && !unitCannotBeOrderedOrIsBusy(a)) + { + attackdesire += Info.ActorTypesAndAttackDesire[a.Info.Name]; + return true; + } + + return false; + }).ToList(); + + if (actors.Count == 0) + desireIncreased = 0; + else + desireIncreased += Info.AttackDesireIncreasedPerScan; + + if (desireIncreased + attackdesire < 100) + return; + + // Randomly choose enemy player to attack + var enemyPlayers = world.Players.Where(p => p.RelationshipWith(player) == PlayerRelationship.Enemy && p.WinState != WinState.Lost).ToList(); + if (enemyPlayers.Count == 0) + return; + + targetPlayer = enemyPlayers.Random(world.LocalRandom); + + var targets = world.Actors.Where(a => + { + if (isInvalidActor(a)) + return false; + + var t = a.GetAllTargetTypes(); + + if (!Info.ValidTargets.Overlaps(t) || Info.InvalidTargets.Overlaps(t)) + return false; + + var hasModifier = false; + var visModifiers = a.TraitsImplementing(); + foreach (var v in visModifiers) + { + if (v.IsVisible(a, player)) + return true; + + hasModifier = true; + } + + return !hasModifier; + }); + + switch (Info.AttackDistance) + { + case AttackDistance.Closest: + targets = targets.OrderBy(a => (a.CenterPosition - actors[0].CenterPosition).HorizontalLengthSquared); + break; + case AttackDistance.Furthest: + targets = targets.OrderByDescending(a => (a.CenterPosition - actors[0].CenterPosition).HorizontalLengthSquared); + break; + case AttackDistance.Random: + targets = targets.Shuffle(world.LocalRandom); + break; + } + + foreach (var t in targets) + { + var orderedActors = new List(); + + foreach (var a in actors) + { + if (!a.Info.HasTraitInfo()) + { + var mobile = a.TraitOrDefault(); + if (mobile == null || !mobile.PathFinder.PathExistsForLocomotor(mobile.Locomotor, a.Location, t.Location)) + continue; + } + + orderedActors.Add(a); + } + + actors.RemoveAll(a => orderedActors.Contains(a)); + + if (orderedActors.Count > 0) + bot.QueueOrder(new Order(Info.AttackOrderName, null, Target.FromActor(t), false, groupedActors: orderedActors.ToArray())); + + if (actors.Count == 0) + break; + } + } + } + } +}