diff --git a/OpenRA.Game/Exts.cs b/OpenRA.Game/Exts.cs index 26c38fa702..3c613bec29 100644 --- a/OpenRA.Game/Exts.cs +++ b/OpenRA.Game/Exts.cs @@ -355,6 +355,11 @@ namespace OpenRA return number * 46341 / 32768; } + public static int MultiplyBySqrtTwoOverTwo(int number) + { + return (int)(number * 23170L / 32768L); + } + public static int IntegerDivisionRoundingAwayFromZero(int dividend, int divisor) { var quotient = Math.DivRem(dividend, divisor, out var remainder); diff --git a/OpenRA.Game/WorldUtils.cs b/OpenRA.Game/WorldUtils.cs index 604fcd1259..483b777c4b 100644 --- a/OpenRA.Game/WorldUtils.cs +++ b/OpenRA.Game/WorldUtils.cs @@ -19,19 +19,51 @@ namespace OpenRA { public static class WorldUtils { - public static Actor ClosestTo(this IEnumerable actors, Actor a) + /// + /// From the given , select the one nearest the given by + /// comparing their . No check is done to see if a path exists. + /// + public static Actor ClosestToIgnoringPath(this IEnumerable actors, Actor actor) { - return actors.ClosestTo(a.CenterPosition); + return actors.ClosestToIgnoringPath(actor.CenterPosition); } - public static Actor ClosestTo(this IEnumerable actors, WPos pos) + /// + /// From the given , select the one nearest the given by + /// comparing the . No check is done to see if a path exists. + /// + public static Actor ClosestToIgnoringPath(this IEnumerable actors, WPos position) { - return actors.MinByOrDefault(a => (a.CenterPosition - pos).LengthSquared); + return actors.MinByOrDefault(a => (a.CenterPosition - position).LengthSquared); } - public static WPos PositionClosestTo(this IEnumerable positions, WPos pos) + /// + /// From the given that can be projected to , + /// select the one nearest the given by + /// comparing their . No check is done to see if a path exists. + /// + public static T ClosestToIgnoringPath(IEnumerable items, Func selector, Actor actor) { - return positions.MinByOrDefault(p => (p - pos).LengthSquared); + return ClosestToIgnoringPath(items, selector, actor.CenterPosition); + } + + /// + /// From the given that can be projected to , + /// select the one nearest the given by + /// comparing the . No check is done to see if a path exists. + /// + public static T ClosestToIgnoringPath(IEnumerable items, Func selector, WPos position) + { + return items.MinByOrDefault(x => (selector(x).CenterPosition - position).LengthSquared); + } + + /// + /// From the given , select the one nearest the given . + /// No check is done to see if a path exists, as an actor is required for that. + /// + public static WPos ClosestToIgnoringPath(this IEnumerable positions, WPos position) + { + return positions.MinByOrDefault(p => (p - position).LengthSquared); } public static IEnumerable FindActorsInCircle(this World world, WPos origin, WDist r) diff --git a/OpenRA.Mods.Cnc/Projectiles/TeslaZap.cs b/OpenRA.Mods.Cnc/Projectiles/TeslaZap.cs index a2eb89352e..19d61d79fd 100644 --- a/OpenRA.Mods.Cnc/Projectiles/TeslaZap.cs +++ b/OpenRA.Mods.Cnc/Projectiles/TeslaZap.cs @@ -79,7 +79,7 @@ namespace OpenRA.Mods.Cnc.Projectiles // Zap tracks target if (info.TrackTarget && args.GuidedTarget.IsValidFor(args.SourceActor)) - target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.PositionClosestTo(args.Source); + target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(args.Source); if (damageDuration-- > 0) args.Weapon.Impact(Target.FromPos(target), new WarheadArgs(args)); diff --git a/OpenRA.Mods.Common/AIUtils.cs b/OpenRA.Mods.Common/AIUtils.cs index 09804b9330..ec90bed688 100644 --- a/OpenRA.Mods.Common/AIUtils.cs +++ b/OpenRA.Mods.Common/AIUtils.cs @@ -63,12 +63,6 @@ namespace OpenRA.Mods.Common .Count(a => a.Owner == owner && buildings.Contains(a.Info.Name)); } - public static List FindEnemiesByCommonName(HashSet commonNames, Player player) - { - return player.World.Actors.Where(a => !a.IsDead && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy && - commonNames.Contains(a.Info.Name)).ToList(); - } - public static ActorInfo GetInfoByCommonName(HashSet names, Player owner) { return owner.World.Map.Rules.Actors.Where(k => names.Contains(k.Key)).Random(owner.World.LocalRandom).Value; diff --git a/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs b/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs index cd0443b39e..924ae1ee56 100644 --- a/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs +++ b/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs @@ -47,7 +47,7 @@ namespace OpenRA.Mods.Common.Activities && a.Owner == self.Owner && rearmInfo.RearmActors.Contains(a.Info.Name) && (!unreservedOnly || Reservable.IsAvailableFor(a, self))) - .ClosestTo(self); + .ClosestToWithPathFrom(self); } bool ShouldLandAtBuilding(Actor self, Actor dest) diff --git a/OpenRA.Mods.Common/Activities/Enter.cs b/OpenRA.Mods.Common/Activities/Enter.cs index 925a9bed47..da84b55710 100644 --- a/OpenRA.Mods.Common/Activities/Enter.cs +++ b/OpenRA.Mods.Common/Activities/Enter.cs @@ -126,7 +126,7 @@ namespace OpenRA.Mods.Common.Activities case EnterState.Entering: { // Check that we reached the requested position - var targetPos = target.Positions.PositionClosestTo(self.CenterPosition); + var targetPos = target.Positions.ClosestToWithPathFrom(self); if (!IsCanceling && self.CenterPosition == targetPos && target.Type == TargetType.Actor) OnEnterComplete(self, target.Actor); diff --git a/OpenRA.Mods.Common/Activities/Hunt.cs b/OpenRA.Mods.Common/Activities/Hunt.cs index e4d02b8370..219e2fc3b2 100644 --- a/OpenRA.Mods.Common/Activities/Hunt.cs +++ b/OpenRA.Mods.Common/Activities/Hunt.cs @@ -36,7 +36,7 @@ namespace OpenRA.Mods.Common.Activities if (IsCanceling) return true; - var targetActor = targets.ClosestTo(self); + var targetActor = targets.ClosestToWithPathFrom(self); if (targetActor == null) return false; diff --git a/OpenRA.Mods.Common/Activities/LayMines.cs b/OpenRA.Mods.Common/Activities/LayMines.cs index e473b2efb8..fb5740af16 100644 --- a/OpenRA.Mods.Common/Activities/LayMines.cs +++ b/OpenRA.Mods.Common/Activities/LayMines.cs @@ -88,8 +88,11 @@ namespace OpenRA.Mods.Common.Activities if (rearmableInfo != null && ammoPools.Any(p => p.Info.Name == minelayer.Info.AmmoPoolName && !p.HasAmmo)) { // Rearm (and possibly repair) at rearm building, then back out here to refill the minefield some more - rearmTarget = self.World.Actors.Where(a => self.Owner.RelationshipWith(a.Owner) == PlayerRelationship.Ally && rearmableInfo.RearmActors.Contains(a.Info.Name)) - .ClosestTo(self); + rearmTarget = self.World.Actors + .Where(a => + self.Owner.RelationshipWith(a.Owner) == PlayerRelationship.Ally + && rearmableInfo.RearmActors.Contains(a.Info.Name)) + .ClosestToWithPathFrom(self); if (rearmTarget == null) return true; diff --git a/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs b/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs index d673f8203a..a3958e1854 100644 --- a/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs +++ b/OpenRA.Mods.Common/Activities/Move/LocalMoveIntoTarget.cs @@ -23,7 +23,7 @@ namespace OpenRA.Mods.Common.Activities readonly Target target; readonly Color? targetLineColor; readonly WDist targetMovementThreshold; - WPos targetStartPos; + WPos? targetStartPos; public LocalMoveIntoTarget(Actor self, in Target target, WDist targetMovementThreshold, Color? targetLineColor = null) { @@ -35,7 +35,7 @@ namespace OpenRA.Mods.Common.Activities protected override void OnFirstRun(Actor self) { - targetStartPos = target.Positions.PositionClosestTo(self.CenterPosition); + targetStartPos = target.Positions.ClosestToWithPathFrom(self); } public override bool Tick(Actor self) @@ -47,14 +47,17 @@ namespace OpenRA.Mods.Common.Activities return false; var currentPos = self.CenterPosition; - var targetPos = target.Positions.PositionClosestTo(currentPos); + var targetPos = target.Positions.ClosestToWithPathFrom(self); + + if (targetStartPos == null || targetPos == null) + return true; // Give up if the target has moved too far - if (targetMovementThreshold > WDist.Zero && (targetPos - targetStartPos).LengthSquared > targetMovementThreshold.LengthSquared) + if (targetMovementThreshold > WDist.Zero && (targetPos.Value - targetStartPos.Value).LengthSquared > targetMovementThreshold.LengthSquared) return true; // Turn if required - var delta = targetPos - currentPos; + var delta = targetPos.Value - currentPos; var facing = delta.HorizontalLengthSquared != 0 ? delta.Yaw : mobile.Facing; if (facing != mobile.Facing) { @@ -66,7 +69,7 @@ namespace OpenRA.Mods.Common.Activities var speed = mobile.MovementSpeedForCell(self.Location); if (delta.LengthSquared <= speed * speed) { - mobile.SetCenterPosition(self, targetPos); + mobile.SetCenterPosition(self, targetPos.Value); return true; } diff --git a/OpenRA.Mods.Common/Projectiles/AreaBeam.cs b/OpenRA.Mods.Common/Projectiles/AreaBeam.cs index 3576b7f9cb..32955c4b64 100644 --- a/OpenRA.Mods.Common/Projectiles/AreaBeam.cs +++ b/OpenRA.Mods.Common/Projectiles/AreaBeam.cs @@ -164,7 +164,7 @@ namespace OpenRA.Mods.Common.Projectiles if (args.GuidedTarget.IsValidFor(args.SourceActor)) { - var guidedTargetPos = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.PositionClosestTo(args.Source); + var guidedTargetPos = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(args.Source); var targetDistance = new WDist((guidedTargetPos - args.Source).Length); // Only continue tracking target if it's within weapon range + diff --git a/OpenRA.Mods.Common/Projectiles/LaserZap.cs b/OpenRA.Mods.Common/Projectiles/LaserZap.cs index 01d32ea4fc..5f578ece9b 100644 --- a/OpenRA.Mods.Common/Projectiles/LaserZap.cs +++ b/OpenRA.Mods.Common/Projectiles/LaserZap.cs @@ -155,7 +155,7 @@ namespace OpenRA.Mods.Common.Projectiles // Beam tracks target if (info.TrackTarget && args.GuidedTarget.IsValidFor(args.SourceActor)) - target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.PositionClosestTo(source); + target = args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(source); // Check for blocking actors if (info.Blockable && BlocksProjectiles.AnyBlockingActorsBetween(world, args.SourceActor.Owner, source, target, info.Width, out var blockedPos)) diff --git a/OpenRA.Mods.Common/Projectiles/Missile.cs b/OpenRA.Mods.Common/Projectiles/Missile.cs index 3e9cdbd341..cefe57b86e 100644 --- a/OpenRA.Mods.Common/Projectiles/Missile.cs +++ b/OpenRA.Mods.Common/Projectiles/Missile.cs @@ -849,7 +849,7 @@ namespace OpenRA.Mods.Common.Projectiles // Check if target position should be updated (actor visible & locked on) var newTarPos = targetPosition; if (args.GuidedTarget.IsValidFor(args.SourceActor) && lockOn) - newTarPos = (args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.PositionClosestTo(args.Source)) + newTarPos = (args.Weapon.TargetActorCenter ? args.GuidedTarget.CenterPosition : args.GuidedTarget.Positions.ClosestToIgnoringPath(args.Source)) + new WVec(WDist.Zero, WDist.Zero, info.AirburstAltitude); // Compute target's predicted velocity vector (assuming uniform circular motion) diff --git a/OpenRA.Mods.Common/Traits/Armament.cs b/OpenRA.Mods.Common/Traits/Armament.cs index cdc33bcc1a..e402375c50 100644 --- a/OpenRA.Mods.Common/Traits/Armament.cs +++ b/OpenRA.Mods.Common/Traits/Armament.cs @@ -282,7 +282,7 @@ namespace OpenRA.Mods.Common.Traits WAngle MuzzleFacing() => MuzzleOrientation(self, barrel).Yaw; var muzzleOrientation = WRot.FromYaw(MuzzleFacing()); - var passiveTarget = Weapon.TargetActorCenter ? target.CenterPosition : target.Positions.PositionClosestTo(MuzzlePosition()); + var passiveTarget = Weapon.TargetActorCenter ? target.CenterPosition : target.Positions.ClosestToIgnoringPath(MuzzlePosition()); var initialOffset = Weapon.FirstBurstTargetOffset; if (initialOffset != WVec.Zero) { diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs index 8e8bdee8d0..a96473eaf9 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs @@ -247,7 +247,7 @@ namespace OpenRA.Mods.Common.Traits public virtual WPos GetTargetPosition(WPos pos, in Target target) { - return HasAnyValidWeapons(target, true) ? target.CenterPosition : target.Positions.PositionClosestTo(pos); + return HasAnyValidWeapons(target, true) ? target.CenterPosition : target.Positions.ClosestToIgnoringPath(pos); } public WDist GetMinimumRange() diff --git a/OpenRA.Mods.Common/Traits/AutoCarryall.cs b/OpenRA.Mods.Common/Traits/AutoCarryall.cs index 66c54c6e6a..6449f5bb72 100644 --- a/OpenRA.Mods.Common/Traits/AutoCarryall.cs +++ b/OpenRA.Mods.Common/Traits/AutoCarryall.cs @@ -104,7 +104,7 @@ namespace OpenRA.Mods.Common.Traits var carriers = self.World.ActorsHavingTrait(c => !Busy(self) && c.EnableAutoCarry) .Where(a => a.Owner == self.Owner && a.IsInWorld); - return carriers.ClosestTo(candidateCargo) == self; + return carriers.ClosestToWithPathTo(candidateCargo) == self; } void FindCarryableForTransport(Actor self) diff --git a/OpenRA.Mods.Common/Traits/AutoCrusher.cs b/OpenRA.Mods.Common/Traits/AutoCrusher.cs index 4bf5818b38..aa07c4e6fd 100644 --- a/OpenRA.Mods.Common/Traits/AutoCrusher.cs +++ b/OpenRA.Mods.Common/Traits/AutoCrusher.cs @@ -63,7 +63,7 @@ namespace OpenRA.Mods.Common.Traits self.Location != a.Location && a.IsAtGroundLevel() && Info.TargetRelationships.HasRelationship(self.Owner.RelationshipWith(a.Owner)) && a.TraitsImplementing().Any(c => c.CrushableBy(a, self, Info.CrushClasses))) - .ClosestTo(self); // TODO: Make it use shortest pathfinding distance instead + .ClosestToWithPathFrom(self); // TODO: Make it use shortest pathfinding distance instead if (crushableActor == null) return; diff --git a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs index 4285432359..f91f20c44a 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/BotModuleLogic/BaseBuilderQueueManager.cs @@ -479,8 +479,9 @@ namespace OpenRA.Mods.Common.Traits case BuildingType.Defense: // Build near the closest enemy structure - var closestEnemy = world.ActorsHavingTrait().Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy) - .ClosestTo(world.Map.CenterOfCell(baseBuilder.DefenseCenter)); + var closestEnemy = world.ActorsHavingTrait() + .Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy) + .ClosestToIgnoringPath(world.Map.CenterOfCell(baseBuilder.DefenseCenter)); var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter; diff --git a/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs index 14a208429a..a45dc1dd1d 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/CaptureManagerBotModule.cs @@ -47,7 +47,6 @@ namespace OpenRA.Mods.Common.Traits { readonly World world; readonly Player player; - readonly Func isEnemyUnit; readonly Predicate unitCannotBeOrderedOrIsIdle; readonly int maximumCaptureTargetOptions; int minCaptureDelayTicks; @@ -64,11 +63,6 @@ namespace OpenRA.Mods.Common.Traits if (world.Type == WorldType.Editor) return; - isEnemyUnit = unit => - player.RelationshipWith(unit.Owner) == PlayerRelationship.Enemy - && !unit.Info.HasTraitInfo() - && unit.Info.HasTraitInfo(); - unitCannotBeOrderedOrIsIdle = a => a.Owner != player || a.IsDead || !a.IsInWorld || a.IsIdle; maximumCaptureTargetOptions = Math.Max(1, Info.MaximumCaptureTargetOptions); @@ -89,16 +83,6 @@ namespace OpenRA.Mods.Common.Traits } } - 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)) @@ -160,7 +144,7 @@ namespace OpenRA.Mods.Common.Traits foreach (var capturer in capturers) { - var targetActor = capturableTargetOptionsList.MinByOrDefault(target => (target.CenterPosition - capturer.Actor.CenterPosition).LengthSquared); + var targetActor = capturableTargetOptionsList.ClosestToWithPathFrom(capturer.Actor); if (targetActor == null) continue; diff --git a/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs index fee5de3f6a..e07a0e1f04 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/SquadManagerBotModule.cs @@ -118,7 +118,7 @@ namespace OpenRA.Mods.Common.Traits readonly List unitsHangingAroundTheBase = new(); // Units that the bot already knows about. Any unit not on this list needs to be given a role. - readonly List activeUnits = new(); + readonly HashSet activeUnits = new(); public List Squads = new(); @@ -195,22 +195,105 @@ namespace OpenRA.Mods.Common.Traits AssignRolesToIdleUnits(bot); } - internal Actor FindClosestEnemy(WPos pos) + internal static Actor ClosestTo(IEnumerable ownActors, Actor targetActor) { - var units = World.Actors.Where(IsPreferredEnemyUnit).ToList(); - return units.Where(IsNotHiddenUnit).ClosestTo(pos) ?? units.ClosestTo(pos); + // Return actors that can get within weapons range of the target. + // First, let's determine the max weapons range for each of the actors. + var target = Target.FromActor(targetActor); + var ownActorsAndTheirAttackRanges = ownActors + .Select(a => (Actor: a, AttackBases: a.TraitsImplementing().Where(Exts.IsTraitEnabled) + .Where(ab => ab.HasAnyValidWeapons(target)).ToList())) + .Where(x => x.AttackBases.Count > 0) + .Select(x => (x.Actor, Range: x.AttackBases.Max(ab => ab.GetMaximumRangeVersusTarget(target)))) + .ToDictionary(x => x.Actor, x => x.Range); + + // Now determine if each actor can either path directly to the target, + // or if it can path to a nearby location at the edge of its weapon range to the target + // A thorough check would check each position within the circle, but for performance + // we'll only check 8 positions around the edge of the circle. + // We need to account for the weapons range here to account for units such as boats. + // They can't path directly to a land target, + // but might be able to get close enough to shore to attack the target from range. + return ownActorsAndTheirAttackRanges.Keys + .ClosestToWithPathToAny(targetActor.World, a => + { + var range = ownActorsAndTheirAttackRanges[a].Length; + var rangeDiag = Exts.MultiplyBySqrtTwoOverTwo(range); + return new[] + { + targetActor.CenterPosition, + targetActor.CenterPosition + new WVec(range, 0, 0), + targetActor.CenterPosition + new WVec(-range, 0, 0), + targetActor.CenterPosition + new WVec(0, range, 0), + targetActor.CenterPosition + new WVec(0, -range, 0), + targetActor.CenterPosition + new WVec(rangeDiag, rangeDiag, 0), + targetActor.CenterPosition + new WVec(-rangeDiag, rangeDiag, 0), + targetActor.CenterPosition + new WVec(-rangeDiag, -rangeDiag, 0), + targetActor.CenterPosition + new WVec(rangeDiag, -rangeDiag, 0), + }; + }); } - internal Actor FindClosestEnemy(WPos pos, WDist radius) + internal IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(IEnumerable actors, Actor sourceActor) { - return World.FindActorsInCircle(pos, radius).Where(a => IsPreferredEnemyUnit(a) && IsNotHiddenUnit(a)).ClosestTo(pos); + // Check units are in fact enemies and not hidden. + // 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)) + .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))))) + .ToDictionary(x => x.Actor, x => x.Range); + + // Now determine if the source actor can path directly to the target, + // or if it can path to a nearby location at the edge of its weapon range to the target + // A thorough check would check each position within the circle, but for performance + // we'll only check 8 positions around the edge of the circle. + // We need to account for the weapons range here to account for units such as boats. + // They can't path directly to a land target, + // but might be able to get close enough to shore to attack the target from range. + return enemiesAndSourceAttackRanges.Keys + .WithPathFrom(sourceActor, a => + { + var range = enemiesAndSourceAttackRanges[a].Length; + var rangeDiag = Exts.MultiplyBySqrtTwoOverTwo(range); + return new[] + { + WVec.Zero, + new WVec(range, 0, 0), + new WVec(-range, 0, 0), + new WVec(0, range, 0), + new WVec(0, -range, 0), + new WVec(rangeDiag, rangeDiag, 0), + new WVec(-rangeDiag, rangeDiag, 0), + new WVec(-rangeDiag, -rangeDiag, 0), + new WVec(rangeDiag, -rangeDiag, 0), + }; + }) + .Select(x => (x.Actor, x.ReachableOffsets.MinBy(o => o.LengthSquared))); + } + + internal (Actor Actor, WVec Offset) FindClosestEnemy(Actor sourceActor) + { + return FindClosestEnemy(World.Actors, sourceActor); + } + + internal (Actor Actor, WVec Offset) FindClosestEnemy(Actor sourceActor, WDist radius) + { + return FindClosestEnemy(World.FindActorsInCircle(sourceActor.CenterPosition, radius), sourceActor); + } + + (Actor Actor, WVec Offset) FindClosestEnemy(IEnumerable actors, Actor sourceActor) + { + return WorldUtils.ClosestToIgnoringPath(FindEnemies(actors, sourceActor), x => x.Actor, sourceActor); } void CleanSquads() { - Squads.RemoveAll(s => !s.IsValid); foreach (var s in Squads) - s.Units.RemoveAll(unitCannotBeOrdered); + s.Units.RemoveWhere(unitCannotBeOrdered); + Squads.RemoveAll(s => !s.IsValid); } // HACK: Use of this function requires that there is one squad of this type. @@ -219,18 +302,28 @@ namespace OpenRA.Mods.Common.Traits return Squads.FirstOrDefault(s => s.Type == type); } - Squad RegisterNewSquad(IBot bot, SquadType type, Actor target = null) + Squad RegisterNewSquad(IBot bot, SquadType type, (Actor Actor, WVec Offset) target = default) { var ret = new Squad(bot, this, type, target); Squads.Add(ret); return ret; } + internal void UnregisterSquad(Squad squad) + { + activeUnits.ExceptWith(squad.Units); + squad.Units.Clear(); + + // CleanSquads will remove the squad from the Squads list. + // We can't do that here as this is designed to be called from within Squad.Update + // and thus would mutate the Squads list we are iterating over. + } + void AssignRolesToIdleUnits(IBot bot) { CleanSquads(); - activeUnits.RemoveAll(unitCannotBeOrdered); + activeUnits.RemoveWhere(unitCannotBeOrdered); unitsHangingAroundTheBase.RemoveAll(unitCannotBeOrdered); foreach (var n in notifyIdleBaseUnits) n.UpdatedIdleBaseUnits(unitsHangingAroundTheBase); @@ -305,8 +398,7 @@ namespace OpenRA.Mods.Common.Traits { var attackForce = RegisterNewSquad(bot, SquadType.Assault); - foreach (var a in unitsHangingAroundTheBase) - attackForce.Units.Add(a); + attackForce.Units.UnionWith(unitsHangingAroundTheBase); unitsHangingAroundTheBase.Clear(); foreach (var n in notifyIdleBaseUnits) @@ -316,29 +408,45 @@ namespace OpenRA.Mods.Common.Traits void TryToRushAttack(IBot bot) { - var allEnemyBaseBuilder = AIUtils.FindEnemiesByCommonName(Info.ConstructionYardTypes, Player); - var ownUnits = activeUnits - .Where(unit => unit.IsIdle && unit.Info.HasTraitInfo() - && !Info.AirUnitsTypes.Contains(unit.Info.Name) && !Info.NavalUnitsTypes.Contains(unit.Info.Name) && !Info.ExcludeFromSquadsTypes.Contains(unit.Info.Name)).ToList(); + .Where(unit => + unit.IsIdle + && unit.Info.HasTraitInfo() + && !Info.AirUnitsTypes.Contains(unit.Info.Name) + && !Info.NavalUnitsTypes.Contains(unit.Info.Name) + && !Info.ExcludeFromSquadsTypes.Contains(unit.Info.Name)) + .ToList(); + + if (ownUnits.Count < Info.SquadSize) + return; + + var allEnemyBaseBuilder = FindEnemies( + World.Actors.Where(a => Info.ConstructionYardTypes.Contains(a.Info.Name)), + ownUnits.First()) + .ToList(); if (allEnemyBaseBuilder.Count == 0 || ownUnits.Count < Info.SquadSize) return; - foreach (var b in allEnemyBaseBuilder) + foreach (var enemyBaseBuilder in allEnemyBaseBuilder) { // Don't rush enemy aircraft! - var enemies = World.FindActorsInCircle(b.CenterPosition, WDist.FromCells(Info.RushAttackScanRadius)) - .Where(unit => IsPreferredEnemyUnit(unit) && unit.Info.HasTraitInfo() && !Info.AirUnitsTypes.Contains(unit.Info.Name) && !Info.NavalUnitsTypes.Contains(unit.Info.Name)).ToList(); + var enemies = FindEnemies( + World.FindActorsInCircle(enemyBaseBuilder.Actor.CenterPosition, WDist.FromCells(Info.RushAttackScanRadius)) + .Where(unit => + unit.Info.HasTraitInfo() + && !Info.AirUnitsTypes.Contains(unit.Info.Name) + && !Info.NavalUnitsTypes.Contains(unit.Info.Name)), + ownUnits.First()) + .ToList(); - if (AttackOrFleeFuzzy.Rush.CanAttack(ownUnits, enemies)) + if (AttackOrFleeFuzzy.Rush.CanAttack(ownUnits, enemies.Select(x => x.Actor).ToList())) { - var target = enemies.Count > 0 ? enemies.Random(World.LocalRandom) : b; + var target = enemies.Count > 0 ? enemies.Random(World.LocalRandom) : enemyBaseBuilder; var rush = GetSquadOfType(SquadType.Rush); rush ??= RegisterNewSquad(bot, SquadType.Rush, target); - foreach (var a3 in ownUnits) - rush.Units.Add(a3); + rush.Units.UnionWith(ownUnits); return; } @@ -348,18 +456,21 @@ namespace OpenRA.Mods.Common.Traits void ProtectOwn(IBot bot, Actor attacker) { var protectSq = GetSquadOfType(SquadType.Protection); - protectSq ??= RegisterNewSquad(bot, SquadType.Protection, attacker); + protectSq ??= RegisterNewSquad(bot, SquadType.Protection, (attacker, WVec.Zero)); - if (!protectSq.IsTargetValid) - protectSq.TargetActor = attacker; + if (protectSq.IsValid && !protectSq.IsTargetValid()) + protectSq.SetActorToTarget((attacker, WVec.Zero)); if (!protectSq.IsValid) { var ownUnits = World.FindActorsInCircle(World.Map.CenterOfCell(GetRandomBaseCenter()), WDist.FromCells(Info.ProtectUnitScanRadius)) - .Where(unit => unit.Owner == Player && !Info.ProtectionTypes.Contains(unit.Info.Name) && unit.Info.HasTraitInfo()); + .Where(unit => + unit.Owner == Player + && !Info.ProtectionTypes.Contains(unit.Info.Name) + && unit.Info.HasTraitInfo()) + .WithPathTo(World, attacker.CenterPosition); - foreach (var a in ownUnits) - protectSq.Units.Add(a); + protectSq.Units.UnionWith(ownUnits); } } @@ -429,7 +540,7 @@ namespace OpenRA.Mods.Common.Traits if (activeUnitsNode != null) { activeUnits.Clear(); - activeUnits.AddRange(FieldLoader.GetValue("ActiveUnits", activeUnitsNode.Value.Value) + activeUnits.UnionWith(FieldLoader.GetValue("ActiveUnits", activeUnitsNode.Value.Value) .Select(a => self.World.GetActorById(a)).Where(a => a != null)); } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs index fa7f55b701..d7d9cae52d 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/Squad.cs @@ -20,34 +20,44 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads public class Squad { - public List Units = new(); + public HashSet Units = new(); public SquadType Type; internal IBot Bot; internal World World; internal SquadManagerBotModule SquadManager; internal MersenneTwister Random; - - internal Target Target; internal StateMachine FuzzyStateMachine; - public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type) - : this(bot, squadManager, type, null) { } + /// + /// Target location to attack. This will be either the targeted actor, + /// or a position close to that actor sufficient to get within weapons range. + /// + internal Target Target { get; set; } - public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, Actor target) + /// + /// Actor that is targeted, for any actor based checks. Use for a targeting location. + /// + internal Actor TargetActor; + + public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type) + : this(bot, squadManager, type, default) { } + + public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, (Actor Actor, WVec Offset) target) { Bot = bot; SquadManager = squadManager; World = bot.Player.PlayerActor.World; Random = World.LocalRandom; Type = type; - Target = Target.FromActor(target); + SetActorToTarget(target); FuzzyStateMachine = new StateMachine(); switch (type) { case SquadType.Assault: case SquadType.Rush: + case SquadType.Naval: FuzzyStateMachine.ChangeState(this, new GroundUnitsIdleState(), true); break; case SquadType.Air: @@ -56,9 +66,6 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads case SquadType.Protection: FuzzyStateMachine.ChangeState(this, new UnitsForProtectionIdleState(), true); break; - case SquadType.Naval: - FuzzyStateMachine.ChangeState(this, new NavyUnitsIdleState(), true); - break; } } @@ -70,15 +77,50 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads public bool IsValid => Units.Count > 0; - public Actor TargetActor + public void SetActorToTarget((Actor Actor, WVec Offset) target) { - get => Target.Actor; - set => Target = Target.FromActor(value); + TargetActor = target.Actor; + if (TargetActor == null) + { + Target = Target.Invalid; + return; + } + + if (target.Offset == WVec.Zero) + Target = Target.FromActor(TargetActor); + else + Target = Target.FromPos(TargetActor.CenterPosition + target.Offset); } - public bool IsTargetValid => Target.IsValidFor(Units.FirstOrDefault()) && !Target.Actor.Info.HasTraitInfo(); + /// + /// Checks the target is still valid, and updates the location if it is still valid. + /// + public bool IsTargetValid() + { + var valid = + TargetActor != null && + TargetActor.IsInWorld && + TargetActor.IsTargetableBy(Units.FirstOrDefault()) && + !TargetActor.Info.HasTraitInfo(); + if (!valid) + return false; - public bool IsTargetVisible => TargetActor.CanBeViewedByPlayer(Bot.Player); + // Refresh the target location. + // If the actor moved out of reach then we'll mark it invalid. + // e.g. a ship targeting a land unit that moves inland out of weapons range. + // or the target crossed a bridge which is then destroyed. + // If it is still in range but we have to target a nearby location, we can update that location. + // 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(); + SetActorToTarget(target); + return target.Actor != null; + } + + public bool IsTargetVisible => + TargetActor != null && + TargetActor.CanBeViewedByPlayer(Bot.Player); public WPos CenterPosition { get { return Units.Select(u => u.CenterPosition).Average(); } } @@ -87,10 +129,14 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads var nodes = new List() { new MiniYamlNode("Type", FieldSaver.FormatValue(Type)), - new MiniYamlNode("Units", FieldSaver.FormatValue(Units.Select(a => a.ActorID).ToArray())), + new MiniYamlNode("Units", FieldSaver.FormatValue(Units.Select(a => a.ActorID).ToArray())) }; - if (Target.Type == TargetType.Actor) - nodes.Add(new MiniYamlNode("Target", FieldSaver.FormatValue(Target.Actor.ActorID))); + + if (Target != Target.Invalid) + { + nodes.Add(new MiniYamlNode("ActorToTarget", FieldSaver.FormatValue(TargetActor.ActorID))); + nodes.Add(new MiniYamlNode("TargetOffset", FieldSaver.FormatValue(Target.CenterPosition - TargetActor.CenterPosition))); + } return new MiniYaml("", nodes); } @@ -98,21 +144,26 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads public static Squad Deserialize(IBot bot, SquadManagerBotModule squadManager, MiniYaml yaml) { var type = SquadType.Rush; - Actor targetActor = null; + var target = ((Actor)null, WVec.Zero); var typeNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Type"); if (typeNode != null) type = FieldLoader.GetValue("Type", typeNode.Value.Value); - var targetNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Target"); - if (targetNode != null) - targetActor = squadManager.World.GetActorById(FieldLoader.GetValue("ActiveUnits", targetNode.Value.Value)); + var actorToTargetNode = yaml.Nodes.FirstOrDefault(n => n.Key == "ActorToTarget"); + var targetOffsetNode = yaml.Nodes.FirstOrDefault(n => n.Key == "TargetOffset"); + if (actorToTargetNode != null && targetOffsetNode != null) + { + var actorToTarget = squadManager.World.GetActorById(FieldLoader.GetValue("ActorToTarget", actorToTargetNode.Value.Value)); + var targetOffset = FieldLoader.GetValue("TargetOffset", targetOffsetNode.Value.Value); + target = (actorToTarget, targetOffset); + } - var squad = new Squad(bot, squadManager, type, targetActor); + var squad = new Squad(bot, squadManager, type, target); var unitsNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Units"); if (unitsNode != null) - squad.Units.AddRange(FieldLoader.GetValue("Units", unitsNode.Value.Value) + squad.Units.UnionWith(FieldLoader.GetValue("Units", unitsNode.Value.Value) .Select(a => squadManager.World.GetActorById(a))); return squad; diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs index b23423c219..6819d1232f 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/AirStates.cs @@ -134,7 +134,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (e == null) return; - owner.TargetActor = e; + owner.SetActorToTarget((e, WVec.Zero)); owner.FuzzyStateMachine.ChangeState(owner, new AirAttackState(), true); } @@ -150,20 +150,19 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { var a = owner.Units.Random(owner.Random); - var closestEnemy = owner.SquadManager.FindClosestEnemy(a.CenterPosition); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else + var closestEnemy = owner.SquadManager.FindClosestEnemy(a); + owner.SetActorToTarget(closestEnemy); + if (closestEnemy.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); return; } } - if (!NearToPosSafely(owner, owner.TargetActor.CenterPosition)) + if (!NearToPosSafely(owner, owner.Target.CenterPosition)) { owner.FuzzyStateMachine.ChangeState(owner, new AirFleeState(), true); return; @@ -188,7 +187,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads } if (CanAttackTarget(a, owner.TargetActor)) - owner.Bot.QueueOrder(new Order("Attack", a, Target.FromActor(owner.TargetActor), false)); + owner.Bot.QueueOrder(new Order("Attack", a, owner.Target, false)); } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs index 796224a701..dceb78972c 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/GroundStates.cs @@ -9,6 +9,7 @@ */ #endregion +using System.Collections.Generic; using System.Linq; using OpenRA.Traits; @@ -21,9 +22,21 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads return ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies)); } - protected static Actor FindClosestEnemy(Squad owner) + protected static (Actor Actor, WVec Offset) FindClosestEnemy(Squad owner) { - return owner.SquadManager.FindClosestEnemy(owner.Units.First().CenterPosition); + return owner.SquadManager.FindClosestEnemy(owner.Units.First()); + } + + protected static IEnumerable<(Actor Actor, WVec Offset)> FindEnemies(Squad owner, IEnumerable actors) + { + return owner.SquadManager.FindEnemies( + actors, + owner.Units.First()); + } + + protected static Actor ClosestToEnemy(Squad owner) + { + return SquadManagerBotModule.ClosestTo(owner.Units, owner.TargetActor); } } @@ -36,24 +49,26 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy == null) + owner.SetActorToTarget(closestEnemy); + if (closestEnemy.Actor == null) return; - - owner.TargetActor = closestEnemy; } - var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)) - .Where(owner.SquadManager.IsPreferredEnemyUnit).ToList(); + var enemyUnits = + FindEnemies(owner, + owner.World.FindActorsInCircle(owner.Target.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius))) + .Select(x => x.Actor) + .ToList(); if (enemyUnits.Count == 0) return; if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemyUnits)) { - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); // We have gathered sufficient units. Attack the nearest enemy unit. owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackMoveState(), true); @@ -78,19 +93,18 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else + owner.SetActorToTarget(closestEnemy); + if (closestEnemy.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); return; } } - var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition); + var leader = ClosestToEnemy(owner); if (leader == null) return; @@ -129,16 +143,14 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads } else { - var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)) - .Where(owner.SquadManager.IsPreferredEnemyUnit); - var target = enemies.ClosestTo(leader.CenterPosition); - if (target != null) + var target = owner.SquadManager.FindClosestEnemy(leader, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)); + if (target.Actor != null) { - owner.TargetActor = target; + owner.SetActorToTarget(target); owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsAttackState(), true); } else - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); } if (ShouldFlee(owner)) @@ -161,22 +173,21 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else + owner.SetActorToTarget(closestEnemy); + if (closestEnemy.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); return; } } - var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition); - if (leader.Location != lastLeaderLocation) + var leader = ClosestToEnemy(owner); + if (leader?.Location != lastLeaderLocation) { - lastLeaderLocation = leader.Location; + lastLeaderLocation = leader?.Location; lastUpdatedTick = owner.World.WorldTick; } @@ -197,7 +208,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads foreach (var a in owner.Units) if (!BusyAttack(a)) - owner.Bot.QueueOrder(new Order("Attack", a, Target.FromActor(owner.TargetActor), false)); + owner.Bot.QueueOrder(new Order("AttackMove", a, owner.Target, false)); if (ShouldFlee(owner)) owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsFleeState(), true); @@ -219,6 +230,6 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads owner.FuzzyStateMachine.ChangeState(owner, new GroundUnitsIdleState(), true); } - public void Deactivate(Squad owner) { owner.Units.Clear(); } + public void Deactivate(Squad owner) { owner.SquadManager.UnregisterSquad(owner); } } } diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs deleted file mode 100644 index 8c706545e1..0000000000 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/NavyStates.cs +++ /dev/null @@ -1,247 +0,0 @@ -#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.Linq; -using OpenRA.Traits; - -namespace OpenRA.Mods.Common.Traits.BotModules.Squads -{ - abstract class NavyStateBase : StateBase - { - protected virtual bool ShouldFlee(Squad owner) - { - return ShouldFlee(owner, enemies => !AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemies)); - } - - protected static Actor FindClosestEnemy(Squad owner) - { - var first = owner.Units.First(); - - // Navy squad AI can exploit enemy naval production to find path, if any. - // (Way better than finding a nearest target which is likely to be on Ground) - // You might be tempted to move these lookups into Activate() but that causes null reference exception. - var mobile = first.Trait(); - - var navalProductions = owner.World.ActorsHavingTrait().Where(a - => owner.SquadManager.Info.NavalProductionTypes.Contains(a.Info.Name) - && mobile.PathFinder.PathExistsForLocomotor(mobile.Locomotor, first.Location, a.Location) - && a.AppearsHostileTo(first)); - - var nearest = navalProductions.ClosestTo(first); - if (nearest != null) - { - // Return nearest when it is FAR enough. - // If the naval production is within MaxBaseRadius, it implies that - // this squad is close to enemy territory and they should expect a naval combat; - // closest enemy makes more sense in that case. - if ((nearest.Location - first.Location).LengthSquared > owner.SquadManager.Info.MaxBaseRadius * owner.SquadManager.Info.MaxBaseRadius) - return nearest; - } - - return owner.SquadManager.FindClosestEnemy(first.CenterPosition); - } - } - - sealed class NavyUnitsIdleState : NavyStateBase, IState - { - public void Activate(Squad owner) { } - - public void Tick(Squad owner) - { - if (!owner.IsValid) - return; - - if (!owner.IsTargetValid) - { - var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy == null) - return; - - owner.TargetActor = closestEnemy; - } - - var enemyUnits = owner.World.FindActorsInCircle(owner.TargetActor.CenterPosition, WDist.FromCells(owner.SquadManager.Info.IdleScanRadius)) - .Where(owner.SquadManager.IsPreferredEnemyUnit).ToList(); - - if (enemyUnits.Count == 0) - return; - - if (AttackOrFleeFuzzy.Default.CanAttack(owner.Units, enemyUnits)) - { - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); - - // We have gathered sufficient units. Attack the nearest enemy unit. - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackMoveState(), true); - } - else - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - } - - public void Deactivate(Squad owner) { } - } - - sealed class NavyUnitsAttackMoveState : NavyStateBase, IState - { - int lastUpdatedTick; - CPos? lastLeaderLocation; - Actor lastTarget; - - public void Activate(Squad owner) { } - - public void Tick(Squad owner) - { - if (!owner.IsValid) - return; - - if (!owner.IsTargetValid) - { - var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else - { - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - return; - } - } - - var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition); - if (leader == null) - return; - - if (leader.Location != lastLeaderLocation) - { - lastLeaderLocation = leader.Location; - lastUpdatedTick = owner.World.WorldTick; - } - - if (owner.TargetActor != lastTarget) - { - lastTarget = owner.TargetActor; - lastUpdatedTick = owner.World.WorldTick; - } - - // HACK: Drop back to the idle state if we haven't moved in 2.5 seconds - // This works around the squad being stuck trying to attack-move to a location - // that they cannot path to, generating expensive pathfinding calls each tick. - if (owner.World.WorldTick > lastUpdatedTick + 63) - { - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsIdleState(), true); - return; - } - - 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(); - - if (ownUnits.Count < owner.Units.Count) - { - // Since units have different movement speeds, they get separated while approaching the target. - // Let them regroup into tighter formation. - owner.Bot.QueueOrder(new Order("Stop", leader, false)); - - var units = owner.Units.Where(a => !ownUnits.Contains(a)).ToArray(); - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, leader.Location), false, groupedActors: units)); - } - else - { - var enemies = owner.World.FindActorsInCircle(leader.CenterPosition, WDist.FromCells(owner.SquadManager.Info.AttackScanRadius)) - .Where(owner.SquadManager.IsPreferredEnemyUnit); - var target = enemies.ClosestTo(leader.CenterPosition); - if (target != null) - { - owner.TargetActor = target; - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsAttackState(), true); - } - else - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); - } - - if (ShouldFlee(owner)) - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - } - - public void Deactivate(Squad owner) { } - } - - sealed class NavyUnitsAttackState : NavyStateBase, IState - { - int lastUpdatedTick; - CPos? lastLeaderLocation; - Actor lastTarget; - - public void Activate(Squad owner) { } - - public void Tick(Squad owner) - { - if (!owner.IsValid) - return; - - if (!owner.IsTargetValid) - { - var closestEnemy = FindClosestEnemy(owner); - if (closestEnemy != null) - owner.TargetActor = closestEnemy; - else - { - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - return; - } - } - - var leader = owner.Units.ClosestTo(owner.TargetActor.CenterPosition); - if (leader.Location != lastLeaderLocation) - { - lastLeaderLocation = leader.Location; - lastUpdatedTick = owner.World.WorldTick; - } - - if (owner.TargetActor != lastTarget) - { - lastTarget = owner.TargetActor; - lastUpdatedTick = owner.World.WorldTick; - } - - // HACK: Drop back to the idle state if we haven't moved in 2.5 seconds - // This works around the squad being stuck trying to attack-move to a location - // that they cannot path to, generating expensive pathfinding calls each tick. - if (owner.World.WorldTick > lastUpdatedTick + 63) - { - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsIdleState(), true); - return; - } - - foreach (var a in owner.Units) - if (!BusyAttack(a)) - owner.Bot.QueueOrder(new Order("Attack", a, Target.FromActor(owner.TargetActor), false)); - - if (ShouldFlee(owner)) - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsFleeState(), true); - } - - public void Deactivate(Squad owner) { } - } - - sealed class NavyUnitsFleeState : NavyStateBase, IState - { - public void Activate(Squad owner) { } - - public void Tick(Squad owner) - { - if (!owner.IsValid) - return; - - GoToRandomOwnBuilding(owner); - owner.FuzzyStateMachine.ChangeState(owner, new NavyUnitsIdleState(), true); - } - - public void Deactivate(Squad owner) { owner.Units.Clear(); } - } -} diff --git a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs index a5998990ec..f6433bff8d 100644 --- a/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs +++ b/OpenRA.Mods.Common/Traits/BotModules/Squads/States/ProtectionStates.cs @@ -9,7 +9,7 @@ */ #endregion -using OpenRA.Traits; +using System.Linq; namespace OpenRA.Mods.Common.Traits.BotModules.Squads { @@ -32,11 +32,11 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads if (!owner.IsValid) return; - if (!owner.IsTargetValid) + if (!owner.IsTargetValid()) { - owner.TargetActor = owner.SquadManager.FindClosestEnemy(owner.CenterPosition, WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius)); - - if (owner.TargetActor == null) + var target = owner.SquadManager.FindClosestEnemy(owner.Units.First(), WDist.FromCells(owner.SquadManager.Info.ProtectionScanRadius)); + owner.SetActorToTarget(target); + if (target.Actor == null) { owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionFleeState(), true); return; @@ -55,7 +55,7 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads Backoff--; } else - owner.Bot.QueueOrder(new Order("AttackMove", null, Target.FromCell(owner.World, owner.TargetActor.Location), false, groupedActors: owner.Units.ToArray())); + owner.Bot.QueueOrder(new Order("AttackMove", null, owner.Target, false, groupedActors: owner.Units.ToArray())); } public void Deactivate(Squad owner) { } @@ -74,6 +74,6 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads owner.FuzzyStateMachine.ChangeState(owner, new UnitsForProtectionIdleState(), true); } - public void Deactivate(Squad owner) { owner.Units.Clear(); } + public void Deactivate(Squad owner) { owner.SquadManager.UnregisterSquad(owner); } } } diff --git a/OpenRA.Mods.Common/Traits/Buildings/RallyPoint.cs b/OpenRA.Mods.Common/Traits/Buildings/RallyPoint.cs index 1a8dd6f5a4..cace318dae 100644 --- a/OpenRA.Mods.Common/Traits/Buildings/RallyPoint.cs +++ b/OpenRA.Mods.Common/Traits/Buildings/RallyPoint.cs @@ -183,10 +183,8 @@ namespace OpenRA.Mods.Common.Traits if (modifiers.HasModifier(TargetModifiers.ForceAttack) && !string.IsNullOrEmpty(info.ForceSetType)) { var closest = self.World.Selection.Actors - .Select(a => (a, a.TraitOrDefault())) - .Where(x => x.RallyPoint != null && x.RallyPoint.Info.ForceSetType == info.ForceSetType) - .OrderBy(x => (location - x.Actor.Location).LengthSquared) - .FirstOrDefault().Actor; + .Where(a => a.TraitOrDefault()?.Info.ForceSetType == info.ForceSetType) + .ClosestToWithPathTo(self.World, target.CenterPosition); ForceSet = closest == self; } diff --git a/OpenRA.Mods.Common/Traits/CaptureManager.cs b/OpenRA.Mods.Common/Traits/CaptureManager.cs index 624226a8eb..8a6cde17e4 100644 --- a/OpenRA.Mods.Common/Traits/CaptureManager.cs +++ b/OpenRA.Mods.Common/Traits/CaptureManager.cs @@ -226,7 +226,7 @@ namespace OpenRA.Mods.Common.Traits currentTargetTotal = captures.Info.CaptureDelay; if (move != null && captures.Info.ConsumedByCapture) { - var pos = target.GetTargetablePositions().PositionClosestTo(self.CenterPosition); + var pos = target.GetTargetablePositions().ClosestToIgnoringPath(self.CenterPosition); currentTargetTotal += move.EstimatedMoveDuration(self, self.CenterPosition, pos); } diff --git a/OpenRA.Mods.Common/WorldExtensions.cs b/OpenRA.Mods.Common/WorldExtensions.cs index b5d0791adf..77aff2e5e7 100644 --- a/OpenRA.Mods.Common/WorldExtensions.cs +++ b/OpenRA.Mods.Common/WorldExtensions.cs @@ -11,12 +11,191 @@ using System; using System.Collections.Generic; +using System.Linq; using OpenRA.Mods.Common.Traits; namespace OpenRA.Mods.Common { public static class WorldExtensions { + /// + /// Filters by only returning those that can be reached as the target of a path from + /// . Only terrain is taken into account, i.e. as if + /// was given. + /// is used to define locations around each actor in + /// of which one must be reachable. + /// + public static IEnumerable<(Actor Actor, WVec[] ReachableOffsets)> WithPathFrom(this IEnumerable actors, Actor sourceActor, Func targetOffsets) + { + if (sourceActor.Info.HasTraitInfo()) + return actors.Select(a => (a, targetOffsets(a))); + var mobile = sourceActor.TraitOrDefault(); + if (mobile == null) + return Enumerable.Empty<(Actor Actor, WVec[] ReachableOffsets)>(); + + var pathFinder = sourceActor.World.WorldActor.Trait(); + var locomotor = mobile.Locomotor; + var map = sourceActor.World.Map; + return actors + .Select(a => + { + return (a, targetOffsets(a).Where(offset => + pathFinder.PathExistsForLocomotor( + mobile.Locomotor, + map.CellContaining(sourceActor.CenterPosition), + map.CellContaining(a.CenterPosition + offset))) + .ToArray()); + }) + .Where(x => x.ReachableOffsets.Length > 0); + } + + /// + /// Filters by only returning those that can be reached as the target of a path from + /// . Only terrain is taken into account, i.e. as if + /// was given. + /// + public static IEnumerable WithPathFrom(this IEnumerable actors, Actor sourceActor) + { + return actors.WithPathFrom(sourceActor, _ => new[] { WVec.Zero }).Select(x => x.Actor); + } + + /// + /// Of that can be reached as the target of a path from + /// , returns the nearest by comparing their . + /// Only terrain is taken into account, i.e. as if was given. + /// is used to define locations around each actor in + /// of which one must be reachable. + /// + public static Actor ClosestToWithPathFrom(this IEnumerable actors, Actor sourceActor, Func targetOffsets = null) + { + return actors + .WithPathFrom(sourceActor, targetOffsets ?? (_ => new[] { WVec.Zero })) + .Select(x => x.Actor) + .ClosestToIgnoringPath(sourceActor); + } + + /// + /// Of that can be reached as the target of a path from + /// , returns the nearest by comparing the . + /// Only terrain is taken into account, i.e. as if was given. + /// + public static WPos? ClosestToWithPathFrom(this IEnumerable positions, Actor sourceActor) + { + if (sourceActor.Info.HasTraitInfo()) + return positions.ClosestToIgnoringPath(sourceActor.CenterPosition); + var mobile = sourceActor.TraitOrDefault(); + if (mobile == null) + return null; + + var pathFinder = sourceActor.World.WorldActor.Trait(); + var locomotor = mobile.Locomotor; + var map = sourceActor.World.Map; + return positions + .Where(p => pathFinder.PathExistsForLocomotor( + locomotor, + map.CellContaining(sourceActor.CenterPosition), + map.CellContaining(p))) + .ClosestToIgnoringPath(sourceActor.CenterPosition); + } + + /// + /// Filters by only returning those where the can + /// be reached as the target of a path from the actor. Only terrain is taken into account, i.e. as if + /// was given. + /// + public static IEnumerable WithPathTo(this IEnumerable actors, World world, WPos targetPosition) + { + var pathFinder = world.WorldActor.Trait(); + var map = world.Map; + return actors + .Where(a => + { + if (a.Info.HasTraitInfo()) + return true; + var mobile = a.TraitOrDefault(); + if (mobile == null) + return false; + return pathFinder.PathExistsForLocomotor( + mobile.Locomotor, + map.CellContaining(targetPosition), + map.CellContaining(a.CenterPosition)); + }); + } + + /// + /// Filters by only returning those where any of the + /// can be reached as the target of a path from the actor. + /// Returns the reachable target positions for each actor. + /// Only terrain is taken into account, i.e. as if was given. + /// + public static IEnumerable<(Actor Actor, WPos[] ReachablePositions)> WithPathToAny( + this IEnumerable actors, World world, Func targetPositions) + { + var pathFinder = world.WorldActor.Trait(); + var map = world.Map; + return actors + .Select(a => + { + if (a.Info.HasTraitInfo()) + return (a, targetPositions(a).ToArray()); + var mobile = a.TraitOrDefault(); + if (mobile == null) + return (a, Array.Empty()); + return (a, targetPositions(a).Where(targetPosition => + pathFinder.PathExistsForLocomotor( + mobile.Locomotor, + map.CellContaining(targetPosition), + map.CellContaining(a.CenterPosition))) + .ToArray()); + }) + .Where(x => x.ReachablePositions.Length > 0); + } + + /// + /// Filters by only returning those where the can be + /// reached as the target of a path from the actor. Only terrain is taken into account, i.e. as if + /// was given. + /// + public static IEnumerable WithPathTo(this IEnumerable actors, Actor targetActor) + { + return actors.WithPathTo(targetActor.World, targetActor.CenterPosition); + } + + /// + /// Of where the can be reached as the target of a + /// path from the actor, returns the nearest by comparing the . + /// Only terrain is taken into account, i.e. as if was given. + /// + public static Actor ClosestToWithPathTo(this IEnumerable actors, World world, WPos targetPosition) + { + return actors + .WithPathTo(world, targetPosition) + .ClosestToIgnoringPath(targetPosition); + } + + /// + /// Of where any of the can be reached as the + /// target of a path from the actor, returns the nearest by comparing the . + /// Only terrain is taken into account, i.e. as if was given. + /// + public static Actor ClosestToWithPathToAny(this IEnumerable actors, World world, Func targetPositions) + { + return actors + .WithPathToAny(world, targetPositions) + .MinByOrDefault(x => x.ReachablePositions.Min(pos => (x.Actor.CenterPosition - pos).LengthSquared)) + .Actor; + } + + /// + /// Of where the can be reached as the target of a + /// path from the actor, returns the nearest by comparing their . + /// Only terrain is taken into account, i.e. as if was given. + /// + public static Actor ClosestToWithPathTo(this IEnumerable actors, Actor targetActor) + { + return actors.ClosestToWithPathTo(targetActor.World, targetActor.CenterPosition); + } + /// /// Finds all the actors of which their health radius is intersected by a line (with a definable width) between two points. /// diff --git a/OpenRA.Mods.D2k/Traits/Sandworm.cs b/OpenRA.Mods.D2k/Traits/Sandworm.cs index e43ae3316f..b223b53a68 100644 --- a/OpenRA.Mods.D2k/Traits/Sandworm.cs +++ b/OpenRA.Mods.D2k/Traits/Sandworm.cs @@ -10,6 +10,7 @@ #endregion using System.Linq; +using OpenRA.Mods.Common; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Traits; @@ -83,6 +84,7 @@ namespace OpenRA.Mods.D2k.Traits // If close enough, we don't care about other actors. var target = self.World.FindActorsInCircle(self.CenterPosition, WormInfo.IgnoreNoiseAttackRange) + .WithPathFrom(self) .Select(t => Target.FromActor(t)) .FirstOrDefault(t => attackTrait.HasAnyValidWeapons(t)); @@ -101,6 +103,7 @@ namespace OpenRA.Mods.D2k.Traits } var actorsInRange = self.World.FindActorsInCircle(self.CenterPosition, WormInfo.MaxSearchRadius) + .WithPathFrom(self) .Where(IsValidTarget).SelectMany(a => a.TraitsImplementing()); var noiseDirection = actorsInRange.Aggregate(WVec.Zero, (a, b) => a + b.AttractionAtPosition(self.CenterPosition));