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.Effects; 16 using OpenRA.Primitives; 17 using OpenRA.Traits; 18 19 namespace OpenRA.GameRules 20 { 21 public class ProjectileArgs 22 { 23 public WeaponInfo Weapon; 24 public int[] DamageModifiers; 25 public int[] InaccuracyModifiers; 26 public int[] RangeModifiers; 27 public int Facing; 28 public Func<int> CurrentMuzzleFacing; 29 public WPos Source; 30 public Func<WPos> CurrentSource; 31 public Actor SourceActor; 32 public WPos PassiveTarget; 33 public Target GuidedTarget; 34 } 35 36 public class WarheadArgs 37 { 38 public WeaponInfo Weapon; 39 public int[] DamageModifiers = { }; 40 public WPos? Source; 41 public Actor SourceActor; 42 public Target WeaponTarget; 43 WarheadArgs(ProjectileArgs args)44 public WarheadArgs(ProjectileArgs args) 45 { 46 Weapon = args.Weapon; 47 DamageModifiers = args.DamageModifiers; 48 Source = args.Source; 49 SourceActor = args.SourceActor; 50 WeaponTarget = args.GuidedTarget; 51 } 52 53 // Default empty constructor for callers that want to initialize fields themselves WarheadArgs()54 public WarheadArgs() { } 55 } 56 57 public interface IProjectile : IEffect { } Create(ProjectileArgs args)58 public interface IProjectileInfo { IProjectile Create(ProjectileArgs args); } 59 60 public sealed class WeaponInfo 61 { 62 [Desc("The maximum range the weapon can fire.")] 63 public readonly WDist Range = WDist.Zero; 64 65 [Desc("First burst is aimed at this offset relative to target position.")] 66 public readonly WVec FirstBurstTargetOffset = WVec.Zero; 67 68 [Desc("Each burst after the first lands by this offset away from the previous burst.")] 69 public readonly WVec FollowingBurstTargetOffset = WVec.Zero; 70 71 [Desc("The sound played each time the weapon is fired.")] 72 public readonly string[] Report = null; 73 74 [Desc("Sound played only on first burst in a salvo.")] 75 public readonly string[] StartBurstReport = null; 76 77 [Desc("The sound played when the weapon is reloaded.")] 78 public readonly string[] AfterFireSound = null; 79 80 [Desc("Delay in ticks to play reloading sound.")] 81 public readonly int AfterFireSoundDelay = 0; 82 83 [Desc("Delay in ticks between reloading ammo magazines.")] 84 public readonly int ReloadDelay = 1; 85 86 [Desc("Number of shots in a single ammo magazine.")] 87 public readonly int Burst = 1; 88 89 [Desc("What types of targets are affected.")] 90 public readonly BitSet<TargetableType> ValidTargets = new BitSet<TargetableType>("Ground", "Water"); 91 92 [Desc("What types of targets are unaffected.", "Overrules ValidTargets.")] 93 public readonly BitSet<TargetableType> InvalidTargets; 94 95 [Desc("Delay in ticks between firing shots from the same ammo magazine. If one entry, it will be used for all bursts.", 96 "If multiple entries, their number needs to match Burst - 1.")] 97 public readonly int[] BurstDelays = { 5 }; 98 99 [Desc("The minimum range the weapon can fire.")] 100 public readonly WDist MinRange = WDist.Zero; 101 102 [Desc("Does this weapon aim at the target's center regardless of other targetable offsets?")] 103 public readonly bool TargetActorCenter = false; 104 105 [FieldLoader.LoadUsing("LoadProjectile")] 106 public readonly IProjectileInfo Projectile; 107 108 [FieldLoader.LoadUsing("LoadWarheads")] 109 public readonly List<IWarhead> Warheads = new List<IWarhead>(); 110 WeaponInfo(string name, MiniYaml content)111 public WeaponInfo(string name, MiniYaml content) 112 { 113 // Resolve any weapon-level yaml inheritance or removals 114 // HACK: The "Defaults" sequence syntax prevents us from doing this generally during yaml parsing 115 content.Nodes = MiniYaml.Merge(new[] { content.Nodes }); 116 FieldLoader.Load(this, content); 117 } 118 LoadProjectile(MiniYaml yaml)119 static object LoadProjectile(MiniYaml yaml) 120 { 121 MiniYaml proj; 122 if (!yaml.ToDictionary().TryGetValue("Projectile", out proj)) 123 return null; 124 var ret = Game.CreateObject<IProjectileInfo>(proj.Value + "Info"); 125 FieldLoader.Load(ret, proj); 126 return ret; 127 } 128 LoadWarheads(MiniYaml yaml)129 static object LoadWarheads(MiniYaml yaml) 130 { 131 var retList = new List<IWarhead>(); 132 foreach (var node in yaml.Nodes.Where(n => n.Key.StartsWith("Warhead"))) 133 { 134 var ret = Game.CreateObject<IWarhead>(node.Value.Value + "Warhead"); 135 FieldLoader.Load(ret, node.Value); 136 retList.Add(ret); 137 } 138 139 return retList; 140 } 141 IsValidTarget(BitSet<TargetableType> targetTypes)142 public bool IsValidTarget(BitSet<TargetableType> targetTypes) 143 { 144 return ValidTargets.Overlaps(targetTypes) && !InvalidTargets.Overlaps(targetTypes); 145 } 146 147 /// <summary>Checks if the weapon is valid against (can target) the target.</summary> IsValidAgainst(Target target, World world, Actor firedBy)148 public bool IsValidAgainst(Target target, World world, Actor firedBy) 149 { 150 if (target.Type == TargetType.Actor) 151 return IsValidAgainst(target.Actor, firedBy); 152 153 if (target.Type == TargetType.FrozenActor) 154 return IsValidAgainst(target.FrozenActor, firedBy); 155 156 if (target.Type == TargetType.Terrain) 157 { 158 var cell = world.Map.CellContaining(target.CenterPosition); 159 if (!world.Map.Contains(cell)) 160 return false; 161 162 var cellInfo = world.Map.GetTerrainInfo(cell); 163 if (!IsValidTarget(cellInfo.TargetTypes)) 164 return false; 165 166 return true; 167 } 168 169 return false; 170 } 171 172 /// <summary>Checks if the weapon is valid against (can target) the actor.</summary> IsValidAgainst(Actor victim, Actor firedBy)173 public bool IsValidAgainst(Actor victim, Actor firedBy) 174 { 175 var targetTypes = victim.GetEnabledTargetTypes(); 176 177 if (!IsValidTarget(targetTypes)) 178 return false; 179 180 // PERF: Avoid LINQ. 181 foreach (var warhead in Warheads) 182 if (warhead.IsValidAgainst(victim, firedBy)) 183 return true; 184 185 return false; 186 } 187 188 /// <summary>Checks if the weapon is valid against (can target) the frozen actor.</summary> IsValidAgainst(FrozenActor victim, Actor firedBy)189 public bool IsValidAgainst(FrozenActor victim, Actor firedBy) 190 { 191 if (!IsValidTarget(victim.TargetTypes)) 192 return false; 193 194 if (!Warheads.Any(w => w.IsValidAgainst(victim, firedBy))) 195 return false; 196 197 return true; 198 } 199 200 /// <summary>Applies all the weapon's warheads to the target.</summary> Impact(Target target, WarheadArgs args)201 public void Impact(Target target, WarheadArgs args) 202 { 203 var world = args.SourceActor.World; 204 foreach (var warhead in Warheads) 205 { 206 if (warhead.Delay > 0) 207 world.AddFrameEndTask(w => w.Add(new DelayedImpact(warhead.Delay, warhead, target, args))); 208 else 209 warhead.DoImpact(target, args); 210 } 211 } 212 213 /// <summary>Applies all the weapon's warheads to the target. Only use for projectile-less, special-case impacts.</summary> Impact(Target target, Actor firedBy)214 public void Impact(Target target, Actor firedBy) 215 { 216 // The impact will happen immediately at target.CenterPosition. 217 var args = new WarheadArgs 218 { 219 Weapon = this, 220 SourceActor = firedBy, 221 WeaponTarget = target 222 }; 223 224 if (firedBy.OccupiesSpace != null) 225 args.Source = firedBy.CenterPosition; 226 227 Impact(target, args); 228 } 229 } 230 } 231