Merge pull request #9734 from RoosterDragon/auto-target-perf

Improve AutoTarget performance
This commit is contained in:
Oliver Brakmann
2015-12-20 19:53:17 +01:00
18 changed files with 174 additions and 89 deletions

View File

@@ -44,6 +44,7 @@ namespace OpenRA
public Rectangle VisualBounds { get; private set; } public Rectangle VisualBounds { get; private set; }
public IEffectiveOwner EffectiveOwner { get; private set; } public IEffectiveOwner EffectiveOwner { get; private set; }
public IOccupySpace OccupiesSpace { get; private set; } public IOccupySpace OccupiesSpace { get; private set; }
public ITargetable[] Targetables { get; private set; }
public bool IsIdle { get { return currentActivity == null; } } public bool IsIdle { get { return currentActivity == null; } }
public bool IsDead { get { return Disposed || (health != null && health.IsDead); } } public bool IsDead { get { return Disposed || (health != null && health.IsDead); } }
@@ -110,6 +111,7 @@ namespace OpenRA
disables = TraitsImplementing<IDisable>().ToArray(); disables = TraitsImplementing<IDisable>().ToArray();
visibilityModifiers = TraitsImplementing<IVisibilityModifier>().ToArray(); visibilityModifiers = TraitsImplementing<IVisibilityModifier>().ToArray();
defaultVisibility = Trait<IDefaultVisibility>(); defaultVisibility = Trait<IDefaultVisibility>();
Targetables = TraitsImplementing<ITargetable>().ToArray();
} }
Rectangle DetermineBounds() Rectangle DetermineBounds()
@@ -334,6 +336,33 @@ namespace OpenRA
return defaultVisibility.IsVisible(this, player); return defaultVisibility.IsVisible(this, player);
} }
public IEnumerable<string> GetAllTargetTypes()
{
// PERF: Avoid LINQ.
foreach (var targetable in Targetables)
foreach (var targetType in targetable.TargetTypes)
yield return targetType;
}
public IEnumerable<string> GetEnabledTargetTypes()
{
// PERF: Avoid LINQ.
foreach (var targetable in Targetables)
if (targetable.IsTraitEnabled())
foreach (var targetType in targetable.TargetTypes)
yield return targetType;
}
public bool IsTargetableBy(Actor byActor)
{
// PERF: Avoid LINQ.
foreach (var targetable in Targetables)
if (targetable.IsTraitEnabled() && targetable.TargetableBy(this, byActor))
return true;
return false;
}
#region Scripting interface #region Scripting interface
Lazy<ScriptActorInterface> luaInterface; Lazy<ScriptActorInterface> luaInterface;

View File

@@ -127,14 +127,17 @@ namespace OpenRA.GameRules
/// <summary>Checks if the weapon is valid against (can target) the actor.</summary> /// <summary>Checks if the weapon is valid against (can target) the actor.</summary>
public bool IsValidAgainst(Actor victim, Actor firedBy) public bool IsValidAgainst(Actor victim, Actor firedBy)
{ {
var targetable = victim.TraitsImplementing<ITargetable>().Where(Exts.IsTraitEnabled); var targetTypes = victim.GetEnabledTargetTypes();
if (!IsValidTarget(targetable.SelectMany(t => t.TargetTypes)))
if (!IsValidTarget(targetTypes))
return false; return false;
if (!Warheads.Any(w => w.IsValidAgainst(victim, firedBy))) // PERF: Avoid LINQ.
return false; foreach (var warhead in Warheads)
if (warhead.IsValidAgainst(victim, firedBy))
return true;
return true; return false;
} }
/// <summary>Checks if the weapon is valid against (can target) the frozen actor.</summary> /// <summary>Checks if the weapon is valid against (can target) the frozen actor.</summary>

View File

@@ -159,13 +159,27 @@ namespace OpenRA
public bool CanTargetActor(Actor a) public bool CanTargetActor(Actor a)
{ {
if (HasFogVisibility && fogVisibilities.Any(f => f.IsVisible(a))) // PERF: Avoid LINQ.
return true; if (HasFogVisibility)
foreach (var fogVisibility in fogVisibilities)
if (fogVisibility.IsVisible(a))
return true;
return CanViewActor(a); return CanViewActor(a);
} }
public bool HasFogVisibility { get { return fogVisibilities.Any(f => f.HasFogVisibility()); } } public bool HasFogVisibility
{
get
{
// PERF: Avoid LINQ.
foreach (var fogVisibility in fogVisibilities)
if (fogVisibility.HasFogVisibility())
return true;
return false;
}
}
#region Scripting interface #region Scripting interface

View File

@@ -62,7 +62,7 @@ namespace OpenRA.Traits
CenterPosition = self.CenterPosition; CenterPosition = self.CenterPosition;
Bounds = self.Bounds; Bounds = self.Bounds;
TargetTypes = self.TraitsImplementing<ITargetable>().Where(Exts.IsTraitEnabled).SelectMany(t => t.TargetTypes).ToHashSet(); TargetTypes = self.GetEnabledTargetTypes().ToHashSet();
UpdateVisibility(); UpdateVisibility();
} }

View File

@@ -22,7 +22,6 @@ namespace OpenRA.Traits
TargetType type; TargetType type;
Actor actor; Actor actor;
IEnumerable<ITargetable> targetable;
FrozenActor frozen; FrozenActor frozen;
WPos pos; WPos pos;
int generation; int generation;
@@ -48,7 +47,6 @@ namespace OpenRA.Traits
return new Target return new Target
{ {
actor = a, actor = a,
targetable = a.TraitsImplementing<ITargetable>(),
type = TargetType.Actor, type = TargetType.Actor,
generation = a.Generation, generation = a.Generation,
}; };
@@ -83,8 +81,7 @@ namespace OpenRA.Traits
if (targeter == null || Type == TargetType.Invalid) if (targeter == null || Type == TargetType.Invalid)
return false; return false;
var targeted = this.actor; if (actor != null && !actor.IsTargetableBy(targeter))
if (targeted != null && !targetable.Any(t => t.IsTraitEnabled() && t.TargetableBy(targeted, targeter)))
return false; return false;
return true; return true;
@@ -94,7 +91,25 @@ namespace OpenRA.Traits
// TODO: either replace based on target type or put in singleton trait // TODO: either replace based on target type or put in singleton trait
public bool RequiresForceFire public bool RequiresForceFire
{ {
get { return targetable != null && targetable.Any(Exts.IsTraitEnabled) && targetable.Where(Exts.IsTraitEnabled).All(t => t.RequiresForceFire); } get
{
if (actor == null)
return false;
// PERF: Avoid LINQ.
var isTargetable = false;
foreach (var targetable in actor.Targetables)
{
if (!targetable.IsTraitEnabled())
continue;
isTargetable = true;
if (!targetable.RequiresForceFire)
return false;
}
return isTargetable;
}
} }
// Representative position - see Positions for the full set of targetable positions. // Representative position - see Positions for the full set of targetable positions.
@@ -126,8 +141,7 @@ namespace OpenRA.Traits
switch (Type) switch (Type)
{ {
case TargetType.Actor: case TargetType.Actor:
var targetable = actor.TraitsImplementing<ITargetable>().Where(Exts.IsTraitEnabled); if (!actor.Targetables.Any(Exts.IsTraitEnabled))
if (!targetable.Any())
return new[] { actor.CenterPosition }; return new[] { actor.CenterPosition };
var targetablePositions = actor.TraitOrDefault<ITargetablePositions>(); var targetablePositions = actor.TraitOrDefault<ITargetablePositions>();

View File

@@ -63,7 +63,7 @@ namespace OpenRA.Mods.Common.AI
if (!a.Info.HasTraitInfo<AttackBaseInfo>()) if (!a.Info.HasTraitInfo<AttackBaseInfo>())
return false; return false;
var targetTypes = target.TraitsImplementing<ITargetable>().Where(Exts.IsTraitEnabled).SelectMany(t => t.TargetTypes); var targetTypes = target.GetEnabledTargetTypes();
if (!targetTypes.Any()) if (!targetTypes.Any())
return false; return false;

View File

@@ -125,11 +125,10 @@ namespace OpenRA.Mods.Common.AI
if (a == null) if (a == null)
return 0; return 0;
var targetable = a.TraitsImplementing<ITargetable>().Where(Exts.IsTraitEnabled); if (!a.IsTargetableBy(firedBy.PlayerActor))
if (!targetable.Any(t => t.TargetableBy(a, firedBy.PlayerActor)))
return 0; return 0;
if (Types.Overlaps(targetable.SelectMany(t => t.TargetTypes))) if (Types.Overlaps(a.GetEnabledTargetTypes()))
{ {
switch (TargetMetric) switch (TargetMetric)
{ {

View File

@@ -68,8 +68,8 @@ namespace OpenRA.Mods.Common.Activities
return NextActivity; return NextActivity;
// Drop the target once none of the weapons are effective against it // Drop the target once none of the weapons are effective against it
var armaments = attack.ChooseArmamentsForTarget(Target, forceAttack); var armaments = attack.ChooseArmamentsForTarget(Target, forceAttack).ToList();
if (!armaments.Any()) if (armaments.Count == 0)
return NextActivity; return NextActivity;
// Update ranges // Update ranges

View File

@@ -25,12 +25,7 @@ namespace OpenRA.Mods.Common.Activities
var attack = self.Trait<AttackBase>(); var attack = self.Trait<AttackBase>();
targets = self.World.ActorsHavingTrait<Huntable>().Where( targets = self.World.ActorsHavingTrait<Huntable>().Where(
a => self != a && !a.IsDead && a.IsInWorld && a.AppearsHostileTo(self) a => self != a && !a.IsDead && a.IsInWorld && a.AppearsHostileTo(self)
&& IsTargetable(a, self) && attack.HasAnyValidWeapons(Target.FromActor(a))); && a.IsTargetableBy(self) && attack.HasAnyValidWeapons(Target.FromActor(a)));
}
bool IsTargetable(Actor self, Actor viewer)
{
return self.TraitsImplementing<ITargetable>().Any(t => t.IsTraitEnabled() && t.TargetableBy(self, viewer));
} }
public override Activity Tick(Actor self) public override Activity Tick(Actor self)

View File

@@ -67,9 +67,9 @@ namespace OpenRA.Mods.Common.Orders
public class TargetTypeOrderTargeter : UnitOrderTargeter public class TargetTypeOrderTargeter : UnitOrderTargeter
{ {
readonly string[] targetTypes; readonly HashSet<string> targetTypes;
public TargetTypeOrderTargeter(string[] targetTypes, string order, int priority, string cursor, bool targetEnemyUnits, bool targetAllyUnits) public TargetTypeOrderTargeter(HashSet<string> targetTypes, string order, int priority, string cursor, bool targetEnemyUnits, bool targetAllyUnits)
: base(order, priority, cursor, targetEnemyUnits, targetAllyUnits) : base(order, priority, cursor, targetEnemyUnits, targetAllyUnits)
{ {
this.targetTypes = targetTypes; this.targetTypes = targetTypes;
@@ -77,7 +77,7 @@ namespace OpenRA.Mods.Common.Orders
public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor) public override bool CanTargetActor(Actor self, Actor target, TargetModifiers modifiers, ref string cursor)
{ {
return target.TraitsImplementing<ITargetable>().Any(t => t.IsTraitEnabled() && t.TargetTypes.Overlaps(targetTypes)); return targetTypes.Overlaps(target.GetEnabledTargetTypes());
} }
public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor)

View File

@@ -189,7 +189,12 @@ namespace OpenRA.Mods.Common.Traits
if (Info.AttackRequiresEnteringCell && !positionable.Value.CanEnterCell(t.Actor.Location, null, false)) if (Info.AttackRequiresEnteringCell && !positionable.Value.CanEnterCell(t.Actor.Location, null, false))
return false; return false;
return Armaments.Any(a => a.Weapon.IsValidAgainst(t, self.World, self)); // PERF: Avoid LINQ.
foreach (var armament in Armaments)
if (armament.Weapon.IsValidAgainst(t, self.World, self))
return true;
return false;
} }
public WDist GetMinimumRange() public WDist GetMinimumRange()
@@ -197,9 +202,17 @@ namespace OpenRA.Mods.Common.Traits
if (IsTraitDisabled) if (IsTraitDisabled)
return WDist.Zero; return WDist.Zero;
var min = Armaments.Where(a => !a.IsTraitDisabled) // PERF: Avoid LINQ.
.Select(a => a.Weapon.MinRange) var min = WDist.MaxValue;
.Append(WDist.MaxValue).Min(); foreach (var armament in Armaments)
{
if (armament.IsTraitDisabled)
continue;
var range = armament.Weapon.MinRange;
if (min > range)
min = range;
}
return min != WDist.MaxValue ? min : WDist.Zero; return min != WDist.MaxValue ? min : WDist.Zero;
} }
@@ -208,23 +221,32 @@ namespace OpenRA.Mods.Common.Traits
if (IsTraitDisabled) if (IsTraitDisabled)
return WDist.Zero; return WDist.Zero;
return Armaments.Where(a => !a.IsTraitDisabled) // PERF: Avoid LINQ.
.Select(a => a.MaxRange()) var max = WDist.Zero;
.Append(WDist.Zero).Max(); foreach (var armament in Armaments)
{
if (armament.IsTraitDisabled)
continue;
var range = armament.MaxRange();
if (max < range)
max = range;
}
return max;
} }
// Enumerates all armaments, that this actor possesses, that can be used against Target t // Enumerates all armaments, that this actor possesses, that can be used against Target t
public IEnumerable<Armament> ChooseArmamentsForTarget(Target t, bool forceAttack, bool onlyEnabled = true) public IEnumerable<Armament> ChooseArmamentsForTarget(Target t, bool forceAttack)
{ {
// If force-fire is not used, and the target requires force-firing or the target is // If force-fire is not used, and the target requires force-firing or the target is
// terrain or invalid, no armaments can be used // terrain or invalid, no armaments can be used
if (!forceAttack && (t.RequiresForceFire || t.Type == TargetType.Terrain || t.Type == TargetType.Invalid)) if (!forceAttack && (t.Type == TargetType.Terrain || t.Type == TargetType.Invalid || t.RequiresForceFire))
return Enumerable.Empty<Armament>(); return Enumerable.Empty<Armament>();
// Get target's owner; in case of terrain or invalid target there will be no problems // Get target's owner; in case of terrain or invalid target there will be no problems
// with owner == null since forceFire will have to be true in this part of the method // with owner == null since forceFire will have to be true in this part of the method
// (short-circuiting in the logical expression below) // (short-circuiting in the logical expression below)
var owner = null as Player; Player owner = null;
if (t.Type == TargetType.FrozenActor) if (t.Type == TargetType.FrozenActor)
{ {
owner = t.FrozenActor.Owner; owner = t.FrozenActor.Owner;
@@ -241,10 +263,11 @@ namespace OpenRA.Mods.Common.Traits
owner = t.Actor.Owner; owner = t.Actor.Owner;
} }
return Armaments.Where(a => (!a.IsTraitDisabled || !onlyEnabled) return Armaments.Where(a =>
&& a.Weapon.IsValidAgainst(t, self.World, self) !a.IsTraitDisabled
&& (owner == null || (forceAttack ? a.Info.ForceTargetStances : a.Info.TargetStances) && (owner == null || (forceAttack ? a.Info.ForceTargetStances : a.Info.TargetStances)
.HasStance(self.Owner.Stances[owner]))); .HasStance(self.Owner.Stances[owner]))
&& a.Weapon.IsValidAgainst(t, self.World, self));
} }
public void AttackTarget(Target target, bool queued, bool allowMove, bool forceAttack = false) public void AttackTarget(Target target, bool queued, bool allowMove, bool forceAttack = false)

View File

@@ -135,24 +135,21 @@ namespace OpenRA.Mods.Common.Traits
--nextScanTime; --nextScanTime;
} }
public Actor ScanForTarget(Actor self, Actor currentTarget, bool allowMove) public Actor ScanForTarget(Actor self, bool allowMove)
{ {
if (nextScanTime <= 0) if (nextScanTime <= 0)
{ {
nextScanTime = self.World.SharedRandom.Next(info.MinimumScanTimeInterval, info.MaximumScanTimeInterval);
var range = info.ScanRadius > 0 ? WDist.FromCells(info.ScanRadius) : attack.GetMaximumRange(); var range = info.ScanRadius > 0 ? WDist.FromCells(info.ScanRadius) : attack.GetMaximumRange();
if (self.IsIdle || currentTarget == null || !Target.FromActor(currentTarget).IsInRange(self.CenterPosition, range)) return ChooseTarget(self, range, allowMove);
{
nextScanTime = self.World.SharedRandom.Next(info.MinimumScanTimeInterval, info.MaximumScanTimeInterval);
return ChooseTarget(self, range, allowMove);
}
} }
return currentTarget; return null;
} }
public void ScanAndAttack(Actor self, bool allowMove) public void ScanAndAttack(Actor self, bool allowMove)
{ {
var targetActor = ScanForTarget(self, null, allowMove); var targetActor = ScanForTarget(self, allowMove);
if (targetActor != null) if (targetActor != null)
Attack(self, targetActor, allowMove); Attack(self, targetActor, allowMove);
} }
@@ -167,43 +164,57 @@ namespace OpenRA.Mods.Common.Traits
Actor ChooseTarget(Actor self, WDist range, bool allowMove) Actor ChooseTarget(Actor self, WDist range, bool allowMove)
{ {
var inRange = self.World.FindActorsInCircle(self.CenterPosition, range) var actorsByArmament = new Dictionary<Armament, List<Actor>>();
.Where(a => var actorsInRange = self.World.FindActorsInCircle(self.CenterPosition, range);
!a.TraitsImplementing<IPreventsAutoTarget>().Any(t => t.PreventsAutoTarget(a, self))); foreach (var actor in actorsInRange)
{
if (PreventsAutoTarget(self, actor) || !self.Owner.CanTargetActor(actor))
continue;
// Select only the first compatible armament for each actor: if this actor is selected
// it will be thanks to the first armament anyways, since that is the first selection
// criterion
var target = Target.FromActor(actor);
var armaments = attack.ChooseArmamentsForTarget(target, false);
if (!allowMove)
armaments = armaments.Where(arm =>
target.IsInRange(self.CenterPosition, arm.MaxRange()) &&
!target.IsInRange(self.CenterPosition, arm.Weapon.MinRange));
var armament = armaments.FirstOrDefault();
if (armament == null)
continue;
List<Actor> actors;
if (actorsByArmament.TryGetValue(armament, out actors))
actors.Add(actor);
else
actorsByArmament.Add(armament, new List<Actor> { actor });
}
// Armaments are enumerated in attack.Armaments in construct order // Armaments are enumerated in attack.Armaments in construct order
// When autotargeting, first choose targets according to the used armament construct order // When autotargeting, first choose targets according to the used armament construct order
// And then according to distance from actor // And then according to distance from actor
// This enables preferential treatment of certain armaments // This enables preferential treatment of certain armaments
// (e.g. tesla trooper's tesla zap should have precedence over tesla charge) // (e.g. tesla trooper's tesla zap should have precedence over tesla charge)
var actorByArmament = inRange
// Select only the first compatible armament for each actor: if this actor is selected
// it will be thanks to the first armament anyways, since that is the first selection
// criterion
.Select(a =>
{
var target = Target.FromActor(a);
return new KeyValuePair<Armament, Actor>(
attack.ChooseArmamentsForTarget(target, false)
.FirstOrDefault(arm => allowMove
|| (target.IsInRange(self.CenterPosition, arm.MaxRange())
&& !target.IsInRange(self.CenterPosition, arm.Weapon.MinRange))), a);
})
.Where(kv => kv.Key != null && self.Owner.CanTargetActor(kv.Value))
.GroupBy(kv => kv.Key, kv => kv.Value)
.ToDictionary(kv => kv.Key, kv => kv.ClosestTo(self));
foreach (var arm in attack.Armaments) foreach (var arm in attack.Armaments)
{ {
Actor actor; List<Actor> actors;
if (actorByArmament.TryGetValue(arm, out actor)) if (actorsByArmament.TryGetValue(arm, out actors))
return actor; return actors.ClosestTo(self);
} }
return null; return null;
} }
bool PreventsAutoTarget(Actor attacker, Actor target)
{
foreach (var pat in target.TraitsImplementing<IPreventsAutoTarget>())
if (pat.PreventsAutoTarget(target, attacker))
return true;
return false;
}
} }
[Desc("Will not get automatically targeted by enemy (like walls)")] [Desc("Will not get automatically targeted by enemy (like walls)")]

View File

@@ -58,8 +58,7 @@ namespace OpenRA.Mods.Common.Traits
if (info.ValidFactions.Any() && !info.ValidFactions.Contains(collector.Owner.Faction.InternalName)) if (info.ValidFactions.Any() && !info.ValidFactions.Contains(collector.Owner.Faction.InternalName))
return false; return false;
var targetable = collector.TraitsImplementing<ITargetable>().Where(Exts.IsTraitEnabled); if (!info.ValidTargets.Overlaps(collector.GetEnabledTargetTypes()))
if (!info.ValidTargets.Overlaps(targetable.SelectMany(t => t.TargetTypes)))
return false; return false;
var positionable = collector.TraitOrDefault<IPositionable>(); var positionable = collector.TraitOrDefault<IPositionable>();

View File

@@ -56,8 +56,7 @@ namespace OpenRA.Mods.Common.Warheads
return false; return false;
// A target type is valid if it is in the valid targets list, and not in the invalid targets list. // A target type is valid if it is in the valid targets list, and not in the invalid targets list.
var targetable = victim.TraitsImplementing<ITargetable>().Where(Exts.IsTraitEnabled); if (!IsValidTarget(victim.GetEnabledTargetTypes()))
if (!IsValidTarget(targetable.SelectMany(t => t.TargetTypes)))
return false; return false;
return true; return true;

View File

@@ -44,7 +44,7 @@ namespace OpenRA.Mods.RA.Traits
{ {
get get
{ {
yield return new TargetTypeOrderTargeter(new[] { "DetonateAttack" }, "DetonateAttack", 5, "attack", true, false) { ForceAttack = false }; yield return new TargetTypeOrderTargeter(new HashSet<string> { "DetonateAttack" }, "DetonateAttack", 5, "attack", true, false) { ForceAttack = false };
yield return new DeployOrderTargeter("Detonate", 5); yield return new DeployOrderTargeter("Detonate", 5);
} }
} }

View File

@@ -94,7 +94,7 @@ namespace OpenRA.Mods.RA.Traits
{ {
get get
{ {
yield return new TargetTypeOrderTargeter(new[] { "Disguise" }, "Disguise", 7, "ability", true, true) { ForceAttack = false }; yield return new TargetTypeOrderTargeter(new HashSet<string> { "Disguise" }, "Disguise", 7, "ability", true, true) { ForceAttack = false };
} }
} }

View File

@@ -77,8 +77,7 @@ namespace OpenRA.Mods.RA.Traits
targetTypes = frozen.TargetTypes; targetTypes = frozen.TargetTypes;
} }
else else
targetTypes = order.TargetActor.TraitsImplementing<ITargetable>().Where(Exts.IsTraitEnabled) targetTypes = order.TargetActor.GetEnabledTargetTypes();
.SelectMany(t => t.TargetTypes);
return Info.Types.Overlaps(targetTypes); return Info.Types.Overlaps(targetTypes);
} }
@@ -96,7 +95,7 @@ namespace OpenRA.Mods.RA.Traits
var target = self.ResolveFrozenActorOrder(order, Color.Red); var target = self.ResolveFrozenActorOrder(order, Color.Red);
if (target.Type != TargetType.Actor if (target.Type != TargetType.Actor
|| !Info.Types.Overlaps(target.Actor.TraitsImplementing<ITargetable>().SelectMany(t => t.TargetTypes))) || !Info.Types.Overlaps(target.Actor.GetAllTargetTypes()))
return; return;
if (!order.Queued) if (!order.Queued)
@@ -124,7 +123,7 @@ namespace OpenRA.Mods.RA.Traits
if (!info.ValidStances.HasStance(stance)) if (!info.ValidStances.HasStance(stance))
return false; return false;
return target.TraitsImplementing<ITargetable>().Any(t => t.TargetTypes.Overlaps(info.Types)); return info.Types.Overlaps(target.GetAllTargetTypes());
} }
public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor) public override bool CanTargetFrozenActor(Actor self, FrozenActor target, TargetModifiers modifiers, ref string cursor)
@@ -134,7 +133,7 @@ namespace OpenRA.Mods.RA.Traits
if (!info.ValidStances.HasStance(stance)) if (!info.ValidStances.HasStance(stance))
return false; return false;
return target.Info.TraitInfos<ITargetableInfo>().Any(t => info.Types.Overlaps(t.GetTargetTypes())); return info.Types.Overlaps(target.Info.TraitInfos<ITargetableInfo>().SelectMany(ti => ti.GetTargetTypes()));
} }
} }
} }

View File

@@ -98,7 +98,7 @@ namespace OpenRA.Mods.RA.Traits
{ {
get get
{ {
yield return new TargetTypeOrderTargeter(new[] { "DetonateAttack" }, "DetonateAttack", 5, "attack", true, false) { ForceAttack = false }; yield return new TargetTypeOrderTargeter(new HashSet<string> { "DetonateAttack" }, "DetonateAttack", 5, "attack", true, false) { ForceAttack = false };
yield return new DeployOrderTargeter("Detonate", 5); yield return new DeployOrderTargeter("Detonate", 5);
} }
} }