Note: Projectiles that can track their target need minor additional changes, but for InstantHit (which already implemented support for this) and unguided projectiles (Bullet, GravityBomb) this commit is already sufficient.
324 lines
10 KiB
C#
324 lines
10 KiB
C#
#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<AttackBaseInfo>
|
|
{
|
|
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<IRangeModifierInfo>().Select(m => m.GetRangeModifierDefault())));
|
|
|
|
base.RulesetLoaded(rules, ai);
|
|
}
|
|
}
|
|
|
|
public class Armament : ConditionalTrait<ArmamentInfo>, ITick, IExplodeModifier
|
|
{
|
|
public readonly WeaponInfo Weapon;
|
|
public readonly Barrel[] Barrels;
|
|
|
|
readonly Actor self;
|
|
readonly AttackBase[] attackTraits;
|
|
AttackBase attack;
|
|
Turreted turret;
|
|
AmmoPool ammoPool;
|
|
BodyOrientation coords;
|
|
IEnumerable<int> rangeModifiers;
|
|
List<Pair<int, Action>> delayedActions = new List<Pair<int, Action>>();
|
|
|
|
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<Barrel>();
|
|
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<AttackBase>().Where(a => a.Info.Armaments.Contains(Info.Name)).ToArray();
|
|
}
|
|
|
|
public virtual WDist MaxRange()
|
|
{
|
|
return new WDist(Util.ApplyPercentageModifiers(Weapon.Range.Length, rangeModifiers));
|
|
}
|
|
|
|
protected override void Created(Actor self)
|
|
{
|
|
turret = self.TraitsImplementing<Turreted>().FirstOrDefault(t => t.Name == Info.Turret);
|
|
ammoPool = self.TraitsImplementing<AmmoPool>().FirstOrDefault(la => la.Info.Name == Info.AmmoPoolName);
|
|
coords = self.Trait<BodyOrientation>();
|
|
rangeModifiers = self.TraitsImplementing<IRangeModifier>().ToArray().Select(m => m.GetRangeModifier());
|
|
|
|
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<WPos> muzzlePosition = () => self.CenterPosition + MuzzleOffset(self, barrel);
|
|
var legacyFacing = MuzzleOrientation(self, barrel).Yaw.Angle / 4;
|
|
|
|
var args = new ProjectileArgs
|
|
{
|
|
Weapon = Weapon,
|
|
Facing = legacyFacing,
|
|
|
|
DamageModifiers = self.TraitsImplementing<IFirepowerModifier>()
|
|
.Select(a => a.GetFirepowerModifier()).ToArray(),
|
|
|
|
InaccuracyModifiers = self.TraitsImplementing<IInaccuracyModifier>()
|
|
.Select(a => a.GetInaccuracyModifier()).ToArray(),
|
|
|
|
RangeModifiers = self.TraitsImplementing<IRangeModifier>()
|
|
.Select(a => a.GetRangeModifier()).ToArray(),
|
|
|
|
Source = muzzlePosition(),
|
|
CurrentSource = muzzlePosition,
|
|
SourceActor = self,
|
|
PassiveTarget = attack.Info.AttackTargetCenter ? target.CenterPosition : target.Positions.PositionClosestTo(muzzlePosition()),
|
|
GuidedTarget = target
|
|
};
|
|
|
|
foreach (var na in self.TraitsImplementing<INotifyAttack>())
|
|
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 self.TraitsImplementing<INotifyAttack>())
|
|
na.Attacking(self, target, this, barrel);
|
|
|
|
Recoil = Info.Recoil;
|
|
}
|
|
});
|
|
|
|
if (--Burst > 0)
|
|
FireDelay = Weapon.BurstDelay;
|
|
else
|
|
{
|
|
var modifiers = self.TraitsImplementing<IReloadModifier>()
|
|
.Select(m => m.GetReloadModifier());
|
|
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 self.TraitsImplementing<INotifyBurstComplete>())
|
|
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; } }
|
|
}
|
|
}
|