From c34dd4b8249a5ef3c4fd4ea06ddd35560e3f1fa2 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 16 Dec 2018 13:01:01 +1300 Subject: [PATCH] Allow Attack activities to target FrozenActors directly. Removing the legacy FrozenActor to Actor workaround fixes a number of long-standing bugs. This also prevents units from losing their target when it transforms into a different actor type. --- .../Activities/Air/FlyAttack.cs | 4 +- OpenRA.Mods.Common/Activities/Attack.cs | 108 +++++++++++++----- .../Traits/Attack/AttackBase.cs | 13 ++- .../Traits/Attack/AttackFollow.cs | 28 ++++- .../Traits/Attack/AttackFrontal.cs | 2 +- .../Traits/Attack/AttackOmni.cs | 4 +- OpenRA.Mods.D2k/Traits/AttackSwallow.cs | 13 ++- mods/cnc/rules/vehicles.yaml | 2 + mods/ra/rules/ships.yaml | 2 + mods/ra/rules/vehicles.yaml | 2 + mods/ts/rules/gdi-vehicles.yaml | 1 + mods/ts/rules/nod-vehicles.yaml | 1 + 12 files changed, 133 insertions(+), 47 deletions(-) diff --git a/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs b/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs index 868e2d250d..f5b9774ced 100644 --- a/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs +++ b/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs @@ -18,10 +18,10 @@ namespace OpenRA.Mods.Common.Activities { public class FlyAttack : Activity { - readonly Target target; readonly Aircraft aircraft; readonly AttackAircraft attackAircraft; readonly Rearmable rearmable; + Target target; int ticksUntilTurn; @@ -43,6 +43,8 @@ namespace OpenRA.Mods.Common.Activities return NextActivity; } + target = target.Recalculate(self.Owner); + if (!target.IsValidFor(self)) return NextActivity; diff --git a/OpenRA.Mods.Common/Activities/Attack.cs b/OpenRA.Mods.Common/Activities/Attack.cs index 19955e3928..319913c2b7 100644 --- a/OpenRA.Mods.Common/Activities/Attack.cs +++ b/OpenRA.Mods.Common/Activities/Attack.cs @@ -23,13 +23,13 @@ namespace OpenRA.Mods.Common.Activities [Flags] protected enum AttackStatus { UnableToAttack, NeedsToTurn, NeedsToMove, Attacking } - protected readonly Target Target; - readonly AttackBase[] attackTraits; + readonly AttackFrontal[] attackTraits; + readonly RevealsShroud[] revealsShroud; readonly IMove move; readonly IFacing facing; readonly IPositionable positionable; readonly bool forceAttack; - readonly int facingTolerance; + protected Target target; WDist minRange; WDist maxRange; @@ -37,22 +37,27 @@ namespace OpenRA.Mods.Common.Activities Activity moveActivity; AttackStatus attackStatus = AttackStatus.UnableToAttack; - public Attack(Actor self, Target target, bool allowMovement, bool forceAttack, int facingTolerance) + public Attack(Actor self, Target target, bool allowMovement, bool forceAttack) { - Target = target; - + this.target = target; this.forceAttack = forceAttack; - this.facingTolerance = facingTolerance; - attackTraits = self.TraitsImplementing().ToArray(); + attackTraits = self.TraitsImplementing().ToArray(); + revealsShroud = self.TraitsImplementing().ToArray(); facing = self.Trait(); positionable = self.Trait(); move = allowMovement ? self.TraitOrDefault() : null; } + protected virtual Target RecalculateTarget(Actor self) + { + return target.Recalculate(self.Owner); + } + public override Activity Tick(Actor self) { + target = RecalculateTarget(self); turnActivity = moveActivity = null; attackStatus = AttackStatus.UnableToAttack; @@ -74,31 +79,37 @@ namespace OpenRA.Mods.Common.Activities return NextActivity; } - protected virtual bool IgnoresVisibility { get { return false; } } - - protected virtual AttackStatus TickAttack(Actor self, AttackBase attack) + protected virtual AttackStatus TickAttack(Actor self, AttackFrontal attack) { if (IsCanceled) return AttackStatus.UnableToAttack; - var type = Target.Type; - if (!Target.IsValidFor(self) || type == TargetType.FrozenActor) + if (!target.IsValidFor(self)) return AttackStatus.UnableToAttack; - if (attack.Info.AttackRequiresEnteringCell && !positionable.CanEnterCell(Target.Actor.Location, null, false)) + if (attack.Info.AttackRequiresEnteringCell && !positionable.CanEnterCell(target.Actor.Location, null, false)) return AttackStatus.UnableToAttack; - // Drop the target if it moves under the shroud / fog. - // HACK: This would otherwise break targeting frozen actors - // The problem is that Shroud.IsTargetable returns false (as it should) for - // frozen actors, but we do want to explicitly target the underlying actor here. - if (!IgnoresVisibility && type == TargetType.Actor - && !Target.Actor.Info.HasTraitInfo() - && !Target.Actor.CanBeViewedByPlayer(self.Owner)) - return AttackStatus.UnableToAttack; + if (!attack.Info.TargetFrozenActors && !forceAttack && target.Type == TargetType.FrozenActor) + { + // Try to move within range, drop the target otherwise + if (move == null) + return AttackStatus.UnableToAttack; + + var rs = revealsShroud + .Where(Exts.IsTraitEnabled) + .MaxByOrDefault(s => s.Range); + + // Default to 2 cells if there are no active traits + var sightRange = rs != null ? rs.Range : WDist.FromCells(2); + + attackStatus |= AttackStatus.NeedsToMove; + moveActivity = ActivityUtils.SequenceActivities(move.MoveWithinRange(target, sightRange), this); + return AttackStatus.NeedsToMove; + } // Drop the target once none of the weapons are effective against it - var armaments = attack.ChooseArmamentsForTarget(Target, forceAttack).ToList(); + var armaments = attack.ChooseArmamentsForTarget(target, forceAttack).ToList(); if (armaments.Count == 0) return AttackStatus.UnableToAttack; @@ -108,8 +119,8 @@ namespace OpenRA.Mods.Common.Activities var pos = self.CenterPosition; var mobile = move as Mobile; - if (!Target.IsInRange(pos, maxRange) - || (minRange.Length != 0 && Target.IsInRange(pos, minRange)) + if (!target.IsInRange(pos, maxRange) + || (minRange.Length != 0 && target.IsInRange(pos, minRange)) || (mobile != null && !mobile.CanInteractWithGroundLayer(self))) { // Try to move within range, drop the target otherwise @@ -117,13 +128,13 @@ namespace OpenRA.Mods.Common.Activities return AttackStatus.UnableToAttack; attackStatus |= AttackStatus.NeedsToMove; - moveActivity = ActivityUtils.SequenceActivities(move.MoveWithinRange(Target, minRange, maxRange), this); + moveActivity = ActivityUtils.SequenceActivities(move.MoveWithinRange(target, minRange, maxRange), this); return AttackStatus.NeedsToMove; } - var targetedPosition = attack.GetTargetPosition(pos, Target); + var targetedPosition = attack.GetTargetPosition(pos, target); var desiredFacing = (targetedPosition - pos).Yaw.Facing; - if (!Util.FacingWithinTolerance(facing.Facing, desiredFacing, facingTolerance)) + if (!Util.FacingWithinTolerance(facing.Facing, desiredFacing, ((AttackFrontalInfo)attack.Info).FacingTolerance)) { attackStatus |= AttackStatus.NeedsToTurn; turnActivity = ActivityUtils.SequenceActivities(new Turn(self, desiredFacing), this); @@ -131,9 +142,48 @@ namespace OpenRA.Mods.Common.Activities } attackStatus |= AttackStatus.Attacking; - attack.DoAttack(self, Target, armaments); + attack.DoAttack(self, target, armaments); return AttackStatus.Attacking; } } + + public static class TargetExts + { + /// Update (Frozen)Actor targets to account for visibility changes or actor replacement + public static Target Recalculate(this Target t, Player viewer) + { + // Check whether the target has transformed into something else + // HACK: This relies on knowing the internal implementation details of Target + if (t.Type == TargetType.Invalid && t.Actor != null && t.Actor.ReplacedByActor != null) + t = Target.FromActor(t.Actor.ReplacedByActor); + + if (t.Type == TargetType.Actor) + { + // Actor has been hidden under the fog + if (!t.Actor.CanBeViewedByPlayer(viewer)) + { + // Replace with FrozenActor if applicable, otherwise drop the target + var frozen = viewer.FrozenActorLayer.FromID(t.Actor.ActorID); + return frozen != null ? Target.FromFrozenActor(frozen) : Target.Invalid; + } + } + else if (t.Type == TargetType.FrozenActor) + { + // Frozen actor has been revealed + if (!t.FrozenActor.Visible || !t.FrozenActor.IsValid) + { + // Original actor is still alive + if (t.FrozenActor.Actor != null && t.FrozenActor.Actor.CanBeViewedByPlayer(viewer)) + return Target.FromActor(t.FrozenActor.Actor); + + // Original actor was killed while hidden + if (t.Actor == null) + return Target.Invalid; + } + } + + return t; + } + } } diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs index 99e39291b7..31367b3b23 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackBase.cs @@ -31,6 +31,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("Does the attack type require the attacker to enter the target's cell?")] public readonly bool AttackRequiresEnteringCell = false; + [Desc("Allow firing into the fog to target frozen actors without requiring force-fire.")] + public readonly bool TargetFrozenActors = false; + [VoiceReference] public readonly string Voice = "Action"; public override abstract object Create(ActorInitializer init); @@ -154,12 +157,11 @@ namespace OpenRA.Mods.Common.Traits var forceAttack = order.OrderString == forceAttackOrderName; if (forceAttack || order.OrderString == attackOrderName) { - var target = self.ResolveFrozenActorOrder(order, Color.Red); - if (!target.IsValidFor(self)) + if (!order.Target.IsValidFor(self)) return; - self.SetTargetLine(target, Color.Red); - AttackTarget(target, order.Queued, true, forceAttack); + self.SetTargetLine(order.Target, Color.Red); + AttackTarget(order.Target, order.Queued, true, forceAttack); } if (order.OrderString == "Stop") @@ -417,7 +419,8 @@ namespace OpenRA.Mods.Common.Traits if (a == null) a = armaments.First(); - cursor = !target.IsInRange(self.CenterPosition, a.MaxRange()) + cursor = !target.IsInRange(self.CenterPosition, a.MaxRange()) || + (!forceAttack && target.Type == TargetType.FrozenActor && !ab.Info.TargetFrozenActors) ? ab.Info.OutsideRangeCursor ?? a.Info.OutsideRangeCursor : ab.Info.Cursor ?? a.Info.Cursor; diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs b/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs index 8db600eb3a..33178939da 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackFollow.cs @@ -12,6 +12,7 @@ using System; using System.Linq; using OpenRA.Activities; +using OpenRA.Mods.Common.Activities; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -31,6 +32,7 @@ namespace OpenRA.Mods.Common.Traits protected override void Tick(Actor self) { + Target = Target.Recalculate(self.Owner); if (IsTraitDisabled) { Target = Target.Invalid; @@ -62,15 +64,17 @@ namespace OpenRA.Mods.Common.Traits class AttackActivity : Activity { readonly AttackFollow attack; + readonly RevealsShroud[] revealsShroud; readonly IMove move; - readonly Target target; readonly bool forceAttack; + Target target; bool hasTicked; public AttackActivity(Actor self, Target target, bool allowMove, bool forceAttack) { attack = self.Trait(); move = allowMove ? self.TraitOrDefault() : null; + revealsShroud = self.TraitsImplementing().ToArray(); this.target = target; this.forceAttack = forceAttack; @@ -78,6 +82,7 @@ namespace OpenRA.Mods.Common.Traits public override Activity Tick(Actor self) { + target = target.Recalculate(self.Owner); if (IsCanceled || !target.IsValidFor(self)) return NextActivity; @@ -87,6 +92,11 @@ namespace OpenRA.Mods.Common.Traits var weapon = attack.ChooseArmamentsForTarget(target, forceAttack).FirstOrDefault(); if (weapon != null) { + // Check that AttackFollow hasn't cancelled the target by modifying attack.Target + // Having both this and AttackFollow modify that field is a horrible hack. + if (hasTicked && attack.Target.Type == TargetType.Invalid) + return NextActivity; + var targetIsMobile = (target.Type == TargetType.Actor && target.Actor.Info.HasTraitInfo()) || (target.Type == TargetType.FrozenActor && target.FrozenActor.Info.HasTraitInfo()); @@ -95,10 +105,18 @@ namespace OpenRA.Mods.Common.Traits var maxRange = targetIsMobile ? new WDist(Math.Max(weapon.Weapon.MinRange.Length, modifiedRange.Length - 1024)) : modifiedRange; - // Check that AttackFollow hasn't cancelled the target by modifying attack.Target - // Having both this and AttackFollow modify that field is a horrible hack. - if (hasTicked && attack.Target.Type == TargetType.Invalid) - return NextActivity; + // Most actors want to be able to see their target before shooting + if (!attack.Info.TargetFrozenActors && !forceAttack && target.Type == TargetType.FrozenActor) + { + var rs = revealsShroud + .Where(Exts.IsTraitEnabled) + .MaxByOrDefault(s => s.Range); + + // Default to 2 cells if there are no active traits + var sightRange = rs != null ? rs.Range : WDist.FromCells(2); + if (sightRange < maxRange) + maxRange = sightRange; + } attack.Target = target; hasTicked = true; diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackFrontal.cs b/OpenRA.Mods.Common/Traits/Attack/AttackFrontal.cs index 7316012c6a..0457febbdb 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackFrontal.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackFrontal.cs @@ -57,7 +57,7 @@ namespace OpenRA.Mods.Common.Traits public override Activity GetAttackActivity(Actor self, Target newTarget, bool allowMove, bool forceAttack) { - return new Activities.Attack(self, newTarget, allowMove, forceAttack, info.FacingTolerance); + return new Activities.Attack(self, newTarget, allowMove, forceAttack); } } } diff --git a/OpenRA.Mods.Common/Traits/Attack/AttackOmni.cs b/OpenRA.Mods.Common/Traits/Attack/AttackOmni.cs index a48206535e..1ea0313c4c 100644 --- a/OpenRA.Mods.Common/Traits/Attack/AttackOmni.cs +++ b/OpenRA.Mods.Common/Traits/Attack/AttackOmni.cs @@ -10,6 +10,7 @@ #endregion using OpenRA.Activities; +using OpenRA.Mods.Common.Activities; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits @@ -32,9 +33,9 @@ namespace OpenRA.Mods.Common.Traits // Some 3rd-party mods rely on this being public public class SetTarget : Activity { - readonly Target target; readonly AttackOmni attack; readonly bool allowMove; + Target target; public SetTarget(AttackOmni attack, Target target, bool allowMove) { @@ -45,6 +46,7 @@ namespace OpenRA.Mods.Common.Traits public override Activity Tick(Actor self) { + target = target.Recalculate(self.Owner); if (IsCanceled || !target.IsValidFor(self) || !attack.IsReachableTarget(target, allowMove)) return NextActivity; diff --git a/OpenRA.Mods.D2k/Traits/AttackSwallow.cs b/OpenRA.Mods.D2k/Traits/AttackSwallow.cs index 97cf011ebe..0c290e2a02 100644 --- a/OpenRA.Mods.D2k/Traits/AttackSwallow.cs +++ b/OpenRA.Mods.D2k/Traits/AttackSwallow.cs @@ -72,16 +72,19 @@ namespace OpenRA.Mods.D2k.Traits public override Activity GetAttackActivity(Actor self, Target newTarget, bool allowMove, bool forceAttack) { - return new SwallowTarget(self, newTarget, allowMove, forceAttack, Info.FacingTolerance); + return new SwallowTarget(self, newTarget, allowMove, forceAttack); } public class SwallowTarget : Attack { - public SwallowTarget(Actor self, Target target, bool allowMovement, bool forceAttack, int facingTolerance) - : base(self, target, allowMovement, forceAttack, facingTolerance) { } + public SwallowTarget(Actor self, Target target, bool allowMovement, bool forceAttack) + : base(self, target, allowMovement, forceAttack) { } - // Worms ignore visibility, so don't need to recalculate targets - protected override bool IgnoresVisibility { get { return true; } } + protected override Target RecalculateTarget(Actor self) + { + // Worms ignore visibility, so don't need to recalculate targets + return target; + } } } } diff --git a/mods/cnc/rules/vehicles.yaml b/mods/cnc/rules/vehicles.yaml index c22e8c3f42..25bae9510e 100644 --- a/mods/cnc/rules/vehicles.yaml +++ b/mods/cnc/rules/vehicles.yaml @@ -174,6 +174,7 @@ ARTY: LocalOffset: 624,0,208 MuzzleSequence: muzzle AttackFrontal: + TargetFrozenActors: True WithMuzzleOverlay: AutoTarget: InitialStanceAI: Defend @@ -520,6 +521,7 @@ MSAM: Weapon: 227mm LocalOffset: 213,-128,0, 213,128,0 AttackFrontal: + TargetFrozenActors: True WithSpriteTurret: SpawnActorOnDeath: Actor: MSAM.Husk diff --git a/mods/ra/rules/ships.yaml b/mods/ra/rules/ships.yaml index 3b63438488..98c0073e39 100644 --- a/mods/ra/rules/ships.yaml +++ b/mods/ra/rules/ships.yaml @@ -118,6 +118,7 @@ MSUB: LocalOffset: 0,-171,0, 0,171,0 FireDelay: 2 AttackFrontal: + TargetFrozenActors: True SelectionDecorations: AutoTarget: InitialStance: HoldFire @@ -234,6 +235,7 @@ CA: MuzzleSequence: muzzle AttackTurreted: Turrets: primary, secondary + TargetFrozenActors: True WithMuzzleOverlay: SelectionDecorations: WithSpriteTurret@PRIMARY: diff --git a/mods/ra/rules/vehicles.yaml b/mods/ra/rules/vehicles.yaml index 9f945e9251..54a7f18cc2 100644 --- a/mods/ra/rules/vehicles.yaml +++ b/mods/ra/rules/vehicles.yaml @@ -28,6 +28,7 @@ V2RL: AutoTarget: ScanRadius: 10 AttackFrontal: + TargetFrozenActors: True WithFacingSpriteBody: RequiresCondition: !reloading Name: loaded @@ -263,6 +264,7 @@ ARTY: LocalOffset: 624,0,208 MuzzleSequence: muzzle AttackFrontal: + TargetFrozenActors: True WithMuzzleOverlay: Explodes: Weapon: ArtilleryExplode diff --git a/mods/ts/rules/gdi-vehicles.yaml b/mods/ts/rules/gdi-vehicles.yaml index f539553e9b..2871c9de6d 100644 --- a/mods/ts/rules/gdi-vehicles.yaml +++ b/mods/ts/rules/gdi-vehicles.yaml @@ -363,6 +363,7 @@ JUGG: Turrets: deployed RequiresCondition: deployed PauseOnCondition: empdisable + TargetFrozenActors: True Armament@deployed: Name: deployed Turret: deployed diff --git a/mods/ts/rules/nod-vehicles.yaml b/mods/ts/rules/nod-vehicles.yaml index 1dcf3bf918..354f3d9398 100644 --- a/mods/ts/rules/nod-vehicles.yaml +++ b/mods/ts/rules/nod-vehicles.yaml @@ -258,6 +258,7 @@ ART2: Turrets: deployed RequiresCondition: deployed PauseOnCondition: empdisable + TargetFrozenActors: True Armament@deployed: Name: deployed Turret: deployed