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.Graphics;
16 using OpenRA.Mods.Common.Traits.Render;
17 using OpenRA.Primitives;
18 
19 using OpenRA.Traits;
20 
21 namespace OpenRA.Mods.Common.Traits
22 {
23 	public class FirePort
24 	{
25 		public WVec Offset;
26 		public WAngle Yaw;
27 		public WAngle Cone;
28 	}
29 
30 	[Desc("Cargo can fire their weapons out of fire ports.")]
31 	public class AttackGarrisonedInfo : AttackFollowInfo, IRulesetLoaded, Requires<CargoInfo>
32 	{
33 		[FieldLoader.Require]
34 		[Desc("Fire port offsets in local coordinates.")]
35 		public readonly WVec[] PortOffsets = null;
36 
37 		[FieldLoader.Require]
38 		[Desc("Fire port yaw angles.")]
39 		public readonly WAngle[] PortYaws = null;
40 
41 		[FieldLoader.Require]
42 		[Desc("Fire port yaw cone angle.")]
43 		public readonly WAngle[] PortCones = null;
44 
45 		public FirePort[] Ports { get; private set; }
46 
47 		[PaletteReference]
48 		public readonly string MuzzlePalette = "effect";
49 
Create(ActorInitializer init)50 		public override object Create(ActorInitializer init) { return new AttackGarrisoned(init.Self, this); }
RulesetLoaded(Ruleset rules, ActorInfo ai)51 		public override void RulesetLoaded(Ruleset rules, ActorInfo ai)
52 		{
53 			if (PortOffsets.Length == 0)
54 				throw new YamlException("PortOffsets must have at least one entry.");
55 
56 			if (PortYaws.Length != PortOffsets.Length)
57 				throw new YamlException("PortYaws must define an angle for each port.");
58 
59 			if (PortCones.Length != PortOffsets.Length)
60 				throw new YamlException("PortCones must define an angle for each port.");
61 
62 			Ports = new FirePort[PortOffsets.Length];
63 
64 			for (var i = 0; i < PortOffsets.Length; i++)
65 			{
66 				Ports[i] = new FirePort
67 				{
68 					Offset = PortOffsets[i],
69 					Yaw = PortYaws[i],
70 					Cone = PortCones[i],
71 				};
72 			}
73 
74 			base.RulesetLoaded(rules, ai);
75 		}
76 	}
77 
78 	public class AttackGarrisoned : AttackFollow, INotifyPassengerEntered, INotifyPassengerExited, IRender
79 	{
80 		public readonly new AttackGarrisonedInfo Info;
81 		Lazy<BodyOrientation> coords;
82 		List<Armament> armaments;
83 		List<AnimationWithOffset> muzzles;
84 		Dictionary<Actor, IFacing> paxFacing;
85 		Dictionary<Actor, IPositionable> paxPos;
86 		Dictionary<Actor, RenderSprites> paxRender;
87 
AttackGarrisoned(Actor self, AttackGarrisonedInfo info)88 		public AttackGarrisoned(Actor self, AttackGarrisonedInfo info)
89 			: base(self, info)
90 		{
91 			Info = info;
92 			coords = Exts.Lazy(() => self.Trait<BodyOrientation>());
93 			armaments = new List<Armament>();
94 			muzzles = new List<AnimationWithOffset>();
95 			paxFacing = new Dictionary<Actor, IFacing>();
96 			paxPos = new Dictionary<Actor, IPositionable>();
97 			paxRender = new Dictionary<Actor, RenderSprites>();
98 		}
99 
InitializeGetArmaments(Actor self)100 		protected override Func<IEnumerable<Armament>> InitializeGetArmaments(Actor self)
101 		{
102 			return () => armaments;
103 		}
104 
INotifyPassengerEntered.OnPassengerEntered(Actor self, Actor passenger)105 		void INotifyPassengerEntered.OnPassengerEntered(Actor self, Actor passenger)
106 		{
107 			paxFacing.Add(passenger, passenger.Trait<IFacing>());
108 			paxPos.Add(passenger, passenger.Trait<IPositionable>());
109 			paxRender.Add(passenger, passenger.Trait<RenderSprites>());
110 			armaments.AddRange(
111 				passenger.TraitsImplementing<Armament>()
112 				.Where(a => Info.Armaments.Contains(a.Info.Name)));
113 		}
114 
INotifyPassengerExited.OnPassengerExited(Actor self, Actor passenger)115 		void INotifyPassengerExited.OnPassengerExited(Actor self, Actor passenger)
116 		{
117 			paxFacing.Remove(passenger);
118 			paxPos.Remove(passenger);
119 			paxRender.Remove(passenger);
120 			armaments.RemoveAll(a => a.Actor == passenger);
121 		}
122 
SelectFirePort(Actor self, WAngle targetYaw)123 		FirePort SelectFirePort(Actor self, WAngle targetYaw)
124 		{
125 			// Pick a random port that faces the target
126 			var bodyYaw = facing != null ? WAngle.FromFacing(facing.Facing) : WAngle.Zero;
127 			var indices = Enumerable.Range(0, Info.Ports.Length).Shuffle(self.World.SharedRandom);
128 			foreach (var i in indices)
129 			{
130 				var yaw = bodyYaw + Info.Ports[i].Yaw;
131 				var leftTurn = (yaw - targetYaw).Angle;
132 				var rightTurn = (targetYaw - yaw).Angle;
133 				if (Math.Min(leftTurn, rightTurn) <= Info.Ports[i].Cone.Angle)
134 					return Info.Ports[i];
135 			}
136 
137 			return null;
138 		}
139 
PortOffset(Actor self, FirePort p)140 		WVec PortOffset(Actor self, FirePort p)
141 		{
142 			var bodyOrientation = coords.Value.QuantizeOrientation(self, self.Orientation);
143 			return coords.Value.LocalToWorld(p.Offset.Rotate(bodyOrientation));
144 		}
145 
DoAttack(Actor self, Target target)146 		public override void DoAttack(Actor self, Target target)
147 		{
148 			if (!CanAttack(self, target))
149 				return;
150 
151 			var pos = self.CenterPosition;
152 			var targetedPosition = GetTargetPosition(pos, target);
153 			var targetYaw = (targetedPosition - pos).Yaw;
154 
155 			foreach (var a in Armaments)
156 			{
157 				if (a.IsTraitDisabled)
158 					continue;
159 
160 				var port = SelectFirePort(self, targetYaw);
161 				if (port == null)
162 					return;
163 
164 				var muzzleFacing = targetYaw.Angle / 4;
165 				paxFacing[a.Actor].Facing = muzzleFacing;
166 				paxPos[a.Actor].SetVisualPosition(a.Actor, pos + PortOffset(self, port));
167 
168 				var barrel = a.CheckFire(a.Actor, facing, target);
169 				if (barrel == null)
170 					continue;
171 
172 				if (a.Info.MuzzleSequence != null)
173 				{
174 					// Muzzle facing is fixed once the firing starts
175 					var muzzleAnim = new Animation(self.World, paxRender[a.Actor].GetImage(a.Actor), () => muzzleFacing);
176 					var sequence = a.Info.MuzzleSequence;
177 
178 					if (a.Info.MuzzleSplitFacings > 0)
179 						sequence += Util.QuantizeFacing(muzzleFacing, a.Info.MuzzleSplitFacings).ToString();
180 
181 					var muzzleFlash = new AnimationWithOffset(muzzleAnim,
182 						() => PortOffset(self, port),
183 						() => false,
184 						p => RenderUtils.ZOffsetFromCenter(self, p, 1024));
185 
186 					muzzles.Add(muzzleFlash);
187 					muzzleAnim.PlayThen(sequence, () => muzzles.Remove(muzzleFlash));
188 				}
189 
190 				foreach (var npa in self.TraitsImplementing<INotifyAttack>())
191 					npa.Attacking(self, target, a, barrel);
192 			}
193 		}
194 
IRender.Render(Actor self, WorldRenderer wr)195 		IEnumerable<IRenderable> IRender.Render(Actor self, WorldRenderer wr)
196 		{
197 			var pal = wr.Palette(Info.MuzzlePalette);
198 
199 			// Display muzzle flashes
200 			foreach (var m in muzzles)
201 				foreach (var r in m.Render(self, wr, pal, 1f))
202 					yield return r;
203 		}
204 
IRender.ScreenBounds(Actor self, WorldRenderer wr)205 		IEnumerable<Rectangle> IRender.ScreenBounds(Actor self, WorldRenderer wr)
206 		{
207 			// Muzzle flashes don't contribute to actor bounds
208 			yield break;
209 		}
210 
Tick(Actor self)211 		protected override void Tick(Actor self)
212 		{
213 			base.Tick(self);
214 
215 			// Take a copy so that Tick() can remove animations
216 			foreach (var m in muzzles.ToArray())
217 				m.Animation.Tick();
218 		}
219 	}
220 }
221