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