diff --git a/OpenRA.Mods.CA/Traits/GuardsSelection.cs b/OpenRA.Mods.CA/Traits/GuardsSelection.cs index 572cd60c4b..fea6a23a51 100644 --- a/OpenRA.Mods.CA/Traits/GuardsSelection.cs +++ b/OpenRA.Mods.CA/Traits/GuardsSelection.cs @@ -10,60 +10,84 @@ using System.Collections.Generic; using System.Linq; +using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Traits; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.CA.Traits { - [Desc("Attach to support unit so that when ordered as part of a group with combat units it will guard those units.")] + [Desc("Attach to support unit so that when ordered as part of a group with combat units it will guard those units when Attack and AttackMove.")] class GuardsSelectionInfo : ConditionalTraitInfo { + [CursorReference] + [Desc("Cursor to display when hovering over a valid target.")] + public readonly string Cursor = "guard"; + + public readonly PlayerRelationship TargetRelationships = PlayerRelationship.Enemy; + public readonly PlayerRelationship ForceTargetRelationships = PlayerRelationship.Enemy | PlayerRelationship.Neutral | PlayerRelationship.Ally; + + [Desc("Cells to keep distance to target when there is no guardable.")] + public readonly WDist DistanceWhenNoGuardable = WDist.FromCells(7); + [Desc("Will only guard units with these target types.")] - public readonly BitSet ValidTargets = new BitSet("Ground", "Water"); + public readonly BitSet ValidTargetsToGuard = new("Ground", "Water"); + + [Desc("Will not guard units with these target types.")] + public readonly BitSet InvalidTargetsToGuard = default; [Desc("Maximum number of guard orders to chain together.")] - public readonly int MaxTargets = 10; + public readonly int MaxGuardingTargets = 10; + + [Desc("Orders to override to guard ally unit in selection. Use AttackGuards if you need override Attack/ForceAttack order.")] + public readonly HashSet OverrideOrders = new() { "AttackMove", "AssaultMove", "AttackGuards" }; - [Desc("Color to use for the target line.")] - public readonly Color TargetLineColor = Color.OrangeRed; + [Desc("Guard ally closest to target when distance between smaller than this value, otherwise choose ally closest to this actor.")] + public readonly int ChooseClosestAllyRangeCells = 7; - [Desc("Maximum range that guarding actors will maintain.")] - public readonly WDist Range = WDist.FromCells(2); + [Desc("When there are units with " + nameof(GuardsSelection) + " in player's selection, the one with higher level will guards the one with lower level.")] + public readonly int GuardsSelectionLevel = 1; - public override object Create(ActorInitializer init) { return new GuardsSelection(init, this); } + public override object Create(ActorInitializer init) { return new GuardsSelection(this); } } - class GuardsSelection : ConditionalTrait, IResolveOrder, INotifyCreated + class GuardsSelection : ConditionalTrait, IResolveOrder, INotifyCreated, IIssueOrder { - IMove move; + AttackBase[] attackBases; + IMoveInfo moveInfo; - public GuardsSelection(ActorInitializer init, GuardsSelectionInfo info) + public GuardsSelection(GuardsSelectionInfo info) : base(info) { } protected override void Created(Actor self) { - move = self.Trait(); + attackBases = self.TraitsImplementing().ToArray(); + moveInfo = self.Info.TraitInfoOrDefault(); base.Created(self); } - void IResolveOrder.ResolveOrder(Actor self, Order order) + IEnumerable IIssueOrder.Orders { - if (IsTraitDisabled) - return; - - if (order.Target.Type == TargetType.Invalid) - return; + get + { + if (IsTraitDisabled || !Info.OverrideOrders.Contains("AttackGuards")) + yield break; - if (order.Queued) - return; + yield return new AttackGuardOrderTargeter(this, 6); + } + } - var validOrders = new HashSet { "AttackMove", "AssaultMove", "Attack", "ForceAttack", "KeepDistance" }; + Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) + { + if (order is AttackGuardOrderTargeter) + return new Order(order.OrderID, self, target, queued); - if (!validOrders.Contains(order.OrderString)) - return; + return null; + } - if (self.Owner.IsBot) + void IResolveOrder.ResolveOrder(Actor self, Order order) + { + if (IsTraitDisabled || order.Target.Type == TargetType.Invalid || order.Queued || self.Owner.IsBot) return; var world = self.World; @@ -71,50 +95,175 @@ void IResolveOrder.ResolveOrder(Actor self, Order order) if (order.Target.Type == TargetType.Actor && (order.Target.Actor.Disposed || order.Target.Actor.Owner == self.Owner || !order.Target.Actor.IsInWorld || order.Target.Actor.IsDead)) return; - var guardActors = world.Selection.Actors + if (order.OrderString == "AttackGuardsWithinRange") + { + self.QueueActivity(order.Queued, new MoveWithinRange(self, order.Target, WDist.Zero, Info.DistanceWhenNoGuardable, targetLineColor: moveInfo.GetTargetLineColor())); + return; + } + else if (!Info.OverrideOrders.Contains(order.OrderString)) + return; + + var guardableActors = world.Selection.Actors .Where(a => a.Owner == self.Owner && !a.Disposed && !a.IsDead && a.IsInWorld && a != self - && IsValidGuardTarget(a)) + && IsValidGuardableTarget(a)) + .OrderBy(a => (a.Location - self.Location).LengthSquared) .ToArray(); - if (guardActors.Length == 0) + // When no actor can be guarded in AttackGuards, move close to target + if (guardableActors.Length == 0) + { + if (order.OrderString == "AttackGuards") + world.IssueOrder(new Order("AttackGuardsWithinRange", self, order.Target, false, null, null)); + return; + } + + // We find candidates that within "ChooseClosestAllyRangeCells" to guard at highest priority. + var minDest = long.MaxValue; + var candidate = 0; + for (var i = 0; i < guardableActors.Length; i++) + { + if ((guardableActors[i].Location - self.Location).LengthSquared <= Info.ChooseClosestAllyRangeCells * Info.ChooseClosestAllyRangeCells) + { + var dist = (guardableActors[i].CenterPosition - order.Target.CenterPosition).HorizontalLengthSquared; + if (dist < minDest) + { + minDest = dist; + var a = guardableActors[i]; + guardableActors[i] = guardableActors[candidate]; + guardableActors[candidate] = a; + candidate++; + } + } + } - var mainGuardActor = guardActors.ClosestTo(order.Target.CenterPosition); + var mainGuardActor = guardableActors[--candidate > 0 ? candidate : candidate = 0]; if (mainGuardActor == null) return; - var mainGuardTarget = Target.FromActor(mainGuardActor); - world.IssueOrder(new Order("Guard", self, mainGuardTarget, false, null, null)); + world.IssueOrder(new Order("Guard", self, Target.FromActor(mainGuardActor), false, null, null)); - var guardTargets = 0; + for (var i = 0; i < candidate && i < Info.MaxGuardingTargets; i++) + world.IssueOrder(new Order("Guard", self, Target.FromActor(guardableActors[candidate - i - 1]), true, null, null)); - foreach (var guardActor in guardActors) - { - guardTargets++; - world.IssueOrder(new Order("Guard", self, Target.FromActor(guardActor), true, null, null)); - - if (guardTargets >= Info.MaxTargets) - break; - } + for (var i = candidate + 1; i < guardableActors.Length && i < Info.MaxGuardingTargets; i++) + world.IssueOrder(new Order("Guard", self, Target.FromActor(guardableActors[i]), true, null, null)); } - bool IsValidGuardTarget(Actor targetActor) + bool IsValidGuardableTarget(Actor targetActor) { - if (!Info.ValidTargets.Overlaps(targetActor.GetEnabledTargetTypes())) + var targets = targetActor.GetEnabledTargetTypes(); + if (!Info.ValidTargetsToGuard.Overlaps(targets) || Info.InvalidTargetsToGuard.Overlaps(targets)) return false; - if (!targetActor.Info.HasTraitInfo()) + if (!targetActor.Info.HasTraitInfo()) return false; var guardsSelection = targetActor.TraitsImplementing(); - if (guardsSelection.Any(t => !t.IsTraitDisabled)) + if (guardsSelection.Any(t => !t.IsTraitDisabled && Info.GuardsSelectionLevel <= t.Info.GuardsSelectionLevel)) return false; return true; } + + public bool CanAttackGuard(Actor self, Target t, bool forceAttack) + { + // If force-fire is not used, and the target requires force-firing or the target is + // terrain or invalid, we will just ignore it. + if (t.Type == TargetType.Invalid || (!forceAttack && (t.Type == TargetType.Terrain || t.RequiresForceFire))) + return false; + + // 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 + // (short-circuiting in the logical expression below) + Player owner = null; + if (t.Type == TargetType.FrozenActor) + owner = t.FrozenActor.Owner; + else if (t.Type == TargetType.Actor) + owner = t.Actor.Owner; + + return (owner == null || (forceAttack ? Info.ForceTargetRelationships : Info.TargetRelationships).HasRelationship(self.Owner.RelationshipWith(owner))) + && !attackBases.Any(ab => !ab.IsTraitDisabled && !ab.IsTraitPaused && ab.Armaments.Any(a => !a.IsTraitDisabled && !a.IsTraitPaused && a.Weapon.IsValidAgainst(t, self.World, self))); + } + } + + sealed class AttackGuardOrderTargeter : IOrderTargeter + { + readonly GuardsSelection gs; + + public AttackGuardOrderTargeter(GuardsSelection gs, int priority) + { + this.gs = gs; + OrderID = "AttackGuards"; + OrderPriority = priority; + } + + public string OrderID { get; private set; } + public int OrderPriority { get; } + public bool TargetOverridesSelection(Actor self, in Target target, List actorsAt, CPos xy, TargetModifiers modifiers) { return true; } + + bool CanTargetActor(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor) + { + IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue); + + if (modifiers.HasModifier(TargetModifiers.ForceMove)) + return false; + + // Disguised actors are revealed by the attack cursor + // HACK: works around limitations in the targeting code that force the + // targeting and attacking logic (which should be logically separate) + // to use the same code + if (target.Type == TargetType.Actor && target.Actor.EffectiveOwner != null && + target.Actor.EffectiveOwner.Disguised && self.Owner.RelationshipWith(target.Actor.Owner) == PlayerRelationship.Enemy) + modifiers |= TargetModifiers.ForceAttack; + + if (!gs.CanAttackGuard(self, target, modifiers.HasModifier(TargetModifiers.ForceAttack))) + return false; + + cursor = gs.Info.Cursor; + + return true; + } + + bool CanTargetLocation(Actor self, CPos location, TargetModifiers modifiers, ref string cursor) + { + if (!self.World.Map.Contains(location)) + return false; + + IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue); + + // Targeting the terrain is only possible with force-attack modifier + if (modifiers.HasModifier(TargetModifiers.ForceMove)) + return false; + + var target = Target.FromCell(self.World, location); + + if (!gs.CanAttackGuard(self, target, modifiers.HasModifier(TargetModifiers.ForceAttack))) + return false; + + cursor = gs.Info.Cursor; + + return true; + } + + public bool CanTarget(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor) + { + switch (target.Type) + { + case TargetType.Actor: + case TargetType.FrozenActor: + return CanTargetActor(self, target, ref modifiers, ref cursor); + case TargetType.Terrain: + return CanTargetLocation(self, self.World.Map.CellContaining(target.CenterPosition), modifiers, ref cursor); + default: + return false; + } + } + + public bool IsQueued { get; private set; } } } diff --git a/OpenRA.Mods.CA/Traits/KeepsDistance.cs b/OpenRA.Mods.CA/Traits/KeepsDistance.cs deleted file mode 100644 index 742a794e51..0000000000 --- a/OpenRA.Mods.CA/Traits/KeepsDistance.cs +++ /dev/null @@ -1,112 +0,0 @@ -#region Copyright & License Information -/** - * Copyright (c) The OpenRA Combined Arms Developers (see CREDITS). - * This file is part of OpenRA Combined Arms, which is free software. - * It is made available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 of the License, - * or (at your option) any later version. For more information, see COPYING. - */ -#endregion - -using System.Collections.Generic; -using OpenRA.Mods.Common.Traits; -using OpenRA.Traits; - -namespace OpenRA.Mods.CA.Traits -{ - [Desc("Will keep distance from enemies that the unit can't attack.")] - class KeepsDistanceInfo : ConditionalTraitInfo - { - [Desc("Cells to keep distance.")] - public readonly WDist Distance = WDist.FromCells(7); - - [CursorReference] - [Desc("Cursor to display when targeting an actor to keep distance from.")] - public readonly string Cursor = "move"; - - public override object Create(ActorInitializer init) { return new KeepsDistance(init, this); } - } - - class KeepsDistance : ConditionalTrait, IIssueOrder, IResolveOrder - { - readonly IMove move; - readonly IMoveInfo moveInfo; - - public KeepsDistance(ActorInitializer init, KeepsDistanceInfo info) - : base(info) - { - move = init.Self.TraitOrDefault(); - moveInfo = init.Self.Info.TraitInfoOrDefault(); - } - - IEnumerable IIssueOrder.Orders - { - get - { - if (IsTraitDisabled) - yield break; - - yield return new KeepDistanceOrderTargeter(Info.Cursor); - } - } - - Order IIssueOrder.IssueOrder(Actor self, IOrderTargeter order, in Target target, bool queued) - { - if (order is KeepDistanceOrderTargeter) - return new Order(order.OrderID, self, target, queued); - - return null; - } - - void IResolveOrder.ResolveOrder(Actor self, Order order) - { - if (order.OrderString == "KeepDistance") - { - self.QueueActivity(order.Queued, move.MoveWithinRange(order.Target, Info.Distance, targetLineColor: moveInfo.GetTargetLineColor())); - self.ShowTargetLines(); - } - } - - class KeepDistanceOrderTargeter : IOrderTargeter - { - readonly string cursor; - - public KeepDistanceOrderTargeter(string cursor) - { - this.cursor = cursor; - } - - public string OrderID => "KeepDistance"; - public int OrderPriority => 5; - public bool TargetOverridesSelection(Actor self, in Target target, List actorsAt, CPos xy, TargetModifiers modifiers) { return true; } - - bool CanTargetActor(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor) - { - IsQueued = modifiers.HasModifier(TargetModifiers.ForceQueue); - - if (modifiers.HasModifier(TargetModifiers.ForceMove)) - return false; - - cursor = this.cursor; - var targetOwner = target.Type == TargetType.Actor ? target.Actor.Owner : target.FrozenActor.Owner; - return self.Owner.RelationshipWith(targetOwner) == PlayerRelationship.Enemy; - } - - public bool CanTarget(Actor self, in Target target, ref TargetModifiers modifiers, ref string cursor) - { - switch (target.Type) - { - case TargetType.Actor: - case TargetType.FrozenActor: - return CanTargetActor(self, target, ref modifiers, ref cursor); - case TargetType.Terrain: - return false; - default: - return false; - } - } - - public bool IsQueued { get; protected set; } - } - } -} diff --git a/mods/ca/maps/ca28-illumination/rules.yaml b/mods/ca/maps/ca28-illumination/rules.yaml index c684aefd6e..6401414d18 100644 --- a/mods/ca/maps/ca28-illumination/rules.yaml +++ b/mods/ca/maps/ca28-illumination/rules.yaml @@ -102,7 +102,9 @@ KANE: RenderDetectionCircle: Color: ffffff20 BorderColor: 00000020 - KeepsDistance: + GuardsSelection: + ValidTargetsToGuard: Ground + InvalidTargetsToGuard: Air CaptureManager: Captures: CaptureTypes: building diff --git a/mods/ca/rules/infantry.yaml b/mods/ca/rules/infantry.yaml index 4ec82ea427..f6c4c44836 100644 --- a/mods/ca/rules/infantry.yaml +++ b/mods/ca/rules/infantry.yaml @@ -904,8 +904,8 @@ MEDI: Targetable@ChaosImmune: TargetTypes: ChaosImmune GuardsSelection: - ValidTargets: Infantry - KeepsDistance: + ValidTargetsToGuard: Infantry + GuardsSelectionLevel: 2 MECH: Inherits: ^Soldier @@ -995,12 +995,12 @@ MECH: Targetable@ChaosImmune: TargetTypes: ChaosImmune GuardsSelection: - ValidTargets: Vehicle + ValidTargetsToGuard: Vehicle + GuardsSelectionLevel: 2 Convertible: SpawnActors: CMEC ReplacedInQueue: Actors: cmec - KeepsDistance: CMEC: Inherits: ^Cyborg @@ -1088,9 +1088,9 @@ CMEC: Types: Veterancy Prerequisites: barracks.upgraded GuardsSelection: - ValidTargets: Vehicle + ValidTargetsToGuard: Vehicle + GuardsSelectionLevel: 2 -TakeCover: - KeepsDistance: HACK: Inherits: ^Soldier @@ -3832,9 +3832,12 @@ YURI: Range: 13c0 Color: cc00ff77 BorderColor: 00000044 - KeepsDistance: ChangesHealth@ELITE: Delay: 50 + GuardsSelection: + ValidTargetsToGuard: Ground + InvalidTargetsToGuard: Air + OverrideOrders: AttackGuards SEAL: Inherits: ^Soldier diff --git a/mods/ca/rules/scrin.yaml b/mods/ca/rules/scrin.yaml index da4945c91f..362edb030c 100644 --- a/mods/ca/rules/scrin.yaml +++ b/mods/ca/rules/scrin.yaml @@ -664,8 +664,8 @@ SMEDI: Targetable: TargetTypes: Ground, Infantry, ChaosImmune GuardsSelection: - ValidTargets: Infantry - KeepsDistance: + ValidTargetsToGuard: Infantry + GuardsSelectionLevel: 2 GSCR: Inherits: BRUT diff --git a/mods/ca/rules/vehicles.yaml b/mods/ca/rules/vehicles.yaml index ac42aeced5..0445f1c04c 100644 --- a/mods/ca/rules/vehicles.yaml +++ b/mods/ca/rules/vehicles.yaml @@ -5699,13 +5699,23 @@ IFV: Condition: cryr-upgrade Prerequisites: cryr.upgrade GuardsSelection@REPAIR: - ValidTargets: Vehicle + ValidTargetsToGuard: Vehicle + GuardsSelectionLevel: 2 RequiresCondition: engturr && !stance-attackanything GuardsSelection@MEDIC: - ValidTargets: Infantry + ValidTargetsToGuard: Infantry + GuardsSelectionLevel: 2 RequiresCondition: medturr && !stance-attackanything GuardsSelection@SPY: + ValidTargetsToGuard: Ground + InvalidTargetsToGuard: Air + GuardsSelectionLevel: 2 RequiresCondition: spyturr && !stance-attackanything + GuardsSelection@COMM: + ValidTargetsToGuard: Ground + InvalidTargetsToGuard: Air + OverrideOrders: AttackGuards + RequiresCondition: commturr && !stance-attackanything WithDecoration@COMMANDOSKULL: RequiresCondition: commturr || psyturr WithDecoration@SEALSKULL: @@ -5715,8 +5725,6 @@ IFV: Palette: effect Position: TopLeft ValidRelationships: Ally, Enemy, Neutral - KeepsDistance: - RequiresCondition: engturr || medturr || spyturr DamageTypeDamageMultiplier@A2GPROTECTION: RequiresCondition: !full || samturr || ggiturr AmbientSoundCA: @@ -7759,7 +7767,10 @@ MANT: Range: 8c0 DetectionTypes: AirCloak RequiresCondition: empdisable || being-warped - KeepsDistance: + GuardsSelection: + ValidTargetsToGuard: Ground + InvalidTargetsToGuard: Air + OverrideOrders: AttackGuards VIPR: Inherits: ^VehicleTD