diff --git a/OpenRA.Mods.RA/Armament.cs b/OpenRA.Mods.RA/Armament.cs index 3f23ff9a6e..9d669e4a26 100644 --- a/OpenRA.Mods.RA/Armament.cs +++ b/OpenRA.Mods.RA/Armament.cs @@ -58,6 +58,7 @@ namespace OpenRA.Mods.RA public readonly ArmamentInfo Info; public readonly WeaponInfo Weapon; public readonly Barrel[] Barrels; + public readonly Actor self; Lazy Turret; Lazy Coords; Lazy limitedAmmo; @@ -69,6 +70,7 @@ namespace OpenRA.Mods.RA public Armament(Actor self, ArmamentInfo info) { + this.self = self; Info = info; // We can't resolve these until runtime @@ -125,22 +127,22 @@ namespace OpenRA.Mods.RA // Note: facing is only used by the legacy positioning code // The world coordinate model uses Actor.Orientation - public void CheckFire(Actor self, IFacing facing, Target target) + public Barrel CheckFire(Actor self, IFacing facing, Target target) { if (FireDelay > 0) - return; + return null; if (limitedAmmo.Value != null && !limitedAmmo.Value.HasAmmo()) - return; + return null; if (!target.IsInRange(self.CenterPosition, Weapon.Range)) - return; + return null; if (Weapon.MinRange != WRange.Zero && target.IsInRange(self.CenterPosition, Weapon.MinRange)) - return; + return null; if (!Weapon.IsValidAgainst(target, self.World)) - return; + return null; var barrel = Barrels[Burst % Barrels.Length]; var muzzlePosition = self.CenterPosition + MuzzleOffset(self, barrel); @@ -185,6 +187,8 @@ namespace OpenRA.Mods.RA FireDelay = Weapon.ROF; Burst = Weapon.Burst; } + + return barrel; } public bool IsReloading { get { return FireDelay > 0; } } @@ -211,5 +215,7 @@ namespace OpenRA.Mods.RA orientation += Turret.Value.LocalOrientation(self); return orientation; } + + public Actor Actor { get { return self; } } } } diff --git a/OpenRA.Mods.RA/Attack/AttackBase.cs b/OpenRA.Mods.RA/Attack/AttackBase.cs index dad546da64..41df7f8abd 100644 --- a/OpenRA.Mods.RA/Attack/AttackBase.cs +++ b/OpenRA.Mods.RA/Attack/AttackBase.cs @@ -135,7 +135,13 @@ namespace OpenRA.Mods.RA public abstract Activity GetAttackActivity(Actor self, Target newTarget, bool allowMove); public bool HasAnyValidWeapons(Target t) { return Armaments.Any(a => a.Weapon.IsValidAgainst(t, self.World)); } - public WRange GetMaximumRange() { return Armaments.Max(a => a.Weapon.Range); } + public WRange GetMaximumRange() + { + if (!Armaments.Any()) + return WRange.Zero; + + return Armaments.Max(a => a.Weapon.Range); + } public Armament ChooseArmamentForTarget(Target t) { return Armaments.FirstOrDefault(a => a.Weapon.IsValidAgainst(t, self.World)); } diff --git a/OpenRA.Mods.RA/Attack/AttackFollow.cs b/OpenRA.Mods.RA/Attack/AttackFollow.cs index 8df68d4304..248fd77a79 100644 --- a/OpenRA.Mods.RA/Attack/AttackFollow.cs +++ b/OpenRA.Mods.RA/Attack/AttackFollow.cs @@ -37,7 +37,7 @@ namespace OpenRA.Mods.RA return base.CanAttack(self, target); } - public void Tick(Actor self) + public virtual void Tick(Actor self) { DoAttack(self, Target); IsAttacking = Target.IsValidFor(self); diff --git a/OpenRA.Mods.RA/Attack/AttackGarrisoned.cs b/OpenRA.Mods.RA/Attack/AttackGarrisoned.cs new file mode 100644 index 0000000000..254e5391a6 --- /dev/null +++ b/OpenRA.Mods.RA/Attack/AttackGarrisoned.cs @@ -0,0 +1,192 @@ +#region Copyright & License Information +/* + * Copyright 2007-2014 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. For more information, + * see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenRA.FileFormats; +using OpenRA.Graphics; +using OpenRA.Mods.RA.Buildings; +using OpenRA.Mods.RA.Move; +using OpenRA.Mods.RA.Render; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA +{ + public class FirePort + { + public WVec Offset; + public WAngle Yaw; + public WAngle Cone; + } + + public class AttackGarrisonedInfo : AttackFollowInfo, Requires + { + [Desc("Fire port offsets in local coordinates")] + public readonly WRange[] PortOffsets = {}; + + [Desc("Fire port yaw angles")] + public readonly WAngle[] PortYaws = {}; + + [Desc("Fire port yaw cone angle")] + public readonly WAngle[] PortCones = {}; + + public override object Create(ActorInitializer init) { return new AttackGarrisoned(init.self, this); } + } + + public class AttackGarrisoned : AttackFollow, INotifyPassengerEntered, INotifyPassengerExited, IRender + { + public readonly FirePort[] Ports; + + AttackGarrisonedInfo info; + Lazy coords; + List armaments; + List muzzles; + Dictionary paxFacing; + Dictionary paxPos; + Dictionary paxRender; + + + public AttackGarrisoned(Actor self, AttackGarrisonedInfo info) + : base(self, info) + { + this.info = info; + coords = Lazy.New(() => self.Trait()); + armaments = new List(); + muzzles = new List(); + paxFacing = new Dictionary(); + paxPos = new Dictionary(); + paxRender = new Dictionary(); + + GetArmaments = () => armaments; + + + if (info.PortOffsets.Length % 3 != 0 || info.PortOffsets.Length == 0) + throw new InvalidOperationException("PortOffsets array length must be a multiple of three"); + + if (info.PortYaws.Length * 3 != info.PortOffsets.Length) + throw new InvalidOperationException("FireYaw must define an angle for each port"); + + if (info.PortCones.Length * 3 != info.PortOffsets.Length) + throw new InvalidOperationException("PortCones must define an angle for each port"); + + var p = new List(); + for (var i = 0; i < info.PortOffsets.Length / 3; i++) + { + p.Add(new FirePort + { + Offset = new WVec(info.PortOffsets[3*i], info.PortOffsets[3*i + 1], info.PortOffsets[3*i + 2]), + Yaw = info.PortYaws[i], + Cone = info.PortCones[i], + }); + } + + Ports = p.ToArray(); + } + + public void PassengerEntered(Actor self, Actor passenger) + { + paxFacing.Add(passenger, passenger.Trait()); + paxPos.Add(passenger, passenger.Trait()); + paxRender.Add(passenger, passenger.Trait()); + armaments = armaments.Append(passenger.TraitsImplementing() + .Where(a => info.Armaments.Contains(a.Info.Name)) + .ToArray()).ToList(); + } + + public void PassengerExited(Actor self, Actor passenger) + { + paxFacing.Remove(passenger); + paxPos.Remove(passenger); + paxRender.Remove(passenger); + armaments.RemoveAll(a => a.Actor == passenger); + } + + + FirePort SelectFirePort(Actor self, WAngle targetYaw) + { + // Pick a random port that faces the target + var bodyYaw = facing.Value != null ? WAngle.FromFacing(facing.Value.Facing) : WAngle.Zero; + var indices = Exts.MakeArray(Ports.Length, i => i).Shuffle(self.World.SharedRandom); + foreach (var i in indices) + { + var yaw = bodyYaw + Ports[i].Yaw; + var leftTurn = (yaw - targetYaw).Angle; + var rightTurn = (targetYaw - yaw).Angle; + if (Math.Min(leftTurn, rightTurn) <= Ports[i].Cone.Angle) + return Ports[i]; + } + + return null; + } + + WVec PortOffset(Actor self, FirePort p) + { + var bodyOrientation = coords.Value.QuantizeOrientation(self, self.Orientation); + return coords.Value.LocalToWorld(p.Offset.Rotate(bodyOrientation)); + } + + public override void DoAttack(Actor self, Target target) + { + if (!CanAttack(self, target)) + return; + + var pos = self.CenterPosition; + var targetYaw = WAngle.FromFacing(Traits.Util.GetFacing(target.CenterPosition - self.CenterPosition, 0)); + + foreach (var a in Armaments) + { + var port = SelectFirePort(self, targetYaw); + if (port == null) + return; + + var muzzleFacing = targetYaw.Angle / 4; + paxFacing[a.Actor].Facing = muzzleFacing; + paxPos[a.Actor].SetVisualPosition(a.Actor, pos + PortOffset(self, port)); + + var barrel = a.CheckFire(a.Actor, facing.Value, target); + if (barrel != null && a.Info.MuzzleSequence != null) + { + // Muzzle facing is fixed once the firing starts + var muzzleAnim = new Animation(paxRender[a.Actor].GetImage(a.Actor), () => muzzleFacing); + var sequence = a.Info.MuzzleSequence; + + if (a.Info.MuzzleSplitFacings > 0) + sequence += Traits.Util.QuantizeFacing(muzzleFacing, a.Info.MuzzleSplitFacings).ToString(); + + var muzzleFlash = new AnimationWithOffset(muzzleAnim, + () => PortOffset(self, port), + () => false, + p => WithTurret.ZOffsetFromCenter(self, p, 1024)); + + muzzles.Add(muzzleFlash); + muzzleAnim.PlayThen(sequence, () => muzzles.Remove(muzzleFlash)); + } + } + } + + public IEnumerable Render(Actor self, WorldRenderer wr) + { + // Display muzzle flashes + foreach (var m in muzzles) + foreach (var r in m.Render(self, wr, wr.Palette("effect"), 1f)) + yield return r; + } + + public override void Tick(Actor self) + { + base.Tick(self); + + // Take a copy so that Tick() can remove animations + foreach (var m in muzzles.ToList()) + m.Animation.Tick(); + } + } +} diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index e21363edca..87dd4e031a 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -486,6 +486,7 @@ +