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