1 #region Copyright & License Information 2 /* 3 * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) 4 * This file is part of OpenRA, which is free software. It is made 5 * available to you under the terms of the GNU General Public License 6 * as published by the Free Software Foundation, either version 3 of 7 * the License, or (at your option) any later version. For more 8 * information, see COPYING. 9 */ 10 #endregion 11 12 using System; 13 using System.Collections.Generic; 14 using System.Linq; 15 using OpenRA.Activities; 16 using OpenRA.Mods.Common.Traits; 17 using OpenRA.Primitives; 18 using OpenRA.Traits; 19 20 namespace OpenRA.Mods.Common.Activities 21 { 22 /* non-turreted attack */ 23 public class Attack : Activity, IActivityNotifyStanceChanged 24 { 25 [Flags] 26 protected enum AttackStatus { UnableToAttack, NeedsToTurn, NeedsToMove, Attacking } 27 28 readonly AttackFrontal[] attackTraits; 29 readonly RevealsShroud[] revealsShroud; 30 readonly IMove move; 31 readonly IFacing facing; 32 readonly IPositionable positionable; 33 readonly bool forceAttack; 34 readonly Color? targetLineColor; 35 36 protected Target target; 37 Target lastVisibleTarget; 38 WDist lastVisibleMaximumRange; 39 BitSet<TargetableType> lastVisibleTargetTypes; 40 Player lastVisibleOwner; 41 bool useLastVisibleTarget; 42 bool wasMovingWithinRange; 43 44 WDist minRange; 45 WDist maxRange; 46 AttackStatus attackStatus = AttackStatus.UnableToAttack; 47 Attack(Actor self, Target target, bool allowMovement, bool forceAttack, Color? targetLineColor = null)48 public Attack(Actor self, Target target, bool allowMovement, bool forceAttack, Color? targetLineColor = null) 49 { 50 this.target = target; 51 this.targetLineColor = targetLineColor; 52 this.forceAttack = forceAttack; 53 54 attackTraits = self.TraitsImplementing<AttackFrontal>().ToArray(); 55 revealsShroud = self.TraitsImplementing<RevealsShroud>().ToArray(); 56 facing = self.Trait<IFacing>(); 57 positionable = self.Trait<IPositionable>(); 58 59 move = allowMovement ? self.TraitOrDefault<IMove>() : null; 60 61 // The target may become hidden between the initial order request and the first tick (e.g. if queued) 62 // Moving to any position (even if quite stale) is still better than immediately giving up 63 if ((target.Type == TargetType.Actor && target.Actor.CanBeViewedByPlayer(self.Owner)) 64 || target.Type == TargetType.FrozenActor || target.Type == TargetType.Terrain) 65 { 66 lastVisibleTarget = Target.FromPos(target.CenterPosition); 67 lastVisibleMaximumRange = attackTraits.Where(x => !x.IsTraitDisabled) 68 .Min(x => x.GetMaximumRangeVersusTarget(target)); 69 70 if (target.Type == TargetType.Actor) 71 { 72 lastVisibleOwner = target.Actor.Owner; 73 lastVisibleTargetTypes = target.Actor.GetEnabledTargetTypes(); 74 } 75 else if (target.Type == TargetType.FrozenActor) 76 { 77 lastVisibleOwner = target.FrozenActor.Owner; 78 lastVisibleTargetTypes = target.FrozenActor.TargetTypes; 79 } 80 } 81 } 82 RecalculateTarget(Actor self, out bool targetIsHiddenActor)83 protected virtual Target RecalculateTarget(Actor self, out bool targetIsHiddenActor) 84 { 85 return target.Recalculate(self.Owner, out targetIsHiddenActor); 86 } 87 Tick(Actor self)88 public override bool Tick(Actor self) 89 { 90 if (IsCanceling) 91 return true; 92 93 bool targetIsHiddenActor; 94 target = RecalculateTarget(self, out targetIsHiddenActor); 95 if (!targetIsHiddenActor && target.Type == TargetType.Actor) 96 { 97 lastVisibleTarget = Target.FromTargetPositions(target); 98 lastVisibleMaximumRange = attackTraits.Where(x => !x.IsTraitDisabled) 99 .Min(x => x.GetMaximumRangeVersusTarget(target)); 100 101 lastVisibleOwner = target.Actor.Owner; 102 lastVisibleTargetTypes = target.Actor.GetEnabledTargetTypes(); 103 } 104 105 useLastVisibleTarget = targetIsHiddenActor || !target.IsValidFor(self); 106 107 // If we are ticking again after previously sequencing a MoveWithRange then that move must have completed 108 // Either we are in range and can see the target, or we've lost track of it and should give up 109 if (wasMovingWithinRange && targetIsHiddenActor) 110 return true; 111 112 // Target is hidden or dead, and we don't have a fallback position to move towards 113 if (useLastVisibleTarget && !lastVisibleTarget.IsValidFor(self)) 114 return true; 115 116 wasMovingWithinRange = false; 117 var pos = self.CenterPosition; 118 var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target; 119 120 // We don't know where the target actually is, so move to where we last saw it 121 if (useLastVisibleTarget) 122 { 123 // We've reached the assumed position but it is not there or we can't move any further - give up 124 if (checkTarget.IsInRange(pos, lastVisibleMaximumRange) || move == null || lastVisibleMaximumRange == WDist.Zero) 125 return true; 126 127 // Move towards the last known position 128 wasMovingWithinRange = true; 129 QueueChild(move.MoveWithinRange(target, WDist.Zero, lastVisibleMaximumRange, checkTarget.CenterPosition, Color.Red)); 130 return false; 131 } 132 133 attackStatus = AttackStatus.UnableToAttack; 134 135 foreach (var attack in attackTraits.Where(x => !x.IsTraitDisabled)) 136 { 137 var status = TickAttack(self, attack); 138 attack.IsAiming = status == AttackStatus.Attacking || status == AttackStatus.NeedsToTurn; 139 } 140 141 if (attackStatus.HasFlag(AttackStatus.NeedsToMove)) 142 wasMovingWithinRange = true; 143 144 if (attackStatus >= AttackStatus.NeedsToTurn) 145 return false; 146 147 return true; 148 } 149 TickAttack(Actor self, AttackFrontal attack)150 protected virtual AttackStatus TickAttack(Actor self, AttackFrontal attack) 151 { 152 if (!target.IsValidFor(self)) 153 return AttackStatus.UnableToAttack; 154 155 if (attack.Info.AttackRequiresEnteringCell && !positionable.CanEnterCell(target.Actor.Location, null, BlockedByActor.None)) 156 return AttackStatus.UnableToAttack; 157 158 if (!attack.Info.TargetFrozenActors && !forceAttack && target.Type == TargetType.FrozenActor) 159 { 160 // Try to move within range, drop the target otherwise 161 if (move == null) 162 return AttackStatus.UnableToAttack; 163 164 var rs = revealsShroud 165 .Where(Exts.IsTraitEnabled) 166 .MaxByOrDefault(s => s.Range); 167 168 // Default to 2 cells if there are no active traits 169 var sightRange = rs != null ? rs.Range : WDist.FromCells(2); 170 171 attackStatus |= AttackStatus.NeedsToMove; 172 QueueChild(move.MoveWithinRange(target, sightRange, target.CenterPosition, Color.Red)); 173 return AttackStatus.NeedsToMove; 174 } 175 176 // Drop the target once none of the weapons are effective against it 177 var armaments = attack.ChooseArmamentsForTarget(target, forceAttack).ToList(); 178 if (armaments.Count == 0) 179 return AttackStatus.UnableToAttack; 180 181 // Update ranges 182 minRange = armaments.Max(a => a.Weapon.MinRange); 183 maxRange = armaments.Min(a => a.MaxRange()); 184 185 var pos = self.CenterPosition; 186 var mobile = move as Mobile; 187 if (!target.IsInRange(pos, maxRange) 188 || (minRange.Length != 0 && target.IsInRange(pos, minRange)) 189 || (mobile != null && !mobile.CanInteractWithGroundLayer(self))) 190 { 191 // Try to move within range, drop the target otherwise 192 if (move == null) 193 return AttackStatus.UnableToAttack; 194 195 attackStatus |= AttackStatus.NeedsToMove; 196 var checkTarget = useLastVisibleTarget ? lastVisibleTarget : target; 197 QueueChild(move.MoveWithinRange(target, minRange, maxRange, checkTarget.CenterPosition, Color.Red)); 198 return AttackStatus.NeedsToMove; 199 } 200 201 if (!attack.TargetInFiringArc(self, target, attack.Info.FacingTolerance)) 202 { 203 var desiredFacing = (attack.GetTargetPosition(pos, target) - pos).Yaw.Facing; 204 attackStatus |= AttackStatus.NeedsToTurn; 205 QueueChild(new Turn(self, desiredFacing)); 206 return AttackStatus.NeedsToTurn; 207 } 208 209 attackStatus |= AttackStatus.Attacking; 210 DoAttack(self, attack, armaments); 211 212 return AttackStatus.Attacking; 213 } 214 DoAttack(Actor self, AttackFrontal attack, IEnumerable<Armament> armaments)215 protected virtual void DoAttack(Actor self, AttackFrontal attack, IEnumerable<Armament> armaments) 216 { 217 if (!attack.IsTraitPaused) 218 foreach (var a in armaments) 219 a.CheckFire(self, facing, target); 220 } 221 IActivityNotifyStanceChanged.StanceChanged(Actor self, AutoTarget autoTarget, UnitStance oldStance, UnitStance newStance)222 void IActivityNotifyStanceChanged.StanceChanged(Actor self, AutoTarget autoTarget, UnitStance oldStance, UnitStance newStance) 223 { 224 // Cancel non-forced targets when switching to a more restrictive stance if they are no longer valid for auto-targeting 225 if (newStance > oldStance || forceAttack) 226 return; 227 228 // If lastVisibleTarget is invalid we could never view the target in the first place, so we just drop it here too 229 if (!lastVisibleTarget.IsValidFor(self) || !autoTarget.HasValidTargetPriority(self, lastVisibleOwner, lastVisibleTargetTypes)) 230 target = Target.Invalid; 231 } 232 TargetLineNodes(Actor self)233 public override IEnumerable<TargetLineNode> TargetLineNodes(Actor self) 234 { 235 if (targetLineColor != null) 236 yield return new TargetLineNode(useLastVisibleTarget ? lastVisibleTarget : target, targetLineColor.Value); 237 } 238 } 239 } 240