diff --git a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs index 3296d68e50..15aaec6864 100644 --- a/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs +++ b/OpenRA.Mods.Common/UtilityCommands/UpgradeRules.cs @@ -575,6 +575,13 @@ namespace OpenRA.Mods.Common.UtilityCommands } } + // Routed unit upgrades via the UnitUpgradeManager trait + if (engineVersion < 20141001) + { + if (depth == 0 && node.Value.Nodes.Any(n => n.Key.StartsWith("GainsStatUpgrades"))) + node.Value.Nodes.Add(new MiniYamlNode("UnitUpgradeManager", new MiniYaml(""))); + } + UpgradeActorRules(engineVersion, ref node.Value.Nodes, node, depth + 1); } } diff --git a/OpenRA.Mods.RA/Crates/UnitUpgradeCrateAction.cs b/OpenRA.Mods.RA/Crates/UnitUpgradeCrateAction.cs index 55543da2b3..47228af0ea 100644 --- a/OpenRA.Mods.RA/Crates/UnitUpgradeCrateAction.cs +++ b/OpenRA.Mods.RA/Crates/UnitUpgradeCrateAction.cs @@ -16,9 +16,12 @@ namespace OpenRA.Mods.RA.Crates [Desc("Grants an upgrade to the collector.")] public class UnitUpgradeCrateActionInfo : CrateActionInfo { - [Desc("The upgrade to grant.")] + [Desc("The upgrades to apply.")] public readonly string[] Upgrades = { }; + [Desc("Duration of the upgrade (in ticks). Set to 0 for a permanent upgrade.")] + public readonly int Duration = 0; + [Desc("The range to search for extra collectors in.", "Extra collectors will also be granted the crate action.")] public readonly WRange Range = new WRange(3); @@ -42,16 +45,8 @@ namespace OpenRA.Mods.RA.Crates bool AcceptsUpgrade(Actor a) { - return a.TraitsImplementing() - .Any(up => info.Upgrades.Any(u => up.AcceptsUpgrade(u))); - } - - void GrantActorUpgrades(Actor a) - { - foreach (var up in a.TraitsImplementing()) - foreach (var u in info.Upgrades) - if (up.AcceptsUpgrade(u)) - up.UpgradeAvailable(a, u, true); + var um = a.TraitOrDefault(); + return um != null && info.Upgrades.Any(u => um.AcceptsUpgrade(a, u)); } public override int GetSelectionShares(Actor collector) @@ -61,25 +56,32 @@ namespace OpenRA.Mods.RA.Crates public override void Activate(Actor collector) { - collector.World.AddFrameEndTask(w => GrantActorUpgrades(collector)); - var actorsInRange = self.World.FindActorsInCircle(self.CenterPosition, info.Range) - .Where(a => a != self && a.Owner == collector.Owner && AcceptsUpgrade(a)); + .Where(a => a != self && a != collector && a.Owner == collector.Owner && AcceptsUpgrade(a)); - if (actorsInRange.Any()) + if (info.MaxExtraCollectors > -1) + actorsInRange = actorsInRange.Take(info.MaxExtraCollectors); + + collector.World.AddFrameEndTask(w => { - if (info.MaxExtraCollectors > -1) - actorsInRange = actorsInRange.Take(info.MaxExtraCollectors); - - collector.World.AddFrameEndTask(w => + foreach (var a in actorsInRange.Append(collector)) { - foreach (var a in actorsInRange) + if (!a.IsInWorld || a.IsDead()) + continue; + + var um = a.TraitOrDefault(); + foreach (var u in info.Upgrades) { - if (!a.IsDead() && a.IsInWorld) - GrantActorUpgrades(a); + if (!um.AcceptsUpgrade(a, u)) + continue; + + if (info.Duration > 0) + um.GrantTimedUpgrade(a, u, info.Duration); + else + um.GrantUpgrade(a, u, this); } - }); - } + } + }); base.Activate(collector); } diff --git a/OpenRA.Mods.RA/GainsExperience.cs b/OpenRA.Mods.RA/GainsExperience.cs index 92f1e34aae..c1c88d1736 100644 --- a/OpenRA.Mods.RA/GainsExperience.cs +++ b/OpenRA.Mods.RA/GainsExperience.cs @@ -102,10 +102,10 @@ namespace OpenRA.Mods.RA Level++; - foreach (var up in self.TraitsImplementing()) + var um = self.TraitOrDefault(); + if (um != null) foreach (var u in upgrades) - if (up.AcceptsUpgrade(u)) - up.UpgradeAvailable(self, u, true); + um.GrantUpgrade(self, u, this); Sound.PlayNotification(self.World.Map.Rules, self.Owner, "Sounds", "LevelUp", self.Owner.Country.Race); self.World.AddFrameEndTask(w => w.Add(new CrateEffect(self, "levelup", info.LevelUpPalette))); diff --git a/OpenRA.Mods.RA/GlobalUpgradable.cs b/OpenRA.Mods.RA/GlobalUpgradable.cs index 021fd90c5c..a38da1f9cd 100644 --- a/OpenRA.Mods.RA/GlobalUpgradable.cs +++ b/OpenRA.Mods.RA/GlobalUpgradable.cs @@ -17,7 +17,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.RA { - public class GlobalUpgradableInfo : ITraitInfo + public class GlobalUpgradableInfo : ITraitInfo, Requires { public readonly string[] Upgrades = { }; public readonly string[] Prerequisites = { }; @@ -28,34 +28,36 @@ namespace OpenRA.Mods.RA public class GlobalUpgradable : INotifyAddedToWorld, INotifyRemovedFromWorld { readonly GlobalUpgradableInfo info; - readonly GlobalUpgradeManager manager; + readonly GlobalUpgradeManager globalManager; + readonly UpgradeManager manager; - public GlobalUpgradable(Actor actor, GlobalUpgradableInfo info) + public GlobalUpgradable(Actor self, GlobalUpgradableInfo info) { this.info = info; - manager = actor.Owner.PlayerActor.Trait(); + globalManager = self.Owner.PlayerActor.Trait(); + manager = self.Trait(); } public void AddedToWorld(Actor self) { if (info.Prerequisites.Any()) - manager.Register(self, this, info.Prerequisites); + globalManager.Register(self, this, info.Prerequisites); } public void RemovedFromWorld(Actor self) { if (info.Prerequisites.Any()) - manager.Unregister(self, this, info.Prerequisites); + globalManager.Unregister(self, this, info.Prerequisites); } public void PrerequisitesUpdated(Actor self, bool available) { - var upgrades = self.TraitsImplementing(); - foreach (var u in upgrades) + foreach (var u in info.Upgrades) { - foreach (var t in info.Upgrades) - if (u.AcceptsUpgrade(t)) - u.UpgradeAvailable(self, t, available); + if (available) + manager.GrantUpgrade(self, u, this); + else + manager.RevokeUpgrade(self, u, this); } } } diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 1401a4b0a2..cd7ccb0fed 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -564,6 +564,13 @@ + + + + + + + diff --git a/OpenRA.Mods.RA/TimedUpgradeBar.cs b/OpenRA.Mods.RA/TimedUpgradeBar.cs new file mode 100644 index 0000000000..b4f2cc56d6 --- /dev/null +++ b/OpenRA.Mods.RA/TimedUpgradeBar.cs @@ -0,0 +1,58 @@ +#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.Drawing; +using System.Linq; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA +{ + [Desc("Visualizes the remaining time for an upgrade.")] + class TimedUpgradeBarInfo : ITraitInfo, Requires + { + [Desc("Upgrade that this bar corresponds to")] + public readonly string Upgrade = null; + + public readonly Color Color = Color.Red; + + public object Create(ActorInitializer init) { return new TimedUpgradeBar(init.self, this); } + } + + class TimedUpgradeBar : ISelectionBar + { + readonly TimedUpgradeBarInfo info; + readonly Actor self; + float value; + + public TimedUpgradeBar(Actor self, TimedUpgradeBarInfo info) + { + this.self = self; + this.info = info; + + self.Trait().RegisterWatcher(info.Upgrade, Update); + } + + public void Update(int duration, int remaining) + { + value = remaining * 1f / duration; + } + + public float GetValue() + { + if (!self.Owner.IsAlliedWith(self.World.RenderPlayer)) + return 0; + + return value; + } + + public Color GetColor() { return info.Color; } + } +} diff --git a/OpenRA.Mods.RA/UpgradeManager.cs b/OpenRA.Mods.RA/UpgradeManager.cs new file mode 100644 index 0000000000..c72fd48155 --- /dev/null +++ b/OpenRA.Mods.RA/UpgradeManager.cs @@ -0,0 +1,134 @@ +#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.Drawing; +using System.Linq; +using OpenRA.Graphics; +using OpenRA.Primitives; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA +{ + public class UpgradeManagerInfo : ITraitInfo + { + public object Create(ActorInitializer init) { return new UpgradeManager(init); } + } + + public class UpgradeManager : ITick + { + class TimedUpgrade + { + public readonly string Upgrade; + public readonly int Duration; + public int Remaining; + + public TimedUpgrade(string upgrade, int duration) + { + Upgrade = upgrade; + Duration = duration; + Remaining = duration; + } + + public void Tick() { Remaining--; } + } + + readonly List timedUpgrades = new List(); + readonly Dictionary> sources = new Dictionary>(); + readonly Dictionary>> watchers = new Dictionary>>(); + readonly Lazy> upgradable; + + public UpgradeManager(ActorInitializer init) + { + upgradable = Exts.Lazy(() => init.self.TraitsImplementing()); + } + + public void GrantTimedUpgrade(Actor self, string upgrade, int duration) + { + var timed = timedUpgrades.FirstOrDefault(u => u.Upgrade == upgrade); + if (timed == null) + { + timed = new TimedUpgrade(upgrade, duration); + timedUpgrades.Add(timed); + GrantUpgrade(self, upgrade, timed); + } + else + timed.Remaining = Math.Max(duration, timed.Remaining); + } + + public void GrantUpgrade(Actor self, string upgrade, object source) + { + List ss; + if (!sources.TryGetValue(upgrade, out ss)) + { + ss = new List(); + sources.Add(upgrade, ss); + + foreach (var up in upgradable.Value) + if (up.AcceptsUpgrade(upgrade)) + up.UpgradeAvailable(self, upgrade, true); + } + + // Track the upgrade source so that the upgrade can be removed without conflicts + ss.Add(source); + } + + public void RevokeUpgrade(Actor self, string upgrade, object source) + { + // This upgrade may have been granted by multiple sources + // We must be careful to only remove the upgrade after all + // sources have been revoked + List ss; + if (!sources.TryGetValue(upgrade, out ss)) + return; + + ss.Remove(source); + if (!ss.Any()) + { + foreach (var up in upgradable.Value) + if (up.AcceptsUpgrade(upgrade)) + up.UpgradeAvailable(self, upgrade, false); + + sources.Remove(upgrade); + } + } + + public bool AcceptsUpgrade(Actor self, string upgrade) + { + return upgradable.Value.Any(up => up.AcceptsUpgrade(upgrade)); + } + + public void RegisterWatcher(string upgrade, Action action) + { + if (!watchers.ContainsKey(upgrade)) + watchers.Add(upgrade, new List>()); + + watchers[upgrade].Add(action); + } + + public void Tick(Actor self) + { + foreach (var u in timedUpgrades) + { + u.Tick(); + if (u.Remaining <= 0) + RevokeUpgrade(self, u.Upgrade, u); + + List> actions; + if (watchers.TryGetValue(u.Upgrade, out actions)) + foreach (var a in actions) + a(u.Duration, u.Remaining); + } + + timedUpgrades.RemoveAll(u => u.Remaining <= 0); + } + } +} diff --git a/mods/cnc/rules/defaults.yaml b/mods/cnc/rules/defaults.yaml index 41364123bf..c7cf54016a 100644 --- a/mods/cnc/rules/defaults.yaml +++ b/mods/cnc/rules/defaults.yaml @@ -47,6 +47,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Tank: AppearsOnRadar: @@ -100,6 +101,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Helicopter: AppearsOnRadar: @@ -138,6 +140,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Infantry: AppearsOnRadar: @@ -218,6 +221,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^CivInfantry: Inherits: ^Infantry @@ -324,6 +328,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Ship: AppearsOnRadar: @@ -357,6 +362,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Building: AppearsOnRadar: diff --git a/mods/d2k/rules/defaults.yaml b/mods/d2k/rules/defaults.yaml index eda438f6c8..f0db34c4c1 100644 --- a/mods/d2k/rules/defaults.yaml +++ b/mods/d2k/rules/defaults.yaml @@ -46,6 +46,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Tank: AppearsOnRadar: @@ -95,6 +96,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Husk: Health: @@ -221,6 +223,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Plane: AppearsOnRadar: @@ -253,6 +256,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Helicopter: Inherits: ^Plane diff --git a/mods/ra/rules/defaults.yaml b/mods/ra/rules/defaults.yaml index 4fa5754fab..53158a0255 100644 --- a/mods/ra/rules/defaults.yaml +++ b/mods/ra/rules/defaults.yaml @@ -60,6 +60,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Tank: AppearsOnRadar: @@ -123,6 +124,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Infantry: AppearsOnRadar: @@ -207,6 +209,7 @@ HealIfBelow: 1 DamageCooldown: 125 RequiresUpgrade: selfheal + UpgradeManager: ^Ship: AppearsOnRadar: @@ -248,6 +251,7 @@ DamageCooldown: 125 RequiresUpgrade: selfheal IronCurtainable: + UpgradeManager: ^Plane: AppearsOnRadar: @@ -292,6 +296,7 @@ DamageCooldown: 125 RequiresUpgrade: selfheal IronCurtainable: + UpgradeManager: ^Helicopter: Inherits: ^Plane @@ -348,6 +353,7 @@ Demolishable: ScriptTriggers: IronCurtainable: + UpgradeManager: ^Defense: Inherits: ^Building diff --git a/mods/ts/rules/defaults.yaml b/mods/ts/rules/defaults.yaml index 101b64c0e1..18d25dab0f 100644 --- a/mods/ts/rules/defaults.yaml +++ b/mods/ts/rules/defaults.yaml @@ -157,6 +157,7 @@ DeathSounds@ZAPPED: DeathSound: Zapped DeathTypes: 6 + UpgradeManager: ^CivilianInfantry: Inherits: ^Infantry @@ -243,6 +244,7 @@ Explodes: Weapon: UnitExplodeSmall EmptyWeapon: UnitExplodeSmall + UpgradeManager: ^Tank: AppearsOnRadar: @@ -302,6 +304,7 @@ Explodes: Weapon: UnitExplodeSmall EmptyWeapon: UnitExplodeSmall + UpgradeManager: ^Helicopter: AppearsOnRadar: @@ -348,6 +351,7 @@ ScriptTriggers: Guard: Guardable: + UpgradeManager: ^BlossomTree: Tooltip: