diff --git a/OpenRA.Mods.Common/AI/HackyAI.cs b/OpenRA.Mods.Common/AI/HackyAI.cs index f4cd009226..59653c67a2 100644 --- a/OpenRA.Mods.Common/AI/HackyAI.cs +++ b/OpenRA.Mods.Common/AI/HackyAI.cs @@ -500,9 +500,9 @@ namespace OpenRA.Mods.Common.AI if (aircraftInfo == null) return true; - var ammoPoolsInfo = actorInfo.TraitInfos(); - - if (ammoPoolsInfo.Any(x => !x.SelfReloads)) + // If the aircraft has at least 1 AmmoPool and defines 1 or more RearmBuildings, check if we have enough of those + var hasAmmoPool = actorInfo.TraitInfos().Any(); + if (hasAmmoPool && aircraftInfo.RearmBuildings.Count > 0) { var countOwnAir = CountUnits(actorInfo.Name, Player); var countBuildings = aircraftInfo.RearmBuildings.Sum(b => CountBuilding(b, Player)); diff --git a/OpenRA.Mods.Common/AI/States/AirStates.cs b/OpenRA.Mods.Common/AI/States/AirStates.cs index cb0de55147..23bfa9e651 100644 --- a/OpenRA.Mods.Common/AI/States/AirStates.cs +++ b/OpenRA.Mods.Common/AI/States/AirStates.cs @@ -120,7 +120,7 @@ namespace OpenRA.Mods.Common.AI protected static bool ReloadsAutomatically(Actor a) { var ammoPools = a.TraitsImplementing(); - return ammoPools.All(x => x.Info.SelfReloads); + return ammoPools.All(x => x.SelfReloads); } protected static bool IsRearm(Actor a) diff --git a/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs b/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs index 26387e5adf..00855c554c 100644 --- a/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs +++ b/OpenRA.Mods.Common/Activities/Air/FlyAttack.cs @@ -21,7 +21,6 @@ namespace OpenRA.Mods.Common.Activities readonly Target target; readonly Aircraft aircraft; readonly AttackPlane attackPlane; - readonly AmmoPool[] ammoPools; int ticksUntilTurn; @@ -30,7 +29,6 @@ namespace OpenRA.Mods.Common.Activities this.target = target; aircraft = self.Trait(); attackPlane = self.TraitOrDefault(); - ammoPools = self.TraitsImplementing().ToArray(); ticksUntilTurn = attackPlane.AttackPlaneInfo.AttackTurnDelay; } @@ -46,8 +44,8 @@ namespace OpenRA.Mods.Common.Activities if (!target.IsValidFor(self)) return NextActivity; - // TODO: This should check whether there is ammo left that is actually suitable for the target - if (ammoPools.All(x => !x.Info.SelfReloads && !x.HasAmmo())) + // If all valid weapons have depleted their ammo, return to RearmBuilding to reload and then resume the activity + if (attackPlane.Armaments.All(x => x.OutOfAmmo || !x.Weapon.IsValidAgainst(target, self.World, self))) return ActivityUtils.SequenceActivities(new ReturnToBase(self, aircraft.Info.AbortOnResupply), this); if (attackPlane != null) diff --git a/OpenRA.Mods.Common/Activities/Air/HeliAttack.cs b/OpenRA.Mods.Common/Activities/Air/HeliAttack.cs index 74fc690899..044cf337c5 100644 --- a/OpenRA.Mods.Common/Activities/Air/HeliAttack.cs +++ b/OpenRA.Mods.Common/Activities/Air/HeliAttack.cs @@ -21,7 +21,6 @@ namespace OpenRA.Mods.Common.Activities { readonly Aircraft helicopter; readonly AttackHeli attackHeli; - readonly AmmoPool[] ammoPools; readonly bool attackOnlyVisibleTargets; Target target; @@ -46,7 +45,6 @@ namespace OpenRA.Mods.Common.Activities Target = target; helicopter = self.Trait(); attackHeli = self.Trait(); - ammoPools = self.TraitsImplementing().ToArray(); this.attackOnlyVisibleTargets = attackOnlyVisibleTargets; } @@ -74,8 +72,8 @@ namespace OpenRA.Mods.Common.Activities return new HeliFly(self, newTarget); } - // If any AmmoPool is depleted and no weapon is valid against target, return to helipad to reload and then resume the activity - if (ammoPools.Any(x => !x.Info.SelfReloads && !x.HasAmmo()) && !attackHeli.HasAnyValidWeapons(target)) + // If all valid weapons have depleted their ammo, return to RearmBuilding to reload and then resume the activity + if (attackHeli.Armaments.All(x => x.OutOfAmmo || !x.Weapon.IsValidAgainst(target, self.World, self))) return ActivityUtils.SequenceActivities(new HeliReturnToBase(self, helicopter.Info.AbortOnResupply), this); var dist = targetPos - pos; diff --git a/OpenRA.Mods.Common/Activities/Air/HeliReturnToBase.cs b/OpenRA.Mods.Common/Activities/Air/HeliReturnToBase.cs index 498fe16445..4ce9768e02 100644 --- a/OpenRA.Mods.Common/Activities/Air/HeliReturnToBase.cs +++ b/OpenRA.Mods.Common/Activities/Air/HeliReturnToBase.cs @@ -109,7 +109,7 @@ namespace OpenRA.Mods.Common.Activities return true; return heli.Info.RearmBuildings.Contains(dest.Info.Name) && self.TraitsImplementing() - .Any(p => !p.Info.SelfReloads && !p.FullAmmo()); + .Any(p => !p.SelfReloads && !p.FullAmmo()); } } } diff --git a/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs b/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs index fd7c2df585..5c456a8d6c 100644 --- a/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs +++ b/OpenRA.Mods.Common/Activities/Air/ReturnToBase.cs @@ -105,7 +105,7 @@ namespace OpenRA.Mods.Common.Activities return true; return planeInfo.RearmBuildings.Contains(dest.Info.Name) && self.TraitsImplementing() - .Any(p => !p.Info.SelfReloads && !p.FullAmmo()); + .Any(p => !p.SelfReloads && !p.FullAmmo()); } public override Activity Tick(Actor self) diff --git a/OpenRA.Mods.Common/Activities/Rearm.cs b/OpenRA.Mods.Common/Activities/Rearm.cs index 6f37d0b643..5c15f4dcf2 100644 --- a/OpenRA.Mods.Common/Activities/Rearm.cs +++ b/OpenRA.Mods.Common/Activities/Rearm.cs @@ -20,21 +20,15 @@ namespace OpenRA.Mods.Common.Activities public class Rearm : Activity { readonly AmmoPool[] ammoPools; - readonly Dictionary ammoPoolsReloadTimes; public Rearm(Actor self) { - ammoPools = self.TraitsImplementing().Where(p => !p.Info.SelfReloads).ToArray(); - - if (ammoPools == null) - return; - - ammoPoolsReloadTimes = ammoPools.ToDictionary(x => x, y => y.Info.ReloadDelay); + ammoPools = self.TraitsImplementing().Where(p => !p.SelfReloads).ToArray(); } public override Activity Tick(Actor self) { - if (IsCanceled || ammoPoolsReloadTimes == null) + if (IsCanceled) return NextActivity; var needsReloading = false; @@ -46,30 +40,32 @@ namespace OpenRA.Mods.Common.Activities needsReloading = true; - if (--ammoPoolsReloadTimes[pool] > 0) - continue; + Reload(self, pool); + } + return needsReloading ? this : NextActivity; + } + + void Reload(Actor self, AmmoPool ammoPool) + { + if (--ammoPool.RemainingTicks == 0) + { // HACK to check if we are on the helipad/airfield/etc. var hostBuilding = self.World.ActorMap.GetActorsAt(self.Location) .FirstOrDefault(a => a.Info.HasTraitInfo()); if (hostBuilding == null || !hostBuilding.IsInWorld) - return NextActivity; - - if (!pool.GiveAmmo()) - continue; + return; foreach (var host in hostBuilding.TraitsImplementing()) host.Rearming(hostBuilding, self); - var sound = pool.Info.RearmSound; - if (sound != null) - Game.Sound.PlayToPlayer(SoundType.World, self.Owner, sound, self.CenterPosition); + ammoPool.RemainingTicks = ammoPool.Info.ReloadDelay; + if (!string.IsNullOrEmpty(ammoPool.Info.RearmSound)) + Game.Sound.PlayToPlayer(SoundType.World, self.Owner, ammoPool.Info.RearmSound, self.CenterPosition); - ammoPoolsReloadTimes[pool] = pool.Info.ReloadDelay; + ammoPool.GiveAmmo(self, ammoPool.Info.ReloadCount); } - - return needsReloading ? this : NextActivity; } } } diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 0c47fcb548..333fc71d68 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -359,6 +359,7 @@ + diff --git a/OpenRA.Mods.Common/Traits/AmmoPool.cs b/OpenRA.Mods.Common/Traits/AmmoPool.cs index fc7eb7746b..6eccc849b0 100644 --- a/OpenRA.Mods.Common/Traits/AmmoPool.cs +++ b/OpenRA.Mods.Common/Traits/AmmoPool.cs @@ -18,7 +18,7 @@ namespace OpenRA.Mods.Common.Traits [Desc("Actor has a limited amount of ammo, after using it all the actor must reload in some way.")] public class AmmoPoolInfo : ITraitInfo { - [Desc("Name of this ammo pool, used to link armaments to this pool.")] + [Desc("Name of this ammo pool, used to link armaments and reload traits to this pool.")] public readonly string Name = "primary"; [Desc("How much ammo does this pool contain when fully loaded.")] @@ -42,62 +42,69 @@ namespace OpenRA.Mods.Common.Traits [Desc("Sound to play for each reloaded ammo magazine.")] public readonly string RearmSound = null; + // HACK: Temporarily kept until Rearm activity is gone for good [Desc("Time to reload per ReloadCount on airfield etc.")] public readonly int ReloadDelay = 50; - [Desc("Whether or not ammo is replenished on its own.")] - public readonly bool SelfReloads = false; - - [Desc("Time to reload per ReloadCount when actor 'SelfReloads'.")] - public readonly int SelfReloadDelay = 50; - - [Desc("Whether or not reload timer should be reset when ammo has been fired.")] - public readonly bool ResetOnFire = false; - public object Create(ActorInitializer init) { return new AmmoPool(init.Self, this); } } - public class AmmoPool : INotifyAttack, IPips, ITick, ISync + public class AmmoPool : INotifyCreated, INotifyAttack, IPips, ISync { public readonly AmmoPoolInfo Info; - [Sync] public int CurrentAmmo; + ConditionManager conditionManager; + int emptyToken = ConditionManager.InvalidConditionToken; + + bool selfReloads; + + // HACK: Temporarily needed until Rearm activity is gone for good [Sync] public int RemainingTicks; - public int PreviousAmmo; + [Sync] int currentAmmo; public AmmoPool(Actor self, AmmoPoolInfo info) { Info = info; if (Info.InitialAmmo < Info.Ammo && Info.InitialAmmo >= 0) - CurrentAmmo = Info.InitialAmmo; + currentAmmo = Info.InitialAmmo; else - CurrentAmmo = Info.Ammo; - - RemainingTicks = Info.SelfReloadDelay; - PreviousAmmo = GetAmmoCount(); + currentAmmo = Info.Ammo; } - public int GetAmmoCount() { return CurrentAmmo; } - public bool FullAmmo() { return CurrentAmmo == Info.Ammo; } - public bool HasAmmo() { return CurrentAmmo > 0; } + public int GetAmmoCount() { return currentAmmo; } + public bool FullAmmo() { return currentAmmo == Info.Ammo; } + public bool HasAmmo() { return currentAmmo > 0; } public bool GiveAmmo() { - if (CurrentAmmo >= Info.Ammo) + if (currentAmmo >= Info.Ammo) return false; - ++CurrentAmmo; + ++currentAmmo; return true; } public bool TakeAmmo() { - if (CurrentAmmo <= 0) + if (currentAmmo <= 0) return false; - --CurrentAmmo; + --currentAmmo; return true; } + // This mostly serves to avoid complicated ReloadAmmoPool look-ups in various other places. + // TODO: Investigate removing this when the Rearm activity is replaced with a condition-based solution. + public bool SelfReloads { get { return selfReloads; } } + + void INotifyCreated.Created(Actor self) + { + conditionManager = self.TraitOrDefault(); + selfReloads = self.TraitsImplementing().Any(r => r.Info.AmmoPool == Info.Name && r.Info.RequiresCondition == null); + + // HACK: Temporarily needed until Rearm activity is gone for good + RemainingTicks = Info.ReloadDelay; + } + void INotifyAttack.Attacking(Actor self, Target target, Armament a, Barrel barrel) { if (a != null && a.Info.AmmoPoolName == Info.Name) @@ -106,35 +113,12 @@ namespace OpenRA.Mods.Common.Traits void INotifyAttack.PreparingAttack(Actor self, Target target, Armament a, Barrel barrel) { } - void ITick.Tick(Actor self) - { - if (!Info.SelfReloads) - return; - - // Resets the tick counter if ammo was fired. - if (Info.ResetOnFire && GetAmmoCount() < PreviousAmmo) - { - RemainingTicks = Info.SelfReloadDelay; - PreviousAmmo = GetAmmoCount(); - } - - if (!FullAmmo() && --RemainingTicks == 0) - { - RemainingTicks = Info.SelfReloadDelay; - - for (var i = 0; i < Info.ReloadCount; i++) - GiveAmmo(); - - PreviousAmmo = GetAmmoCount(); - } - } - public IEnumerable GetPips(Actor self) { var pips = Info.PipCount >= 0 ? Info.PipCount : Info.Ammo; return Enumerable.Range(0, pips).Select(i => - (CurrentAmmo * pips) / Info.Ammo > i ? + (currentAmmo * pips) / Info.Ammo > i ? Info.PipType : Info.PipTypeEmpty); } } diff --git a/OpenRA.Mods.Common/Traits/Armament.cs b/OpenRA.Mods.Common/Traits/Armament.cs index 173a2bdf3d..34ee3e1d2a 100644 --- a/OpenRA.Mods.Common/Traits/Armament.cs +++ b/OpenRA.Mods.Common/Traits/Armament.cs @@ -107,6 +107,7 @@ namespace OpenRA.Mods.Common.Traits readonly Actor self; Turreted turret; AmmoPool ammoPool; + ReloadAmmoPool reloadAmmoPool; BodyOrientation coords; INotifyBurstComplete[] notifyBurstComplete; INotifyAttack[] notifyAttacks; @@ -157,6 +158,7 @@ namespace OpenRA.Mods.Common.Traits { turret = self.TraitsImplementing().FirstOrDefault(t => t.Name == Info.Turret); ammoPool = self.TraitsImplementing().FirstOrDefault(la => la.Info.Name == Info.AmmoPoolName); + reloadAmmoPool = self.TraitsImplementing().FirstOrDefault(ra => ra.Info.AmmoPool == Info.AmmoPoolName); coords = self.Trait(); notifyBurstComplete = self.TraitsImplementing().ToArray(); notifyAttacks = self.TraitsImplementing().ToArray(); @@ -339,7 +341,7 @@ namespace OpenRA.Mods.Common.Traits } } - public virtual bool OutOfAmmo { get { return ammoPool != null && !ammoPool.Info.SelfReloads && !ammoPool.HasAmmo(); } } + public virtual bool OutOfAmmo { get { return ammoPool != null && !ammoPool.HasAmmo() && (reloadAmmoPool == null || reloadAmmoPool.IsTraitDisabled); } } public virtual bool IsReloading { get { return FireDelay > 0 || IsTraitDisabled; } } public virtual bool AllowExplode { get { return !IsReloading; } } bool IExplodeModifier.ShouldExplode(Actor self) { return AllowExplode; } diff --git a/OpenRA.Mods.Common/Traits/ReloadAmmoPool.cs b/OpenRA.Mods.Common/Traits/ReloadAmmoPool.cs new file mode 100644 index 0000000000..638d62a9ae --- /dev/null +++ b/OpenRA.Mods.Common/Traits/ReloadAmmoPool.cs @@ -0,0 +1,88 @@ +#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. For more information, + * see COPYING. + */ +#endregion + +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Reloads an ammo pool.")] + public class ReloadAmmoPoolInfo : PausableConditionalTraitInfo + { + [Desc("Reload ammo pool with this name.")] + public readonly string AmmoPool = "primary"; + + [Desc("Reload time in ticks per Count.")] + public readonly int Delay = 50; + + [Desc("How much ammo is reloaded after Delay.")] + public readonly int Count = 1; + + [Desc("Whether or not reload timer should be reset when ammo has been fired.")] + public readonly bool ResetOnFire = false; + + [Desc("Play this sound each time ammo is reloaded.")] + public readonly string Sound = null; + + public override object Create(ActorInitializer init) { return new ReloadAmmoPool(this); } + + public override void RulesetLoaded(Ruleset rules, ActorInfo ai) + { + if (ai.TraitInfos().Count(ap => ap.Name == AmmoPool) != 1) + throw new YamlException("ReloadsAmmoPool.AmmoPool requires exactly one AmmoPool with matching Name!"); + + base.RulesetLoaded(rules, ai); + } + } + + public class ReloadAmmoPool : PausableConditionalTrait, ITick, INotifyCreated, INotifyAttack, ISync + { + AmmoPool ammoPool; + + [Sync] int remainingTicks; + + public ReloadAmmoPool(ReloadAmmoPoolInfo info) + : base(info) { } + + void INotifyCreated.Created(Actor self) + { + ammoPool = self.TraitsImplementing().Single(ap => ap.Info.Name == Info.AmmoPool); + remainingTicks = Info.Delay; + } + + void INotifyAttack.Attacking(Actor self, Target target, Armament a, Barrel barrel) + { + if (Info.ResetOnFire) + remainingTicks = Info.Delay; + } + + void INotifyAttack.PreparingAttack(Actor self, Target target, Armament a, Barrel barrel) { } + + void ITick.Tick(Actor self) + { + if (IsTraitPaused || IsTraitDisabled) + return; + + Reload(self, Info.Delay, Info.Count, Info.Sound); + } + + protected virtual void Reload(Actor self, int reloadDelay, int reloadCount, string sound) + { + if (!ammoPool.FullAmmo() && --remainingTicks == 0) + { + remainingTicks = reloadDelay; + if (!string.IsNullOrEmpty(sound)) + Game.Sound.PlayToPlayer(SoundType.World, self.Owner, sound, self.CenterPosition); + + ammoPool.GiveAmmo(self, reloadCount); + } + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Repairable.cs b/OpenRA.Mods.Common/Traits/Repairable.cs index 2e2baa1802..2ea23eb628 100644 --- a/OpenRA.Mods.Common/Traits/Repairable.cs +++ b/OpenRA.Mods.Common/Traits/Repairable.cs @@ -77,7 +77,7 @@ namespace OpenRA.Mods.Common.Traits bool CanRearm() { - return ammoPools.Any(x => !x.Info.SelfReloads && !x.FullAmmo()); + return ammoPools.Any(x => !x.SelfReloads && !x.FullAmmo()); } public string VoicePhraseForOrder(Actor self, Order order)