diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 99e297493d..c321910a12 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -293,6 +293,7 @@ + @@ -472,6 +473,7 @@ + diff --git a/OpenRA.Mods.Common/Traits/GainsExperience.cs b/OpenRA.Mods.Common/Traits/GainsExperience.cs index ed8c765eec..04a7e9b73f 100644 --- a/OpenRA.Mods.Common/Traits/GainsExperience.cs +++ b/OpenRA.Mods.Common/Traits/GainsExperience.cs @@ -18,7 +18,7 @@ using OpenRA.Traits; namespace OpenRA.Mods.Common.Traits { [Desc("This actor's experience increases when it has killed a GivesExperience actor.")] - public class GainsExperienceInfo : ITraitInfo, Requires + public class GainsExperienceInfo : ITraitInfo, Requires, Requires { [FieldLoader.LoadUsing("LoadUpgrades")] [Desc("Upgrades to grant at each level", @@ -29,6 +29,9 @@ namespace OpenRA.Mods.Common.Traits [Desc("Palette for the level up sprite.")] public readonly string LevelUpPalette = "effect"; + [Desc("Should the level-up animation be suppressed when actor is created?")] + public readonly bool SuppressLevelupAnimation = true; + public object Create(ActorInitializer init) { return new GainsExperience(init, this); } static object LoadUpgrades(MiniYaml y) @@ -56,6 +59,7 @@ namespace OpenRA.Mods.Common.Traits { readonly Actor self; readonly GainsExperienceInfo info; + readonly UpgradeManager um; readonly List> nextLevel = new List>(); @@ -77,18 +81,20 @@ namespace OpenRA.Mods.Common.Traits nextLevel.Add(Pair.New(kv.Key * cost, kv.Value)); if (init.Contains()) - GiveExperience(init.Get()); + GiveExperience(init.Get(), info.SuppressLevelupAnimation); + + um = self.Trait(); } public bool CanGainLevel { get { return Level < MaxLevel; } } - public void GiveLevels(int numLevels) + public void GiveLevels(int numLevels, bool silent = false) { var newLevel = Math.Min(Level + numLevels, MaxLevel); - GiveExperience(nextLevel[newLevel - 1].First - experience); + GiveExperience(nextLevel[newLevel - 1].First - experience, silent); } - public void GiveExperience(int amount) + public void GiveExperience(int amount, bool silent = false) { experience += amount; @@ -98,11 +104,12 @@ namespace OpenRA.Mods.Common.Traits Level++; - var um = self.TraitOrDefault(); - if (um != null) - foreach (var u in upgrades) - um.GrantUpgrade(self, u, this); + foreach (var u in upgrades) + um.GrantUpgrade(self, u, this); + } + if (!silent) + { 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.Common/Traits/ProduceableWithLevel.cs b/OpenRA.Mods.Common/Traits/ProduceableWithLevel.cs new file mode 100644 index 0000000000..31d380c97d --- /dev/null +++ b/OpenRA.Mods.Common/Traits/ProduceableWithLevel.cs @@ -0,0 +1,52 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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 OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Actors possessing this trait should define the GainsExperience trait. When the prerequisites are fulfilled, ", + "this trait grants a level-up to newly spawned actors. If additionally the actor's owning player defines the ProductionIconOverlay ", + "trait, the production queue icon renders with an overlay defined in that trait.")] + public class ProduceableWithLevelInfo : ITraitInfo, Requires + { + public readonly string[] Prerequisites = { }; + + [Desc("Number of levels to give to the actor on creation.")] + public readonly int InitialLevels = 1; + + [Desc("Should the level-up animation be suppressed when actor is created?")] + public readonly bool SuppressLevelupAnimation = true; + + public object Create(ActorInitializer init) { return new ProduceableWithLevel(init, this); } + } + + public class ProduceableWithLevel : INotifyCreated + { + readonly ProduceableWithLevelInfo info; + + public ProduceableWithLevel(ActorInitializer init, ProduceableWithLevelInfo info) + { + this.info = info; + } + + public void Created(Actor self) + { + if (!self.Owner.PlayerActor.Trait().HasPrerequisites(info.Prerequisites)) + return; + + var ge = self.Trait(); + if (!ge.CanGainLevel) + return; + + ge.GiveLevels(info.InitialLevels, info.SuppressLevelupAnimation); + } + } +} diff --git a/OpenRA.Mods.Common/Traits/Upgrades/UpgradableTrait.cs b/OpenRA.Mods.Common/Traits/Upgrades/UpgradableTrait.cs index 2aeab13948..b6d99b6026 100644 --- a/OpenRA.Mods.Common/Traits/Upgrades/UpgradableTrait.cs +++ b/OpenRA.Mods.Common/Traits/Upgrades/UpgradableTrait.cs @@ -40,7 +40,7 @@ namespace OpenRA.Mods.Common.Traits /// Abstract base for enabling and disabling trait using upgrades. /// Requires basing *Info on UpgradableTraitInfo and using base(info) constructor. /// Note that EnabledByUpgrade is not called at creation even if this starts as enabled. - /// , + /// public abstract class UpgradableTrait : IUpgradable, IDisabledTrait, ISync where InfoType : UpgradableTraitInfo { public readonly InfoType Info; diff --git a/OpenRA.Mods.Common/Traits/VeteranProductionIconOverlay.cs b/OpenRA.Mods.Common/Traits/VeteranProductionIconOverlay.cs new file mode 100644 index 0000000000..efdb46815f --- /dev/null +++ b/OpenRA.Mods.Common/Traits/VeteranProductionIconOverlay.cs @@ -0,0 +1,151 @@ +#region Copyright & License Information +/* + * Copyright 2007-2015 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.Collections.Generic; +using OpenRA.Graphics; +using OpenRA.Traits; + +namespace OpenRA.Mods.Common.Traits +{ + [Desc("Attach this to the player actor. When attached, enables all actors possessing the LevelupWhenCreated ", + "trait to have their production queue icons render with an overlay defined in this trait. ", + "The icon change occurs when LevelupWhenCreated.Prerequisites are met.")] + public class VeteranProductionIconOverlayInfo : ITraitInfo, Requires + { + [Desc("Image used for the overlay.")] + public readonly string Image = null; + + [Desc("Sequence used for the overlay (cannot be animated).")] + [SequenceReference("Image")] public readonly string Sequence = null; + + [Desc("Palette to render the sprite in. Reference the world actor's PaletteFrom* traits.")] + public readonly string Palette = "chrome"; + + [Desc("Point on the production icon's used as reference for offsetting the overlay. ", + "Possible values are any combination of Top, VCenter, Bottom and Left, HCenter, Right separated by a comma.")] + public readonly ReferencePoints ReferencePoint = ReferencePoints.Top | ReferencePoints.Left; + + [Desc("Pixel offset relative to the icon's reference point.")] + public readonly int2 Offset = int2.Zero; + + [Desc("Visual scale of the overlay.")] + public readonly float Scale = 1f; + + public object Create(ActorInitializer init) { return new VeteranProductionIconOverlay(init, this); } + } + + public class VeteranProductionIconOverlay : ITechTreeElement, IProductionIconOverlay + { + // HACK: TechTree doesn't associate Watcher.Key with the registering ITechTreeElement. + // So in a situation where multiple ITechTreeElements register Watchers with the same Key, + // and one removes its Watcher, all other ITechTreeElements' Watchers get removed too. + // This makes sure that the keys are unique with respect to the registering ITechTreeElement. + const string Prefix = "ProductionIconOverlay."; + + readonly Actor self; + readonly Sprite sprite; + readonly VeteranProductionIconOverlayInfo info; + + Dictionary overlayActive = new Dictionary(); + + public VeteranProductionIconOverlay(ActorInitializer init, VeteranProductionIconOverlayInfo info) + { + self = init.Self; + + var anim = new Animation(self.World, info.Image); + anim.Play(info.Sequence); + sprite = anim.Image; + + this.info = info; + + var ttc = self.Trait(); + + foreach (var a in self.World.Map.Rules.Actors.Values) + { + var uwc = a.Traits.GetOrDefault(); + if (uwc != null) + ttc.Add(MakeKey(a.Name), uwc.Prerequisites, 0, this); + } + } + + public Sprite Sprite() + { + return sprite; + } + + public string Palette() + { + return info.Palette; + } + + public float Scale() + { + return info.Scale; + } + + public float2 Offset(float2 iconSize) + { + float offsetX = 0, offsetY = 0; + switch (info.ReferencePoint & (ReferencePoints)3) + { + case ReferencePoints.Top: + offsetY = (-iconSize.Y + sprite.Size.Y) / 2; + break; + case ReferencePoints.VCenter: + break; + case ReferencePoints.Bottom: + offsetY = (iconSize.Y - sprite.Size.Y) / 2; + break; + } + + switch (info.ReferencePoint & (ReferencePoints)(3 << 2)) + { + case ReferencePoints.Left: + offsetX = (-iconSize.X + sprite.Size.X) / 2; + break; + case ReferencePoints.HCenter: + break; + case ReferencePoints.Right: + offsetX = (iconSize.X - sprite.Size.X) / 2; + break; + } + + return new float2(offsetX, offsetY) + info.Offset; + } + + public bool IsOverlayActive(ActorInfo ai) + { + bool isActive; + overlayActive.TryGetValue(ai, out isActive); + + return isActive; + } + + static string MakeKey(string name) + { + return Prefix + name; + } + + static string GetName(string key) + { + return key.Substring(Prefix.Length); + } + + public void PrerequisitesAvailable(string key) + { + var ai = self.World.Map.Rules.Actors[GetName(key)]; + overlayActive[ai] = true; + } + + public void PrerequisitesUnavailable(string key) { } + public void PrerequisitesItemHidden(string key) { } + public void PrerequisitesItemVisible(string key) { } + } +} diff --git a/OpenRA.Mods.Common/TraitsInterfaces.cs b/OpenRA.Mods.Common/TraitsInterfaces.cs index 5dd5863784..34e91a7963 100644 --- a/OpenRA.Mods.Common/TraitsInterfaces.cs +++ b/OpenRA.Mods.Common/TraitsInterfaces.cs @@ -73,6 +73,15 @@ namespace OpenRA.Mods.Common.Traits void PrerequisitesItemVisible(string key); } + public interface IProductionIconOverlay + { + Sprite Sprite(); + string Palette(); + float Scale(); + float2 Offset(float2 iconSize); + bool IsOverlayActive(ActorInfo ai); + } + public interface INotifyTransform { void BeforeTransform(Actor self); void OnTransform(Actor self); void AfterTransform(Actor toActor); } public interface IAcceptResources diff --git a/OpenRA.Mods.Common/Widgets/ObserverProductionIconsWidget.cs b/OpenRA.Mods.Common/Widgets/ObserverProductionIconsWidget.cs index e36069009d..a9fb0a2b6c 100644 --- a/OpenRA.Mods.Common/Widgets/ObserverProductionIconsWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ObserverProductionIconsWidget.cs @@ -79,6 +79,11 @@ namespace OpenRA.Mods.Common.Widgets var location = new float2(RenderBounds.Location) + new float2(queue.i * (IconWidth + IconSpacing), 0); WidgetUtils.DrawSHPCentered(icon.Image, location + 0.5f * iconSize, worldRenderer.Palette(bi.IconPalette), 0.5f); + var pio = queue.Trait.Actor.Owner.PlayerActor.TraitsImplementing().FirstOrDefault(); + if (pio != null && pio.IsOverlayActive(actor)) + WidgetUtils.DrawSHPCentered(pio.Sprite(), location + 0.5f * iconSize + pio.Offset(0.5f * iconSize), + worldRenderer.Palette(pio.Palette()), 0.5f * pio.Scale()); + var clock = clocks[queue.Trait]; clock.PlayFetchIndex("idle", () => current.TotalTime == 0 ? 0 : ((current.TotalTime - current.RemainingTime) diff --git a/OpenRA.Mods.Common/Widgets/ProductionPaletteWidget.cs b/OpenRA.Mods.Common/Widgets/ProductionPaletteWidget.cs index 133890129e..73fe20090c 100644 --- a/OpenRA.Mods.Common/Widgets/ProductionPaletteWidget.cs +++ b/OpenRA.Mods.Common/Widgets/ProductionPaletteWidget.cs @@ -351,11 +351,18 @@ namespace OpenRA.Mods.Common.Widgets var buildableItems = CurrentQueue.BuildableItems(); + var pio = currentQueue.Actor.Owner.PlayerActor.TraitsImplementing().FirstOrDefault(); + var pioOffset = pio != null ? pio.Offset(IconSize) : new float2(0, 0); + // Icons foreach (var icon in icons.Values) { WidgetUtils.DrawSHPCentered(icon.Sprite, icon.Pos + iconOffset, icon.Palette); + // Draw the ProductionIconOverlay's sprite + if (pio != null && pio.IsOverlayActive(icon.Actor)) + WidgetUtils.DrawSHPCentered(pio.Sprite(), icon.Pos + iconOffset + pioOffset, worldRenderer.Palette(pio.Palette()), pio.Scale()); + // Build progress if (icon.Queued.Count > 0) { diff --git a/mods/ra/bits/cameo-chevron.pal b/mods/ra/bits/cameo-chevron.pal new file mode 100644 index 0000000000..46b5d0aba6 Binary files /dev/null and b/mods/ra/bits/cameo-chevron.pal differ diff --git a/mods/ra/bits/cameo-chevron.shp b/mods/ra/bits/cameo-chevron.shp new file mode 100644 index 0000000000..7c327dffde Binary files /dev/null and b/mods/ra/bits/cameo-chevron.shp differ diff --git a/mods/ra/rules/palettes.yaml b/mods/ra/rules/palettes.yaml index fbdcc159e8..7f3fe09070 100644 --- a/mods/ra/rules/palettes.yaml +++ b/mods/ra/rules/palettes.yaml @@ -10,6 +10,10 @@ Filename: temperat.pal ShadowIndex: 3 AllowModifiers: false + PaletteFromFile@cameo-chevron: + Name: cameo-chevron + Filename: cameo-chevron.pal + AllowModifiers: false PaletteFromFile@effect: Name: effect Filename: temperat.pal diff --git a/mods/ra/rules/player.yaml b/mods/ra/rules/player.yaml index ce7607c5ee..40f4de7813 100644 --- a/mods/ra/rules/player.yaml +++ b/mods/ra/rules/player.yaml @@ -65,4 +65,9 @@ Player: Prerequisites: techlevel.infonly, techlevel.low, techlevel.medium, techlevel.unrestricted GlobalUpgradeManager: EnemyWatcher: + VeteranProductionIconOverlay: + Offset: 2, 2 + Image: cameo-chevron + Sequence: idle + Palette: cameo-chevron diff --git a/mods/ra/sequences/misc.yaml b/mods/ra/sequences/misc.yaml index 9b7795b908..772b4c6349 100644 --- a/mods/ra/sequences/misc.yaml +++ b/mods/ra/sequences/misc.yaml @@ -382,6 +382,11 @@ rank: rank: Length: * +cameo-chevron: + idle: + Length: * + BlendMode: Additive + atomic: up: atomicup Length: *