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.Primitives; 16 using OpenRA.Traits; 17 18 namespace OpenRA.Mods.Common.Traits 19 { 20 public class TurretedInfo : PausableConditionalTraitInfo, Requires<BodyOrientationInfo>, IActorPreviewInitInfo, IEditorActorOptions 21 { 22 public readonly string Turret = "primary"; 23 [Desc("Speed at which the turret turns.")] 24 public readonly int TurnSpeed = 255; 25 public readonly int InitialFacing = 0; 26 27 [Desc("Number of ticks before turret is realigned. (-1 turns off realignment)")] 28 public readonly int RealignDelay = 40; 29 30 [Desc("Muzzle position relative to turret or body. (forward, right, up) triples")] 31 public readonly WVec Offset = WVec.Zero; 32 33 [Desc("Facing to use for actor previews (map editor, color picker, etc)")] 34 public readonly int PreviewFacing = 96; 35 36 [Desc("Display order for the turret facing slider in the map editor")] 37 public readonly int EditorTurretFacingDisplayOrder = 4; 38 IActorPreviewInitInfo.ActorPreviewInits(ActorInfo ai, ActorPreviewType type)39 IEnumerable<object> IActorPreviewInitInfo.ActorPreviewInits(ActorInfo ai, ActorPreviewType type) 40 { 41 // HACK: The ActorInit system does not support multiple instances of the same type 42 // Make sure that we only return one TurretFacingInit, even for actors with multiple turrets 43 if (ai.TraitInfos<TurretedInfo>().FirstOrDefault() == this) 44 yield return new TurretFacingInit(PreviewFacing); 45 } 46 IEditorActorOptions.ActorOptions(ActorInfo ai, World world)47 IEnumerable<EditorActorOption> IEditorActorOptions.ActorOptions(ActorInfo ai, World world) 48 { 49 // TODO: Handle multiple turrets properly (will probably require a rewrite of the Init system) 50 if (ai.TraitInfos<TurretedInfo>().FirstOrDefault() != this) 51 yield break; 52 53 yield return new EditorActorSlider("Turret", EditorTurretFacingDisplayOrder, 0, 255, 8, 54 actor => 55 { 56 var init = actor.Init<TurretFacingInit>(); 57 if (init != null) 58 return init.Value(world); 59 60 var facingInit = actor.Init<FacingInit>(); 61 if (facingInit != null) 62 return facingInit.Value(world); 63 64 return InitialFacing; 65 }, 66 (actor, value) => 67 { 68 actor.RemoveInit<TurretFacingsInit>(); 69 actor.ReplaceInit(new TurretFacingInit((int)value)); 70 }); 71 } 72 Create(ActorInitializer init)73 public override object Create(ActorInitializer init) { return new Turreted(init, this); } 74 } 75 76 public class Turreted : PausableConditionalTrait<TurretedInfo>, ITick, IDeathActorInitModifier, IActorPreviewInitModifier 77 { 78 AttackTurreted attack; 79 IFacing facing; 80 BodyOrientation body; 81 82 [Sync] 83 public int QuantizedFacings = 0; 84 85 [Sync] 86 public int TurretFacing = 0; 87 88 public int? DesiredFacing; 89 int realignTick = 0; 90 91 // For subclasses that want to move the turret relative to the body 92 protected WVec localOffset = WVec.Zero; 93 94 public WVec Offset { get { return Info.Offset + localOffset; } } 95 public string Name { get { return Info.Turret; } } 96 TurretFacingFromInit(IActorInitializer init, int def, string turret = null)97 public static Func<int> TurretFacingFromInit(IActorInitializer init, int def, string turret = null) 98 { 99 if (turret != null && init.Contains<DynamicTurretFacingsInit>()) 100 { 101 Func<int> facing; 102 if (init.Get<DynamicTurretFacingsInit, Dictionary<string, Func<int>>>().TryGetValue(turret, out facing)) 103 return facing; 104 } 105 106 if (turret != null && init.Contains<TurretFacingsInit>()) 107 { 108 int facing; 109 if (init.Get<TurretFacingsInit, Dictionary<string, int>>().TryGetValue(turret, out facing)) 110 return () => facing; 111 } 112 113 if (init.Contains<TurretFacingInit>()) 114 { 115 var facing = init.Get<TurretFacingInit, int>(); 116 return () => facing; 117 } 118 119 if (init.Contains<DynamicFacingInit>()) 120 return init.Get<DynamicFacingInit, Func<int>>(); 121 122 if (init.Contains<FacingInit>()) 123 { 124 var facing = init.Get<FacingInit, int>(); 125 return () => facing; 126 } 127 128 return () => def; 129 } 130 Turreted(ActorInitializer init, TurretedInfo info)131 public Turreted(ActorInitializer init, TurretedInfo info) 132 : base(info) 133 { 134 TurretFacing = TurretFacingFromInit(init, Info.InitialFacing, Info.Turret)(); 135 } 136 Created(Actor self)137 protected override void Created(Actor self) 138 { 139 base.Created(self); 140 attack = self.TraitsImplementing<AttackTurreted>().SingleOrDefault(at => ((AttackTurretedInfo)at.Info).Turrets.Contains(Info.Turret)); 141 facing = self.TraitOrDefault<IFacing>(); 142 body = self.Trait<BodyOrientation>(); 143 } 144 ITick.Tick(Actor self)145 void ITick.Tick(Actor self) 146 { 147 Tick(self); 148 } 149 Tick(Actor self)150 protected virtual void Tick(Actor self) 151 { 152 if (IsTraitDisabled) 153 return; 154 155 // NOTE: FaceTarget is called in AttackTurreted.CanAttack if the turret has a target. 156 if (attack != null) 157 { 158 // Only realign while not attacking anything 159 if (attack.IsAiming) 160 return; 161 162 if (realignTick < Info.RealignDelay) 163 realignTick++; 164 else if (Info.RealignDelay > -1) 165 DesiredFacing = null; 166 167 MoveTurret(); 168 } 169 else 170 { 171 realignTick = 0; 172 MoveTurret(); 173 } 174 } 175 MoveTurret()176 void MoveTurret() 177 { 178 var df = DesiredFacing ?? (facing != null ? facing.Facing : TurretFacing); 179 TurretFacing = Util.TickFacing(TurretFacing, df, Info.TurnSpeed); 180 } 181 FaceTarget(Actor self, Target target)182 public bool FaceTarget(Actor self, Target target) 183 { 184 if (IsTraitDisabled || IsTraitPaused || attack == null || attack.IsTraitDisabled || attack.IsTraitPaused) 185 return false; 186 187 var pos = self.CenterPosition; 188 var targetPos = attack.GetTargetPosition(pos, target); 189 var delta = targetPos - pos; 190 DesiredFacing = delta.HorizontalLengthSquared != 0 ? delta.Yaw.Facing : TurretFacing; 191 MoveTurret(); 192 return HasAchievedDesiredFacing; 193 } 194 195 public virtual bool HasAchievedDesiredFacing 196 { 197 get { return DesiredFacing == null || TurretFacing == DesiredFacing.Value; } 198 } 199 200 // Turret offset in world-space Position(Actor self)201 public WVec Position(Actor self) 202 { 203 var bodyOrientation = body.QuantizeOrientation(self, self.Orientation); 204 return body.LocalToWorld(Offset.Rotate(bodyOrientation)); 205 } 206 207 // Orientation in world-space WorldOrientation(Actor self)208 public WRot WorldOrientation(Actor self) 209 { 210 // Hack: turretFacing is relative to the world, so subtract the body yaw 211 var world = WRot.FromYaw(WAngle.FromFacing(TurretFacing)); 212 213 if (QuantizedFacings == 0) 214 return world; 215 216 // Quantize orientation to match a rendered sprite 217 // Implies no pitch or yaw 218 var facing = body.QuantizeFacing(world.Yaw.Angle / 4, QuantizedFacings); 219 return new WRot(WAngle.Zero, WAngle.Zero, WAngle.FromFacing(facing)); 220 } 221 ModifyDeathActorInit(Actor self, TypeDictionary init)222 public void ModifyDeathActorInit(Actor self, TypeDictionary init) 223 { 224 var facings = init.GetOrDefault<TurretFacingsInit>(); 225 if (facings == null) 226 { 227 facings = new TurretFacingsInit(); 228 init.Add(facings); 229 } 230 231 if (!facings.Value(self.World).ContainsKey(Name)) 232 facings.Value(self.World).Add(Name, TurretFacing); 233 } 234 IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits)235 void IActorPreviewInitModifier.ModifyActorPreviewInit(Actor self, TypeDictionary inits) 236 { 237 var facings = inits.GetOrDefault<DynamicTurretFacingsInit>(); 238 if (facings == null) 239 { 240 facings = new DynamicTurretFacingsInit(); 241 inits.Add(facings); 242 } 243 244 Func<int> bodyFacing = () => facing.Facing; 245 var dynamicFacing = inits.GetOrDefault<DynamicFacingInit>(); 246 var staticFacing = inits.GetOrDefault<FacingInit>(); 247 if (dynamicFacing != null) 248 bodyFacing = dynamicFacing.Value(self.World); 249 else if (staticFacing != null) 250 bodyFacing = () => staticFacing.Value(self.World); 251 252 // Freeze the relative turret facing to its current value 253 var facingOffset = TurretFacing - bodyFacing(); 254 facings.Value(self.World).Add(Name, () => bodyFacing() + facingOffset); 255 } 256 TraitDisabled(Actor self)257 protected override void TraitDisabled(Actor self) 258 { 259 if (attack != null && attack.IsAiming) 260 attack.OnStopOrder(self); 261 } 262 } 263 264 public class TurretFacingInit : IActorInit<int> 265 { 266 [FieldFromYamlKey] 267 readonly int value = 128; 268 TurretFacingInit()269 public TurretFacingInit() { } TurretFacingInit(int init)270 public TurretFacingInit(int init) { value = init; } Value(World world)271 public int Value(World world) { return value; } 272 } 273 274 public class TurretFacingsInit : IActorInit<Dictionary<string, int>> 275 { 276 [DictionaryFromYamlKey] 277 readonly Dictionary<string, int> value = new Dictionary<string, int>(); 278 TurretFacingsInit()279 public TurretFacingsInit() { } TurretFacingsInit(Dictionary<string, int> init)280 public TurretFacingsInit(Dictionary<string, int> init) { value = init; } Value(World world)281 public Dictionary<string, int> Value(World world) { return value; } 282 } 283 284 public class DynamicTurretFacingsInit : IActorInit<Dictionary<string, Func<int>>> 285 { 286 readonly Dictionary<string, Func<int>> value = new Dictionary<string, Func<int>>(); DynamicTurretFacingsInit()287 public DynamicTurretFacingsInit() { } DynamicTurretFacingsInit(Dictionary<string, Func<int>> init)288 public DynamicTurretFacingsInit(Dictionary<string, Func<int>> init) { value = init; } Value(World world)289 public Dictionary<string, Func<int>> Value(World world) { return value; } 290 } 291 } 292