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: *