Add helper methods to locate actors that can be reached via a path.

Previously, the ClosestTo and PositionClosestTo existed to perform a simple distance based check to choose the closest location from a choice of locations to a single other location. For some functions this is sufficient, but for many functions we want to then move between the locations. If the location selected is in fact unreachable (e.g. on another island) then we would not want to consider it.

We now introduce ClosestToIgnoringPath for checks where we don't care about a path existing, e.g. weapons hitting nearby targets. When we do care about paths, we introduce ClosestToWithPathFrom and ClosestToWithPathTo which will check that a path exists. The PathFrom check will make sure one of the actors from the list can make it to the single target location. The PathTo check will make sure the single actor can make it to one of the target locations. This difference allows us to specify which actor will be doing the moving. This is important as a path might exists for one actor, but not another. Consider two islands with a hovercraft on one and a tank on the other. The hovercraft can path to the tank, but the tank cannot path to the hovercraft.

We also introduce WithPathFrom and WithPathTo. These will perform filtering by checking for valid paths, but won't select the closest location.

By employing the new methods that filter for paths, we fix various behaviour that would cause actors to get confused. Imagine an islands map, by checking for paths we ensure logic will locate reachable locations on the island, rather than considering a location on a nearby island that is physically closer but unreachable. This fixes AI squad automation, and other automated behaviours such as rearming.
This commit is contained in:
RoosterDragon
2023-07-20 18:36:47 +01:00
committed by Gustas
parent 2ac855488b
commit 23f3f8d90c
28 changed files with 527 additions and 400 deletions

View File

@@ -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);

View File

@@ -19,19 +19,51 @@ namespace OpenRA
{
public static class WorldUtils
{
public static Actor ClosestTo(this IEnumerable<Actor> actors, Actor a)
/// <summary>
/// From the given <paramref name="actors"/>, select the one nearest the given <paramref name="actor"/> by
/// comparing their <see cref="Actor.CenterPosition"/>. No check is done to see if a path exists.
/// </summary>
public static Actor ClosestToIgnoringPath(this IEnumerable<Actor> actors, Actor actor)
{
return actors.ClosestTo(a.CenterPosition);
return actors.ClosestToIgnoringPath(actor.CenterPosition);
}
public static Actor ClosestTo(this IEnumerable<Actor> actors, WPos pos)
/// <summary>
/// From the given <paramref name="actors"/>, select the one nearest the given <paramref name="position"/> by
/// comparing the <see cref="Actor.CenterPosition"/>. No check is done to see if a path exists.
/// </summary>
public static Actor ClosestToIgnoringPath(this IEnumerable<Actor> 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<WPos> positions, WPos pos)
/// <summary>
/// From the given <paramref name="items"/> that can be projected to <see cref="Actor"/>,
/// select the one nearest the given <paramref name="actor"/> by
/// comparing their <see cref="Actor.CenterPosition"/>. No check is done to see if a path exists.
/// </summary>
public static T ClosestToIgnoringPath<T>(IEnumerable<T> items, Func<T, Actor> selector, Actor actor)
{
return positions.MinByOrDefault(p => (p - pos).LengthSquared);
return ClosestToIgnoringPath(items, selector, actor.CenterPosition);
}
/// <summary>
/// From the given <paramref name="items"/> that can be projected to <see cref="Actor"/>,
/// select the one nearest the given <paramref name="position"/> by
/// comparing the <see cref="Actor.CenterPosition"/>. No check is done to see if a path exists.
/// </summary>
public static T ClosestToIgnoringPath<T>(IEnumerable<T> items, Func<T, Actor> selector, WPos position)
{
return items.MinByOrDefault(x => (selector(x).CenterPosition - position).LengthSquared);
}
/// <summary>
/// From the given <paramref name="positions"/>, select the one nearest the given <paramref name="position"/>.
/// No check is done to see if a path exists, as an actor is required for that.
/// </summary>
public static WPos ClosestToIgnoringPath(this IEnumerable<WPos> positions, WPos position)
{
return positions.MinByOrDefault(p => (p - position).LengthSquared);
}
public static IEnumerable<Actor> FindActorsInCircle(this World world, WPos origin, WDist r)

View File

@@ -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));

View File

@@ -63,12 +63,6 @@ namespace OpenRA.Mods.Common
.Count(a => a.Owner == owner && buildings.Contains(a.Info.Name));
}
public static List<Actor> FindEnemiesByCommonName(HashSet<string> 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<string> names, Player owner)
{
return owner.World.Map.Rules.Actors.Where(k => names.Contains(k.Key)).Random(owner.World.LocalRandom).Value;

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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 +

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)
{

View File

@@ -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()

View File

@@ -104,7 +104,7 @@ namespace OpenRA.Mods.Common.Traits
var carriers = self.World.ActorsHavingTrait<AutoCarryall>(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)

View File

@@ -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<ICrushable>().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;

View File

@@ -479,8 +479,9 @@ namespace OpenRA.Mods.Common.Traits
case BuildingType.Defense:
// Build near the closest enemy structure
var closestEnemy = world.ActorsHavingTrait<Building>().Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy)
.ClosestTo(world.Map.CenterOfCell(baseBuilder.DefenseCenter));
var closestEnemy = world.ActorsHavingTrait<Building>()
.Where(a => !a.Disposed && player.RelationshipWith(a.Owner) == PlayerRelationship.Enemy)
.ClosestToIgnoringPath(world.Map.CenterOfCell(baseBuilder.DefenseCenter));
var targetCell = closestEnemy != null ? closestEnemy.Location : baseCenter;

View File

@@ -47,7 +47,6 @@ namespace OpenRA.Mods.Common.Traits
{
readonly World world;
readonly Player player;
readonly Func<Actor, bool> isEnemyUnit;
readonly Predicate<Actor> 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<HuskInfo>()
&& unit.Info.HasTraitInfo<ITargetableInfo>();
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<Actor> 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;

View File

@@ -118,7 +118,7 @@ namespace OpenRA.Mods.Common.Traits
readonly List<Actor> unitsHangingAroundTheBase = new();
// Units that the bot already knows about. Any unit not on this list needs to be given a role.
readonly List<Actor> activeUnits = new();
readonly HashSet<Actor> activeUnits = new();
public List<Squad> Squads = new();
@@ -195,22 +195,105 @@ namespace OpenRA.Mods.Common.Traits
AssignRolesToIdleUnits(bot);
}
internal Actor FindClosestEnemy(WPos pos)
internal static Actor ClosestTo(IEnumerable<Actor> 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<AttackBase>().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<Actor> 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<AttackBase>().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<Actor> 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<AttackBaseInfo>()
&& !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<AttackBaseInfo>()
&& !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<AttackBaseInfo>() && !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<AttackBaseInfo>()
&& !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<AttackBaseInfo>());
.Where(unit =>
unit.Owner == Player
&& !Info.ProtectionTypes.Contains(unit.Info.Name)
&& unit.Info.HasTraitInfo<AttackBaseInfo>())
.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<uint[]>("ActiveUnits", activeUnitsNode.Value.Value)
activeUnits.UnionWith(FieldLoader.GetValue<uint[]>("ActiveUnits", activeUnitsNode.Value.Value)
.Select(a => self.World.GetActorById(a)).Where(a => a != null));
}

View File

@@ -20,34 +20,44 @@ namespace OpenRA.Mods.Common.Traits.BotModules.Squads
public class Squad
{
public List<Actor> Units = new();
public HashSet<Actor> 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) { }
/// <summary>
/// Target location to attack. This will be either the targeted actor,
/// or a position close to that actor sufficient to get within weapons range.
/// </summary>
internal Target Target { get; set; }
public Squad(IBot bot, SquadManagerBotModule squadManager, SquadType type, Actor target)
/// <summary>
/// Actor that is targeted, for any actor based checks. Use <see cref="Target"/> for a targeting location.
/// </summary>
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<HuskInfo>();
/// <summary>
/// Checks the target is still valid, and updates the <see cref="Target"/> location if it is still valid.
/// </summary>
public bool IsTargetValid()
{
var valid =
TargetActor != null &&
TargetActor.IsInWorld &&
TargetActor.IsTargetableBy(Units.FirstOrDefault()) &&
!TargetActor.Info.HasTraitInfo<HuskInfo>();
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<MiniYamlNode>()
{
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<SquadType>("Type", typeNode.Value.Value);
var targetNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Target");
if (targetNode != null)
targetActor = squadManager.World.GetActorById(FieldLoader.GetValue<uint>("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<uint>("ActorToTarget", actorToTargetNode.Value.Value));
var targetOffset = FieldLoader.GetValue<WVec>("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<uint[]>("Units", unitsNode.Value.Value)
squad.Units.UnionWith(FieldLoader.GetValue<uint[]>("Units", unitsNode.Value.Value)
.Select(a => squadManager.World.GetActorById(a)));
return squad;

View File

@@ -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));
}
}

View File

@@ -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<Actor> 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); }
}
}

View File

@@ -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<Mobile>();
var navalProductions = owner.World.ActorsHavingTrait<Building>().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(); }
}
}

View File

@@ -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); }
}
}

View File

@@ -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<Actor, (Actor Actor, RallyPoint RallyPoint)>(a => (a, a.TraitOrDefault<RallyPoint>()))
.Where(x => x.RallyPoint != null && x.RallyPoint.Info.ForceSetType == info.ForceSetType)
.OrderBy(x => (location - x.Actor.Location).LengthSquared)
.FirstOrDefault().Actor;
.Where(a => a.TraitOrDefault<RallyPoint>()?.Info.ForceSetType == info.ForceSetType)
.ClosestToWithPathTo(self.World, target.CenterPosition);
ForceSet = closest == self;
}

View File

@@ -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);
}

View File

@@ -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
{
/// <summary>
/// Filters <paramref name="actors"/> by only returning those that can be reached as the target of a path from
/// <paramref name="sourceActor"/>. Only terrain is taken into account, i.e. as if
/// <see cref="BlockedByActor.None"/> was given.
/// <paramref name="targetOffsets"/> is used to define locations around each actor in <paramref name="actors"/>
/// of which one must be reachable.
/// </summary>
public static IEnumerable<(Actor Actor, WVec[] ReachableOffsets)> WithPathFrom(this IEnumerable<Actor> actors, Actor sourceActor, Func<Actor, WVec[]> targetOffsets)
{
if (sourceActor.Info.HasTraitInfo<AircraftInfo>())
return actors.Select<Actor, (Actor Actor, WVec[] ReachableOffsets)>(a => (a, targetOffsets(a)));
var mobile = sourceActor.TraitOrDefault<Mobile>();
if (mobile == null)
return Enumerable.Empty<(Actor Actor, WVec[] ReachableOffsets)>();
var pathFinder = sourceActor.World.WorldActor.Trait<PathFinder>();
var locomotor = mobile.Locomotor;
var map = sourceActor.World.Map;
return actors
.Select<Actor, (Actor Actor, WVec[] ReachableOffsets)>(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);
}
/// <summary>
/// Filters <paramref name="actors"/> by only returning those that can be reached as the target of a path from
/// <paramref name="sourceActor"/>. Only terrain is taken into account, i.e. as if
/// <see cref="BlockedByActor.None"/> was given.
/// </summary>
public static IEnumerable<Actor> WithPathFrom(this IEnumerable<Actor> actors, Actor sourceActor)
{
return actors.WithPathFrom(sourceActor, _ => new[] { WVec.Zero }).Select(x => x.Actor);
}
/// <summary>
/// Of <paramref name="actors"/> that can be reached as the target of a path from
/// <paramref name="sourceActor"/>, returns the nearest by comparing their <see cref="Actor.CenterPosition"/>.
/// Only terrain is taken into account, i.e. as if <see cref="BlockedByActor.None"/> was given.
/// <paramref name="targetOffsets"/> is used to define locations around each actor in <paramref name="actors"/>
/// of which one must be reachable.
/// </summary>
public static Actor ClosestToWithPathFrom(this IEnumerable<Actor> actors, Actor sourceActor, Func<Actor, WVec[]> targetOffsets = null)
{
return actors
.WithPathFrom(sourceActor, targetOffsets ?? (_ => new[] { WVec.Zero }))
.Select(x => x.Actor)
.ClosestToIgnoringPath(sourceActor);
}
/// <summary>
/// Of <paramref name="positions"/> that can be reached as the target of a path from
/// <paramref name="sourceActor"/>, returns the nearest by comparing the <see cref="Actor.CenterPosition"/>.
/// Only terrain is taken into account, i.e. as if <see cref="BlockedByActor.None"/> was given.
/// </summary>
public static WPos? ClosestToWithPathFrom(this IEnumerable<WPos> positions, Actor sourceActor)
{
if (sourceActor.Info.HasTraitInfo<AircraftInfo>())
return positions.ClosestToIgnoringPath(sourceActor.CenterPosition);
var mobile = sourceActor.TraitOrDefault<Mobile>();
if (mobile == null)
return null;
var pathFinder = sourceActor.World.WorldActor.Trait<PathFinder>();
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);
}
/// <summary>
/// Filters <paramref name="actors"/> by only returning those where the <paramref name="targetPosition"/> can
/// be reached as the target of a path from the actor. Only terrain is taken into account, i.e. as if
/// <see cref="BlockedByActor.None"/> was given.
/// </summary>
public static IEnumerable<Actor> WithPathTo(this IEnumerable<Actor> actors, World world, WPos targetPosition)
{
var pathFinder = world.WorldActor.Trait<PathFinder>();
var map = world.Map;
return actors
.Where(a =>
{
if (a.Info.HasTraitInfo<AircraftInfo>())
return true;
var mobile = a.TraitOrDefault<Mobile>();
if (mobile == null)
return false;
return pathFinder.PathExistsForLocomotor(
mobile.Locomotor,
map.CellContaining(targetPosition),
map.CellContaining(a.CenterPosition));
});
}
/// <summary>
/// Filters <paramref name="actors"/> by only returning those where any of the
/// <paramref name="targetPositions"/> 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 <see cref="BlockedByActor.None"/> was given.
/// </summary>
public static IEnumerable<(Actor Actor, WPos[] ReachablePositions)> WithPathToAny(
this IEnumerable<Actor> actors, World world, Func<Actor, WPos[]> targetPositions)
{
var pathFinder = world.WorldActor.Trait<PathFinder>();
var map = world.Map;
return actors
.Select<Actor, (Actor Actor, WPos[] ReachablePositions)>(a =>
{
if (a.Info.HasTraitInfo<AircraftInfo>())
return (a, targetPositions(a).ToArray());
var mobile = a.TraitOrDefault<Mobile>();
if (mobile == null)
return (a, Array.Empty<WPos>());
return (a, targetPositions(a).Where(targetPosition =>
pathFinder.PathExistsForLocomotor(
mobile.Locomotor,
map.CellContaining(targetPosition),
map.CellContaining(a.CenterPosition)))
.ToArray());
})
.Where(x => x.ReachablePositions.Length > 0);
}
/// <summary>
/// Filters <paramref name="actors"/> by only returning those where the <paramref name="targetActor"/> can be
/// reached as the target of a path from the actor. Only terrain is taken into account, i.e. as if
/// <see cref="BlockedByActor.None"/> was given.
/// </summary>
public static IEnumerable<Actor> WithPathTo(this IEnumerable<Actor> actors, Actor targetActor)
{
return actors.WithPathTo(targetActor.World, targetActor.CenterPosition);
}
/// <summary>
/// Of <paramref name="actors"/> where the <paramref name="targetPosition"/> can be reached as the target of a
/// path from the actor, returns the nearest by comparing the <see cref="Actor.CenterPosition"/>.
/// Only terrain is taken into account, i.e. as if <see cref="BlockedByActor.None"/> was given.
/// </summary>
public static Actor ClosestToWithPathTo(this IEnumerable<Actor> actors, World world, WPos targetPosition)
{
return actors
.WithPathTo(world, targetPosition)
.ClosestToIgnoringPath(targetPosition);
}
/// <summary>
/// Of <paramref name="actors"/> where any of the <paramref name="targetPositions"/> can be reached as the
/// target of a path from the actor, returns the nearest by comparing the <see cref="Actor.CenterPosition"/>.
/// Only terrain is taken into account, i.e. as if <see cref="BlockedByActor.None"/> was given.
/// </summary>
public static Actor ClosestToWithPathToAny(this IEnumerable<Actor> actors, World world, Func<Actor, WPos[]> targetPositions)
{
return actors
.WithPathToAny(world, targetPositions)
.MinByOrDefault(x => x.ReachablePositions.Min(pos => (x.Actor.CenterPosition - pos).LengthSquared))
.Actor;
}
/// <summary>
/// Of <paramref name="actors"/> where the <paramref name="targetActor"/> can be reached as the target of a
/// path from the actor, returns the nearest by comparing their <see cref="Actor.CenterPosition"/>.
/// Only terrain is taken into account, i.e. as if <see cref="BlockedByActor.None"/> was given.
/// </summary>
public static Actor ClosestToWithPathTo(this IEnumerable<Actor> actors, Actor targetActor)
{
return actors.ClosestToWithPathTo(targetActor.World, targetActor.CenterPosition);
}
/// <summary>
/// Finds all the actors of which their health radius is intersected by a line (with a definable width) between two points.
/// </summary>

View File

@@ -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<AttractsWorms>());
var noiseDirection = actorsInRange.Aggregate(WVec.Zero, (a, b) => a + b.AttractionAtPosition(self.CenterPosition));