#region Copyright & License Information /* * Copyright 2007-2017 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Linq; using OpenRA.GameRules; using OpenRA.Primitives; using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { public class Barrel { public WVec Offset; public WAngle Yaw; } [Desc("Allows you to attach weapons to the unit (use @IdentifierSuffix for > 1)")] public class ArmamentInfo : ConditionalTraitInfo, Requires { public readonly string Name = "primary"; [WeaponReference, FieldLoader.Require] [Desc("Has to be defined in weapons.yaml as well.")] public readonly string Weapon = null; [Desc("Which limited ammo pool (if present) should this armament be assigned to.")] public readonly string AmmoPoolName = "primary"; [Desc("Which turret (if present) should this armament be assigned to.")] public readonly string Turret = "primary"; [Desc("Time (in frames) until the weapon can fire again.")] public readonly int FireDelay = 0; [Desc("Muzzle position relative to turret or body. (forward, right, up) triples")] public readonly WVec[] LocalOffset = { }; [Desc("Muzzle yaw relative to turret or body.")] public readonly WAngle[] LocalYaw = { }; [Desc("Move the turret backwards when firing.")] public readonly WDist Recoil = WDist.Zero; [Desc("Recoil recovery per-frame")] public readonly WDist RecoilRecovery = new WDist(9); [Desc("Muzzle flash sequence to render")] public readonly string MuzzleSequence = null; [Desc("Palette to render Muzzle flash sequence in")] [PaletteReference] public readonly string MuzzlePalette = "effect"; [Desc("Use multiple muzzle images if non-zero")] public readonly int MuzzleSplitFacings = 0; public WeaponInfo WeaponInfo { get; private set; } public WDist ModifiedRange { get; private set; } public readonly Stance TargetStances = Stance.Enemy; public readonly Stance ForceTargetStances = Stance.Enemy | Stance.Neutral | Stance.Ally; // TODO: instead of having multiple Armaments and unique AttackBase, // an actor should be able to have multiple AttackBases with // a single corresponding Armament each public readonly string Cursor = "attack"; // TODO: same as above public readonly string OutsideRangeCursor = "attackoutsiderange"; public override object Create(ActorInitializer init) { return new Armament(init.Self, this); } public override void RulesetLoaded(Ruleset rules, ActorInfo ai) { WeaponInfo weaponInfo; var weaponToLower = Weapon.ToLowerInvariant(); if (!rules.Weapons.TryGetValue(weaponToLower, out weaponInfo)) throw new YamlException("Weapons Ruleset does not contain an entry '{0}'".F(weaponToLower)); WeaponInfo = weaponInfo; ModifiedRange = new WDist(Util.ApplyPercentageModifiers( WeaponInfo.Range.Length, ai.TraitInfos().Select(m => m.GetRangeModifierDefault()))); base.RulesetLoaded(rules, ai); } } public class Armament : ConditionalTrait, ITick, IExplodeModifier { public readonly WeaponInfo Weapon; public readonly Barrel[] Barrels; readonly Actor self; readonly AttackBase[] attackTraits; AttackBase attack; Turreted turret; AmmoPool ammoPool; BodyOrientation coords; INotifyBurstComplete[] notifyBurstComplete; INotifyAttack[] notifyAttacks; IEnumerable rangeModifiers; IEnumerable reloadModifiers; IEnumerable damageModifiers; IEnumerable inaccuracyModifiers; List> delayedActions = new List>(); public WDist Recoil; public int FireDelay { get; protected set; } public int Burst { get; protected set; } public Armament(Actor self, ArmamentInfo info) : base(info) { this.self = self; Weapon = info.WeaponInfo; Burst = Weapon.Burst; var barrels = new List(); for (var i = 0; i < info.LocalOffset.Length; i++) { barrels.Add(new Barrel { Offset = info.LocalOffset[i], Yaw = info.LocalYaw.Length > i ? info.LocalYaw[i] : WAngle.Zero }); } if (barrels.Count == 0) barrels.Add(new Barrel { Offset = WVec.Zero, Yaw = WAngle.Zero }); Barrels = barrels.ToArray(); attackTraits = self.TraitsImplementing().Where(a => a.Info.Armaments.Contains(Info.Name)).ToArray(); } public virtual WDist MaxRange() { return new WDist(Util.ApplyPercentageModifiers(Weapon.Range.Length, rangeModifiers.ToArray())); } protected override void Created(Actor self) { turret = self.TraitsImplementing().FirstOrDefault(t => t.Name == Info.Turret); ammoPool = self.TraitsImplementing().FirstOrDefault(la => la.Info.Name == Info.AmmoPoolName); coords = self.Trait(); notifyBurstComplete = self.TraitsImplementing().ToArray(); notifyAttacks = self.TraitsImplementing().ToArray(); rangeModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetRangeModifier()); reloadModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetReloadModifier()); damageModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetFirepowerModifier()); inaccuracyModifiers = self.TraitsImplementing().ToArray().Select(m => m.GetInaccuracyModifier()); base.Created(self); } protected virtual void Tick(Actor self) { if (IsTraitDisabled) return; if (FireDelay > 0) --FireDelay; Recoil = new WDist(Math.Max(0, Recoil.Length - Info.RecoilRecovery.Length)); for (var i = 0; i < delayedActions.Count; i++) { var x = delayedActions[i]; if (--x.First <= 0) x.Second(); delayedActions[i] = x; } delayedActions.RemoveAll(a => a.First <= 0); } void ITick.Tick(Actor self) { // Split into a protected method to allow subclassing Tick(self); } protected void ScheduleDelayedAction(int t, Action a) { if (t > 0) delayedActions.Add(Pair.New(t, a)); else a(); } // Note: facing is only used by the legacy positioning code // The world coordinate model uses Actor.Orientation public virtual Barrel CheckFire(Actor self, IFacing facing, Target target) { attack = attackTraits.FirstOrDefault(Exts.IsTraitEnabled); if (attack == null) return null; if (IsReloading) return null; if (ammoPool != null && !ammoPool.HasAmmo()) return null; if (turret != null && !turret.HasAchievedDesiredFacing) return null; if (!target.IsInRange(self.CenterPosition, MaxRange())) return null; if (Weapon.MinRange != WDist.Zero && target.IsInRange(self.CenterPosition, Weapon.MinRange)) return null; if (!Weapon.IsValidAgainst(target, self.World, self)) return null; var barrel = Barrels[Burst % Barrels.Length]; Func muzzlePosition = () => self.CenterPosition + MuzzleOffset(self, barrel); var legacyFacing = MuzzleOrientation(self, barrel).Yaw.Angle / 4; var args = new ProjectileArgs { Weapon = Weapon, Facing = legacyFacing, DamageModifiers = damageModifiers.ToArray(), InaccuracyModifiers = inaccuracyModifiers.ToArray(), RangeModifiers = rangeModifiers.ToArray(), Source = muzzlePosition(), CurrentSource = muzzlePosition, SourceActor = self, PassiveTarget = attack.Info.AttackTargetCenter ? target.CenterPosition : target.Positions.PositionClosestTo(muzzlePosition()), GuidedTarget = target }; foreach (var na in notifyAttacks) na.PreparingAttack(self, target, this, barrel); ScheduleDelayedAction(Info.FireDelay, () => { if (args.Weapon.Projectile != null) { var projectile = args.Weapon.Projectile.Create(args); if (projectile != null) self.World.Add(projectile); if (args.Weapon.Report != null && args.Weapon.Report.Any()) Game.Sound.Play(SoundType.World, args.Weapon.Report.Random(self.World.SharedRandom), self.CenterPosition); if (Burst == args.Weapon.Burst && args.Weapon.StartBurstReport != null && args.Weapon.StartBurstReport.Any()) Game.Sound.Play(SoundType.World, args.Weapon.StartBurstReport.Random(self.World.SharedRandom), self.CenterPosition); foreach (var na in notifyAttacks) na.Attacking(self, target, this, barrel); Recoil = Info.Recoil; } }); if (--Burst > 0) FireDelay = Weapon.BurstDelay; else { var modifiers = reloadModifiers.ToArray(); FireDelay = Util.ApplyPercentageModifiers(Weapon.ReloadDelay, modifiers); Burst = Weapon.Burst; if (args.Weapon.AfterFireSound != null && args.Weapon.AfterFireSound.Any()) { ScheduleDelayedAction(Weapon.AfterFireSoundDelay, () => { Game.Sound.Play(SoundType.World, Weapon.AfterFireSound.Random(self.World.SharedRandom), self.CenterPosition); }); } foreach (var nbc in notifyBurstComplete) nbc.FiredBurst(self, target, this); } return barrel; } public virtual bool OutOfAmmo { get { return ammoPool != null && !ammoPool.Info.SelfReloads && !ammoPool.HasAmmo(); } } public virtual bool IsReloading { get { return FireDelay > 0 || IsTraitDisabled; } } public virtual bool AllowExplode { get { return !IsReloading; } } bool IExplodeModifier.ShouldExplode(Actor self) { return AllowExplode; } public virtual WVec MuzzleOffset(Actor self, Barrel b) { var bodyOrientation = coords.QuantizeOrientation(self, self.Orientation); var localOffset = b.Offset + new WVec(-Recoil, WDist.Zero, WDist.Zero); if (turret != null) { // WorldOrientation is quantized to satisfy the *Fudges. // Need to then convert back to a pseudo-local coordinate space, apply offsets, // then rotate back at the end var turretOrientation = turret.WorldOrientation(self) - bodyOrientation; localOffset = localOffset.Rotate(turretOrientation); localOffset += turret.Offset; } return coords.LocalToWorld(localOffset.Rotate(bodyOrientation)); } public virtual WRot MuzzleOrientation(Actor self, Barrel b) { var orientation = turret != null ? turret.WorldOrientation(self) : coords.QuantizeOrientation(self, self.Orientation); return orientation + WRot.FromYaw(b.Yaw); } public Actor Actor { get { return self; } } } }