diff --git a/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs index b85fc33972..6793bedda0 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs @@ -153,10 +153,13 @@ namespace OpenRA.Mods.Common.Traits return false; var targetTypes = a.GetEnabledTargetTypes(); - return !targetTypes.IsEmpty && !targetTypes.Overlaps(Info.IgnoredEnemyTargetTypes); + if (targetTypes.IsEmpty || targetTypes.Overlaps(Info.IgnoredEnemyTargetTypes)) + return false; + + return IsNotHiddenUnit(a); } - public bool IsNotHiddenUnit(Actor a) + bool IsNotHiddenUnit(Actor a) { var hasModifier = false; var visModifiers = a.TraitsImplementing(); @@ -244,7 +247,7 @@ namespace OpenRA.Mods.Common.Traits // Then check which are in weapons range of the source. var activeAttackBases = sourceActor.TraitsImplementing().Where(Exts.IsTraitEnabled).ToArray(); var enemiesAndSourceAttackRanges = actors - .Where(a => IsPreferredEnemyUnit(a) && IsNotHiddenUnit(a)) + .Where(IsPreferredEnemyUnit) .Select(a => (Actor: a, AttackBases: activeAttackBases.Where(ab => ab.HasAnyValidWeapons(Target.FromActor(a))).ToList())) .Where(x => x.AttackBases.Count > 0) .Select(x => (x.Actor, Range: x.AttackBases.Max(ab => ab.GetMaximumRangeVersusTarget(Target.FromActor(x.Actor))))) @@ -462,7 +465,7 @@ namespace OpenRA.Mods.Common.Traits var protectSq = GetSquadOfType(SquadType.Protection); protectSq ??= RegisterNewSquad(bot, SquadType.Protection, (attacker, WVec.Zero)); - if (protectSq.IsValid && !protectSq.IsTargetValid()) + if (protectSq.IsValid && !protectSq.IsTargetValid(protectSq.CenterUnit())) protectSq.SetActorToTarget((attacker, WVec.Zero)); if (!protectSq.IsValid) diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs index d7d9cae52d..1b5f632be6 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs @@ -95,12 +95,12 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads /// /// Checks the target is still valid, and updates the location if it is still valid. /// - public bool IsTargetValid() + public bool IsTargetValid(Actor squadUnit) { var valid = TargetActor != null && TargetActor.IsInWorld && - TargetActor.IsTargetableBy(Units.FirstOrDefault()) && + Units.Any(Target.IsValidFor) && !TargetActor.Info.HasTraitInfo(); if (!valid) return false; @@ -113,7 +113,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads // e.g. a ship targeting a land unit, but the land unit moved north. // We need to update our location to move north as well. // If we can reach the actor directly, we'll just target it directly. - var target = SquadManager.FindEnemies(new[] { TargetActor }, Units.First()).FirstOrDefault(); + var target = SquadManager.FindEnemies(new[] { TargetActor }, squadUnit).FirstOrDefault(); SetActorToTarget(target); return target.Actor != null; } @@ -122,7 +122,16 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads TargetActor != null && TargetActor.CanBeViewedByPlayer(Bot.Player); - public WPos CenterPosition { get { return Units.Select(u => u.CenterPosition).Average(); } } + public WPos CenterPosition() + { + return Units.Select(a => a.CenterPosition).Average(); + } + + public Actor CenterUnit() + { + var centerPosition = CenterPosition(); + return Units.MinByOrDefault(a => (a.CenterPosition - centerPosition).LengthSquared); + } public MiniYaml Serialize() { diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs index 6f4609e61e..a17668366d 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs @@ -147,10 +147,10 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid()) + var leader = owner.CenterUnit(); + if (!owner.IsTargetValid(leader)) { - var a = owner.Units.Random(owner.Random); - var closestEnemy = owner.SquadManager.FindClosestEnemy(a); + var closestEnemy = owner.SquadManager.FindClosestEnemy(leader); owner.SetActorToTarget(closestEnemy); if (closestEnemy.Actor == null) { diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs index dceb78972c..9f72c58f7c 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs @@ -17,21 +17,59 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads { abstract class GroundStateBase : StateBase { + Actor leader; + + /// + /// Elects a unit to lead the squad, other units in the squad will regroup to the leader if they start to spread out. + /// The leader remains the same unless a new one is forced or the leader is no longer part of the squad. + /// + protected Actor Leader(Squad owner) + { + if (leader == null || !owner.Units.Contains(leader)) + leader = NewLeader(owner); + return leader; + } + + static Actor NewLeader(Squad owner) + { + IEnumerable units = owner.Units; + + // Identify the Locomotor with the most restrictive passable terrain list. For squads with mixed + // locomotors, we hope to choose the most restrictive option. This means we won't nominate a leader who has + // more options. This avoids situations where we would nominate a hovercraft as the leader and tanks would + // fail to follow it because they can't go over water. By forcing us to choose a unit with limited movement + // options, we maximise the chance other units will be able to follow it. We could still be screwed if the + // squad has a mix of units with disparate movement, e.g. land units and naval units. We must trust the + // squad has been formed from a set of units that don't suffer this problem. + var leastCommonDenominator = units + .Select(a => a.TraitOrDefault()?.Locomotor) + .Where(l => l != null) + .MinByOrDefault(l => l.Info.TerrainSpeeds.Count) + ?.Info.TerrainSpeeds.Count; + if (leastCommonDenominator != null) + units = units.Where(a => a.TraitOrDefault()?.Locomotor.Info.TerrainSpeeds.Count == leastCommonDenominator).ToList(); + + // Choosing a unit in the center reduces the need for an immediate regroup. + var centerPosition = units.Select(a => a.CenterPosition).Average(); + return units.MinBy(a => (a.CenterPosition - centerPosition).LengthSquared); + } + protected virtual bool ShouldFlee(Squad owner) { return ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies)); } - protected static (Actor Actor, WVec Offset) FindClosestEnemy(Squad owner) + protected (Actor Actor, WVec Offset) NewLeaderAndFindClosestEnemy(Squad owner) { - return owner.SquadManager.FindClosestEnemy(owner.Units.First()); + leader = null; // Force a new leader to be elected, useful if we are targeting a new enemy. + return owner.SquadManager.FindClosestEnemy(Leader(owner)); } - protected static IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(Squad owner, IEnumerable actors) + protected IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(Squad owner, IEnumerable actors) { return owner.SquadManager.FindEnemies( actors, - owner.Units.First()); + Leader(owner)); } protected static Actor ClosestToEnemy(Squad owner) @@ -49,9 +87,9 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid()) + if (!owner.IsTargetValid(Leader(owner))) { - var closestEnemy = FindClosestEnemy(owner); + var closestEnemy = NewLeaderAndFindClosestEnemy(owner); owner.SetActorToTarget(closestEnemy); if (closestEnemy.Actor == null) return; @@ -93,9 +131,9 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid()) + if (!owner.IsTargetValid(Leader(owner))) { - var closestEnemy = FindClosestEnemy(owner); + var closestEnemy = NewLeaderAndFindClosestEnemy(owner); owner.SetActorToTarget(closestEnemy); if (closestEnemy.Actor == null) { @@ -104,10 +142,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads } } - var leader = ClosestToEnemy(owner); - if (leader == null) - return; - + var leader = Leader(owner); if (leader.Location != lastLeaderLocation) { lastLeaderLocation = leader.Location; @@ -130,7 +165,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads } var ownUnits = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.Units.Count) / 3) - .Where(a => a.Owner == owner.Units.First().Owner && owner.Units.Contains(a)).ToHashSet(); + .Where(owner.Units.Contains).ToHashSet(); if (ownUnits.Count < owner.Units.Count) { @@ -173,9 +208,9 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid()) + if (!owner.IsTargetValid(Leader(owner))) { - var closestEnemy = FindClosestEnemy(owner); + var closestEnemy = NewLeaderAndFindClosestEnemy(owner); owner.SetActorToTarget(closestEnemy); if (closestEnemy.Actor == null) { @@ -184,10 +219,10 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads } } - var leader = ClosestToEnemy(owner); - if (leader?.Location != lastLeaderLocation) + var leader = Leader(owner); + if (leader.Location != lastLeaderLocation) { - lastLeaderLocation = leader?.Location; + lastLeaderLocation = leader.Location; lastUpdatedTick = owner.World.WorldTick; } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs index f6433bff8d..9b4bd3c900 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs @@ -32,9 +32,10 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid()) + var leader = Leader(owner); + if (!owner.IsTargetValid(leader)) { - var target = owner.SquadManager.FindClosestEnemy(owner.Units.First(), WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius)); + var target = owner.SquadManager.FindClosestEnemy(leader, WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius)); owner.SetActorToTarget(target); if (target.Actor == null) { diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs index 3c5c23f20a..9fbd4608f8 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/StateBase.cs @@ -85,9 +85,8 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!squad.IsValid) return false; - var randomSquadUnit = squad.Units.Random(squad.Random); var dangerRadius = squad.SquadManager.Info.DangerScanRadius; - var units = squad.World.FindActorsInCircle(randomSquadUnit.CenterPosition, WDist.FromCells(dangerRadius)).ToList(); + var units = squad.World.FindActorsInCircle(squad.CenterPosition(), WDist.FromCells(dangerRadius)).ToList(); // If there are any own buildings within the DangerRadius, don't flee // PERF: Avoid LINQ