Rework decoration renderable traits:

- Removed implicit pip definitions and IPips interface.
  New decoration traits have been added to render them.
  Pip types are no longer hardcoded in OpenRA.Game.

- Decoration rendering is now managed by SelectionDecorations(Base),
  which allows us to remove assumptions about the selection box
  geometry from the decoration traits.

- RenderNameTag has been replaced by WithNameTagDecoration, which is
  an otherwise normal decoration trait.

- Unify the configuration and reduce duplication between traits.

- Removed hardcoded references to specific selection box renderables.

- Remove legacy cruft.
This commit is contained in:
Paul Chote
2020-03-09 19:56:31 +00:00
committed by atlimit8
parent 73a78eadb1
commit ac200f6173
31 changed files with 1377 additions and 686 deletions

View File

@@ -66,6 +66,11 @@ namespace OpenRA.Primitives
public int2 Location { get { return new int2(X, Y); } } public int2 Location { get { return new int2(X, Y); } }
public Size Size { get { return new Size(Width, Height); } } public Size Size { get { return new Size(Width, Height); } }
public int2 TopLeft { get { return Location; } }
public int2 TopRight { get { return new int2(X + Width, Y); } }
public int2 BottomLeft { get { return new int2(X, Y + Height); } }
public int2 BottomRight { get { return new int2(X + Width, Y + Height); } }
public bool Contains(int x, int y) public bool Contains(int x, int y)
{ {
return x >= Left && x < Right && y >= Top && y < Bottom; return x >= Left && x < Right && y >= Top && y < Bottom;

View File

@@ -66,9 +66,6 @@ namespace OpenRA.Traits
void Kill(Actor self, Actor attacker, BitSet<DamageType> damageTypes); void Kill(Actor self, Actor attacker, BitSet<DamageType> damageTypes);
} }
// depends on the order of pips in WorldRenderer.cs!
public enum PipType { Transparent, Green, Yellow, Red, Gray, Blue, Ammo, AmmoEmpty }
[Flags] [Flags]
public enum Stance public enum Stance
{ {
@@ -128,38 +125,6 @@ namespace OpenRA.Traits
public interface IMouseBoundsInfo : ITraitInfoInterface { } public interface IMouseBoundsInfo : ITraitInfoInterface { }
public interface IAutoMouseBounds { Rectangle AutoMouseoverBounds(Actor self, WorldRenderer wr); } public interface IAutoMouseBounds { Rectangle AutoMouseoverBounds(Actor self, WorldRenderer wr); }
// HACK: This provides a shim for legacy code until it can be rewritten
public interface IDecorationBounds { Rectangle DecorationBounds(Actor self, WorldRenderer wr); }
public interface IDecorationBoundsInfo : ITraitInfoInterface { }
public static class DecorationBoundsExtensions
{
public static Rectangle FirstNonEmptyBounds(this IEnumerable<IDecorationBounds> decorationBounds, Actor self, WorldRenderer wr)
{
// PERF: Avoid LINQ.
foreach (var decoration in decorationBounds)
{
var bounds = decoration.DecorationBounds(self, wr);
if (!bounds.IsEmpty)
return bounds;
}
return Rectangle.Empty;
}
public static Rectangle FirstNonEmptyBounds(this IDecorationBounds[] decorationBounds, Actor self, WorldRenderer wr)
{
// PERF: Avoid LINQ.
foreach (var decoration in decorationBounds)
{
var bounds = decoration.DecorationBounds(self, wr);
if (!bounds.IsEmpty)
return bounds;
}
return Rectangle.Empty;
}
}
public interface IIssueOrder public interface IIssueOrder
{ {
IEnumerable<IOrderTargeter> Orders { get; } IEnumerable<IOrderTargeter> Orders { get; }
@@ -296,12 +261,15 @@ namespace OpenRA.Traits
public interface ILoadsPalettes { void LoadPalettes(WorldRenderer wr); } public interface ILoadsPalettes { void LoadPalettes(WorldRenderer wr); }
public interface ILoadsPlayerPalettes { void LoadPlayerPalettes(WorldRenderer wr, string playerName, Color playerColor, bool replaceExisting); } public interface ILoadsPlayerPalettes { void LoadPlayerPalettes(WorldRenderer wr, string playerName, Color playerColor, bool replaceExisting); }
public interface IPaletteModifier { void AdjustPalette(IReadOnlyDictionary<string, MutablePalette> b); } public interface IPaletteModifier { void AdjustPalette(IReadOnlyDictionary<string, MutablePalette> b); }
public interface IPips { IEnumerable<PipType> GetPips(Actor self); }
[RequireExplicitImplementation] [RequireExplicitImplementation]
public interface ISelectionBar { float GetValue(); Color GetColor(); bool DisplayWhenEmpty { get; } } public interface ISelectionBar { float GetValue(); Color GetColor(); bool DisplayWhenEmpty { get; } }
public interface ISelectionDecorations { void DrawRollover(Actor self, WorldRenderer worldRenderer); } public interface ISelectionDecorations
{
IEnumerable<IRenderable> RenderRolloverAnnotations(Actor self, WorldRenderer worldRenderer);
IEnumerable<IRenderable> RenderSelectionAnnotations(Actor self, WorldRenderer worldRenderer, Color color);
}
public interface IMapPreviewSignatureInfo : ITraitInfoInterface public interface IMapPreviewSignatureInfo : ITraitInfoInterface
{ {

View File

@@ -179,8 +179,9 @@ namespace OpenRA.Mods.Cnc.Traits
{ {
if (unit.CanBeViewedByPlayer(manager.Self.Owner)) if (unit.CanBeViewedByPlayer(manager.Self.Owner))
{ {
var bounds = unit.TraitsImplementing<IDecorationBounds>().FirstNonEmptyBounds(unit, wr); var decorations = unit.TraitsImplementing<ISelectionDecorations>().FirstEnabledTraitOrDefault();
yield return new SelectionBoxAnnotationRenderable(unit, bounds, Color.Red); foreach (var d in decorations.RenderSelectionAnnotations(unit, wr, Color.Red))
yield return d;
} }
} }
} }
@@ -302,8 +303,9 @@ namespace OpenRA.Mods.Cnc.Traits
{ {
if (unit.CanBeViewedByPlayer(manager.Self.Owner)) if (unit.CanBeViewedByPlayer(manager.Self.Owner))
{ {
var bounds = unit.TraitsImplementing<IDecorationBounds>().FirstNonEmptyBounds(unit, wr); var decorations = unit.TraitsImplementing<ISelectionDecorations>().FirstEnabledTraitOrDefault();
yield return new SelectionBoxAnnotationRenderable(unit, bounds, Color.Red); foreach (var d in decorations.RenderSelectionAnnotations(unit, wr, Color.Red))
yield return d;
} }
} }
} }

View File

@@ -0,0 +1,72 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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 OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Graphics
{
public struct UITextRenderable : IRenderable, IFinalizedRenderable
{
readonly SpriteFont font;
readonly WPos effectiveWorldPos;
readonly int2 screenPos;
readonly int zOffset;
readonly Color color;
readonly Color bgDark;
readonly Color bgLight;
readonly string text;
public UITextRenderable(SpriteFont font, WPos effectiveWorldPos, int2 screenPos, int zOffset, Color color, Color bgDark, Color bgLight, string text)
{
this.font = font;
this.effectiveWorldPos = effectiveWorldPos;
this.screenPos = screenPos;
this.zOffset = zOffset;
this.color = color;
this.bgDark = bgDark;
this.bgLight = bgLight;
this.text = text;
}
public UITextRenderable(SpriteFont font, WPos effectiveWorldPos, int2 screenPos, int zOffset, Color color, string text)
: this(font, effectiveWorldPos, screenPos, zOffset, color,
ChromeMetrics.Get<Color>("TextContrastColorDark"),
ChromeMetrics.Get<Color>("TextContrastColorLight"),
text) { }
public WPos Pos { get { return effectiveWorldPos; } }
public PaletteReference Palette { get { return null; } }
public int ZOffset { get { return zOffset; } }
public bool IsDecoration { get { return true; } }
public IRenderable WithPalette(PaletteReference newPalette) { return new UITextRenderable(font, effectiveWorldPos, screenPos, zOffset, color, text); }
public IRenderable WithZOffset(int newOffset) { return new UITextRenderable(font, effectiveWorldPos, screenPos, zOffset, color, text); }
public IRenderable OffsetBy(WVec vec) { return new UITextRenderable(font, effectiveWorldPos + vec, screenPos, zOffset, color, text); }
public IRenderable AsDecoration() { return this; }
public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
public void Render(WorldRenderer wr)
{
font.DrawTextWithContrast(text, screenPos, color, bgDark, bgLight, 1);
}
public void RenderDebugGeometry(WorldRenderer wr)
{
var size = font.Measure(text).ToFloat2();
Game.Renderer.RgbaColorRenderer.DrawRect(screenPos - 0.5f * size, screenPos + 0.5f * size, 1, Color.Red);
}
public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; }
}
}

View File

@@ -30,15 +30,6 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Initial ammo the actor is created with. Defaults to Ammo.")] [Desc("Initial ammo the actor is created with. Defaults to Ammo.")]
public readonly int InitialAmmo = -1; public readonly int InitialAmmo = -1;
[Desc("Defaults to value in Ammo. 0 means no visible pips.")]
public readonly int PipCount = -1;
[Desc("PipType to use for loaded ammo.")]
public readonly PipType PipType = PipType.Green;
[Desc("PipType to use for empty ammo.")]
public readonly PipType PipTypeEmpty = PipType.Transparent;
[Desc("How much ammo is reloaded after a certain period.")] [Desc("How much ammo is reloaded after a certain period.")]
public readonly int ReloadCount = 1; public readonly int ReloadCount = 1;
@@ -56,7 +47,7 @@ namespace OpenRA.Mods.Common.Traits
public object Create(ActorInitializer init) { return new AmmoPool(init.Self, this); } public object Create(ActorInitializer init) { return new AmmoPool(init.Self, this); }
} }
public class AmmoPool : INotifyCreated, INotifyAttack, IPips, ISync public class AmmoPool : INotifyCreated, INotifyAttack, ISync
{ {
public readonly AmmoPoolInfo Info; public readonly AmmoPoolInfo Info;
readonly Stack<int> tokens = new Stack<int>(); readonly Stack<int> tokens = new Stack<int>();
@@ -126,14 +117,5 @@ namespace OpenRA.Mods.Common.Traits
while (CurrentAmmoCount < tokens.Count && tokens.Count > 0) while (CurrentAmmoCount < tokens.Count && tokens.Count > 0)
conditionManager.RevokeCondition(self, tokens.Pop()); conditionManager.RevokeCondition(self, tokens.Pop());
} }
public IEnumerable<PipType> GetPips(Actor self)
{
var pips = Info.PipCount >= 0 ? Info.PipCount : Info.Ammo;
return Enumerable.Range(0, pips).Select(i =>
(CurrentAmmoCount * pips) / Info.Ammo > i ?
Info.PipType : Info.PipTypeEmpty);
}
} }
} }

View File

@@ -26,9 +26,6 @@ namespace OpenRA.Mods.Common.Traits
[Desc("The maximum sum of Passenger.Weight that this actor can support.")] [Desc("The maximum sum of Passenger.Weight that this actor can support.")]
public readonly int MaxWeight = 0; public readonly int MaxWeight = 0;
[Desc("Number of pips to display when this actor is selected.")]
public readonly int PipCount = 0;
[Desc("`Passenger.CargoType`s that can be loaded into this actor.")] [Desc("`Passenger.CargoType`s that can be loaded into this actor.")]
public readonly HashSet<string> Types = new HashSet<string>(); public readonly HashSet<string> Types = new HashSet<string>();
@@ -88,7 +85,7 @@ namespace OpenRA.Mods.Common.Traits
public object Create(ActorInitializer init) { return new Cargo(init, this); } public object Create(ActorInitializer init) { return new Cargo(init, this); }
} }
public class Cargo : IPips, IIssueOrder, IResolveOrder, IOrderVoice, INotifyCreated, INotifyKilled, public class Cargo : IIssueOrder, IResolveOrder, IOrderVoice, INotifyCreated, INotifyKilled,
INotifyOwnerChanged, INotifySold, INotifyActorDisposing, IIssueDeployOrder, INotifyOwnerChanged, INotifySold, INotifyActorDisposing, IIssueDeployOrder,
ITransformActorInitModifier ITransformActorInitModifier
{ {
@@ -379,30 +376,6 @@ namespace OpenRA.Mods.Common.Traits
t.TurretFacing = facing.Value.Facing + Info.PassengerFacing; t.TurretFacing = facing.Value.Facing + Info.PassengerFacing;
} }
public IEnumerable<PipType> GetPips(Actor self)
{
var numPips = Info.PipCount;
for (var i = 0; i < numPips; i++)
yield return GetPipAt(i);
}
PipType GetPipAt(int i)
{
var n = i * Info.MaxWeight / Info.PipCount;
foreach (var c in cargo)
{
var pi = c.Info.TraitInfo<PassengerInfo>();
if (n < pi.Weight)
return pi.PipType;
else
n -= pi.Weight;
}
return PipType.Transparent;
}
public void Load(Actor self, Actor a) public void Load(Actor self, Actor a)
{ {
cargo.Add(a); cargo.Add(a);

View File

@@ -42,9 +42,6 @@ namespace OpenRA.Mods.Common.Traits
[Desc("How many bales can it dump at once.")] [Desc("How many bales can it dump at once.")]
public readonly int BaleUnloadAmount = 1; public readonly int BaleUnloadAmount = 1;
[Desc("How many squares to show the fill level.")]
public readonly int PipCount = 7;
public readonly int HarvestFacings = 0; public readonly int HarvestFacings = 0;
[Desc("Which resources it can harvest.")] [Desc("Which resources it can harvest.")]
@@ -90,15 +87,16 @@ namespace OpenRA.Mods.Common.Traits
public object Create(ActorInitializer init) { return new Harvester(init.Self, this); } public object Create(ActorInitializer init) { return new Harvester(init.Self, this); }
} }
public class Harvester : IIssueOrder, IResolveOrder, IPips, IOrderVoice, public class Harvester : IIssueOrder, IResolveOrder, IOrderVoice,
ISpeedModifier, ISync, INotifyCreated ISpeedModifier, ISync, INotifyCreated
{ {
public readonly HarvesterInfo Info; public readonly HarvesterInfo Info;
public readonly IReadOnlyDictionary<ResourceTypeInfo, int> Contents;
readonly Mobile mobile; readonly Mobile mobile;
readonly ResourceLayer resLayer; readonly ResourceLayer resLayer;
readonly ResourceClaimLayer claimLayer; readonly ResourceClaimLayer claimLayer;
readonly Dictionary<ResourceTypeInfo, int> contents = new Dictionary<ResourceTypeInfo, int>(); readonly Dictionary<ResourceTypeInfo, int> contents = new Dictionary<ResourceTypeInfo, int>();
INotifyHarvesterAction[] notifyHarvesterAction;
ConditionManager conditionManager; ConditionManager conditionManager;
int conditionToken = ConditionManager.InvalidConditionToken; int conditionToken = ConditionManager.InvalidConditionToken;
HarvesterResourceMultiplier[] resourceMultipliers; HarvesterResourceMultiplier[] resourceMultipliers;
@@ -127,6 +125,8 @@ namespace OpenRA.Mods.Common.Traits
public Harvester(Actor self, HarvesterInfo info) public Harvester(Actor self, HarvesterInfo info)
{ {
Info = info; Info = info;
Contents = new ReadOnlyDictionary<ResourceTypeInfo, int>(contents);
mobile = self.Trait<Mobile>(); mobile = self.Trait<Mobile>();
resLayer = self.World.WorldActor.Trait<ResourceLayer>(); resLayer = self.World.WorldActor.Trait<ResourceLayer>();
claimLayer = self.World.WorldActor.Trait<ResourceClaimLayer>(); claimLayer = self.World.WorldActor.Trait<ResourceClaimLayer>();
@@ -134,7 +134,6 @@ namespace OpenRA.Mods.Common.Traits
void INotifyCreated.Created(Actor self) void INotifyCreated.Created(Actor self)
{ {
notifyHarvesterAction = self.TraitsImplementing<INotifyHarvesterAction>().ToArray();
resourceMultipliers = self.TraitsImplementing<HarvesterResourceMultiplier>().ToArray(); resourceMultipliers = self.TraitsImplementing<HarvesterResourceMultiplier>().ToArray();
conditionManager = self.TraitOrDefault<ConditionManager>(); conditionManager = self.TraitOrDefault<ConditionManager>();
UpdateCondition(self); UpdateCondition(self);
@@ -348,27 +347,6 @@ namespace OpenRA.Mods.Common.Traits
} }
} }
PipType GetPipAt(int i)
{
var n = i * Info.Capacity / Info.PipCount;
foreach (var rt in contents)
if (n < rt.Value)
return rt.Key.PipColor;
else
n -= rt.Value;
return PipType.Transparent;
}
IEnumerable<PipType> IPips.GetPips(Actor self)
{
var numPips = Info.PipCount;
for (var i = 0; i < numPips; i++)
yield return GetPipAt(i);
}
int ISpeedModifier.GetSpeedModifier() int ISpeedModifier.GetSpeedModifier()
{ {
return 100 - (100 - Info.FullyLoadedSpeed) * contents.Values.Sum() / Info.Capacity; return 100 - (100 - Info.FullyLoadedSpeed) * contents.Values.Sum() / Info.Capacity;

View File

@@ -17,7 +17,7 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits namespace OpenRA.Mods.Common.Traits
{ {
[Desc("Used to enable mouse interaction on actors that are not Selectable.")] [Desc("Used to enable mouse interaction on actors that are not Selectable.")]
public class InteractableInfo : ITraitInfo, IMouseBoundsInfo, IDecorationBoundsInfo public class InteractableInfo : ITraitInfo, IMouseBoundsInfo
{ {
[Desc("Defines a custom rectangle for mouse interaction with the actor.", [Desc("Defines a custom rectangle for mouse interaction with the actor.",
"If null, the engine will guess an appropriate size based on the With*Body trait.", "If null, the engine will guess an appropriate size based on the With*Body trait.",
@@ -32,7 +32,7 @@ namespace OpenRA.Mods.Common.Traits
public virtual object Create(ActorInitializer init) { return new Interactable(this); } public virtual object Create(ActorInitializer init) { return new Interactable(this); }
} }
public class Interactable : INotifyCreated, IMouseBounds, IDecorationBounds public class Interactable : INotifyCreated, IMouseBounds
{ {
readonly InteractableInfo info; readonly InteractableInfo info;
IAutoMouseBounds[] autoBounds; IAutoMouseBounds[] autoBounds;
@@ -72,7 +72,7 @@ namespace OpenRA.Mods.Common.Traits
return Bounds(self, wr, info.Bounds); return Bounds(self, wr, info.Bounds);
} }
Rectangle IDecorationBounds.DecorationBounds(Actor self, WorldRenderer wr) public Rectangle DecorationBounds(Actor self, WorldRenderer wr)
{ {
return Bounds(self, wr, info.DecorationBounds ?? info.Bounds); return Bounds(self, wr, info.DecorationBounds ?? info.Bounds);
} }

View File

@@ -23,7 +23,10 @@ namespace OpenRA.Mods.Common.Traits
public class PassengerInfo : ITraitInfo, IObservesVariablesInfo public class PassengerInfo : ITraitInfo, IObservesVariablesInfo
{ {
public readonly string CargoType = null; public readonly string CargoType = null;
public readonly PipType PipType = PipType.Green;
[Desc("If defined, use a custom pip type defined on the transport's WithCargoPipsDecoration.CustomPipSequences list.")]
public readonly string CustomPipType = null;
public readonly int Weight = 1; public readonly int Weight = 1;
[GrantedConditionReference] [GrantedConditionReference]

View File

@@ -1,65 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render
{
[Desc("Displays the player name above the unit")]
class RenderNameTagInfo : ITraitInfo, Requires<IDecorationBoundsInfo>
{
public readonly int MaxLength = 10;
public readonly string Font = "TinyBold";
public object Create(ActorInitializer init) { return new RenderNameTag(init.Self, this); }
}
class RenderNameTag : IRenderAnnotations
{
readonly SpriteFont font;
readonly Color color;
readonly string name;
readonly IDecorationBounds[] decorationBounds;
public RenderNameTag(Actor self, RenderNameTagInfo info)
{
font = Game.Renderer.Fonts[info.Font];
color = self.Owner.Color;
if (self.Owner.PlayerName.Length > info.MaxLength)
name = self.Owner.PlayerName.Substring(0, info.MaxLength);
else
name = self.Owner.PlayerName;
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
}
IEnumerable<IRenderable> IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr)
{
if (self.World.FogObscures(self))
yield break;
var bounds = decorationBounds.FirstNonEmptyBounds(self, wr);
var spaceBuffer = (int)(10 / wr.Viewport.Zoom);
var effectPos = wr.ProjectedPosition(new int2((bounds.Left + bounds.Right) / 2, bounds.Y - spaceBuffer));
yield return new TextAnnotationRenderable(font, effectPos, 0, color, name);
}
bool IRenderAnnotations.SpatiallyPartitionable { get { return false; } }
}
}

View File

@@ -18,160 +18,49 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render namespace OpenRA.Mods.Common.Traits.Render
{ {
public class SelectionDecorationsInfo : ITraitInfo, Requires<IDecorationBoundsInfo> public class SelectionDecorationsInfo : SelectionDecorationsBaseInfo, Requires<InteractableInfo>
{ {
[PaletteReference] public override object Create(ActorInitializer init) { return new SelectionDecorations(init.Self, this); }
public readonly string Palette = "chrome";
[Desc("Health bar, production progress bar etc.")]
public readonly bool RenderSelectionBars = true;
public readonly bool RenderSelectionBox = true;
public readonly Color SelectionBoxColor = Color.White;
public readonly string Image = "pips";
public object Create(ActorInitializer init) { return new SelectionDecorations(init.Self, this); }
} }
public class SelectionDecorations : ISelectionDecorations, IRenderAnnotations, INotifyCreated, ITick public class SelectionDecorations : SelectionDecorationsBase
{ {
// depends on the order of pips in TraitsInterfaces.cs! readonly Interactable interactable;
static readonly string[] PipStrings = { "pip-empty", "pip-green", "pip-yellow", "pip-red", "pip-gray", "pip-blue", "pip-ammo", "pip-ammoempty" };
public readonly SelectionDecorationsInfo Info;
readonly IDecorationBounds[] decorationBounds;
readonly Animation pipImages;
IPips[] pipSources;
public SelectionDecorations(Actor self, SelectionDecorationsInfo info) public SelectionDecorations(Actor self, SelectionDecorationsInfo info)
: base(info)
{ {
Info = info; interactable = self.Trait<Interactable>();
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
pipImages = new Animation(self.World, Info.Image);
} }
void INotifyCreated.Created(Actor self) protected override int2 GetDecorationPosition(Actor self, WorldRenderer wr, DecorationPosition pos)
{ {
pipSources = self.TraitsImplementing<IPips>().ToArray(); var bounds = interactable.DecorationBounds(self, wr);
} switch (pos)
IEnumerable<WPos> ActivityTargetPath(Actor self)
{
if (!self.IsInWorld || self.IsDead)
yield break;
var activity = self.CurrentActivity;
if (activity != null)
{ {
var targets = activity.GetTargets(self); case DecorationPosition.TopLeft: return bounds.TopLeft;
yield return self.CenterPosition; case DecorationPosition.TopRight: return bounds.TopRight;
case DecorationPosition.BottomLeft: return bounds.BottomLeft;
foreach (var t in targets.Where(t => t.Type != TargetType.Invalid)) case DecorationPosition.BottomRight: return bounds.BottomRight;
yield return t.CenterPosition; case DecorationPosition.Top: return new int2(bounds.Left + bounds.Size.Width / 2, bounds.Top);
default: return bounds.TopLeft + new int2(bounds.Size.Width / 2, bounds.Size.Height / 2);
} }
} }
IEnumerable<IRenderable> IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr) protected override IEnumerable<IRenderable> RenderSelectionBox(Actor self, WorldRenderer wr, Color color)
{ {
if (self.World.FogObscures(self)) var bounds = interactable.DecorationBounds(self, wr);
return Enumerable.Empty<IRenderable>(); yield return new SelectionBoxAnnotationRenderable(self, bounds, color);
return DrawDecorations(self, wr);
} }
bool IRenderAnnotations.SpatiallyPartitionable { get { return true; } } protected override IEnumerable<IRenderable> RenderSelectionBars(Actor self, WorldRenderer wr, bool displayHealth, bool displayExtra)
IEnumerable<IRenderable> DrawDecorations(Actor self, WorldRenderer wr)
{ {
var selected = self.World.Selection.Contains(self); // Don't render the selection bars for non-selectable actors
var regularWorld = self.World.Type == WorldType.Regular; if (!(interactable is Selectable) || (!displayHealth && !displayExtra))
var statusBars = Game.Settings.Game.StatusBars;
var bounds = decorationBounds.FirstNonEmptyBounds(self, wr);
// Health bars are shown when:
// * actor is selected
// * status bar preference is set to "always show"
// * status bar preference is set to "when damaged" and actor is damaged
var displayHealth = selected || (regularWorld && statusBars == StatusBarsType.AlwaysShow)
|| (regularWorld && statusBars == StatusBarsType.DamageShow && self.GetDamageState() != DamageState.Undamaged);
// Extra bars are shown when:
// * actor is selected
// * status bar preference is set to "always show"
// * status bar preference is set to "when damaged"
var displayExtra = selected || (regularWorld && statusBars != StatusBarsType.Standard);
if (Info.RenderSelectionBox && selected)
yield return new SelectionBoxAnnotationRenderable(self, bounds, Info.SelectionBoxColor);
if (Info.RenderSelectionBars && (displayHealth || displayExtra))
yield return new SelectionBarsAnnotationRenderable(self, bounds, displayHealth, displayExtra);
// Target lines and pips are always only displayed for selected allied actors
if (!selected || !self.Owner.IsAlliedWith(wr.World.RenderPlayer))
yield break; yield break;
if (self.World.LocalPlayer != null && self.World.LocalPlayer.PlayerActor.Trait<DeveloperMode>().PathDebug) var bounds = interactable.DecorationBounds(self, wr);
yield return new TargetLineRenderable(ActivityTargetPath(self), Color.Green); yield return new SelectionBarsAnnotationRenderable(self, bounds, displayHealth, displayExtra);
foreach (var r in DrawPips(self, bounds, wr))
yield return r;
}
public void DrawRollover(Actor self, WorldRenderer worldRenderer)
{
var bounds = decorationBounds.FirstNonEmptyBounds(self, worldRenderer);
new SelectionBarsAnnotationRenderable(self, bounds, true, true).Render(worldRenderer);
}
IEnumerable<IRenderable> DrawPips(Actor self, Rectangle bounds, WorldRenderer wr)
{
if (pipSources.Length == 0)
return Enumerable.Empty<IRenderable>();
return DrawPipsInner(self, bounds, wr);
}
IEnumerable<IRenderable> DrawPipsInner(Actor self, Rectangle bounds, WorldRenderer wr)
{
pipImages.PlayRepeating(PipStrings[0]);
var palette = wr.Palette(Info.Palette);
var basePosition = wr.Viewport.WorldToViewPx(new int2(bounds.Left, bounds.Bottom));
var pipSize = pipImages.Image.Size.XY.ToInt2();
var pipxyBase = basePosition + new int2(1 - pipSize.X / 2, -(3 + pipSize.Y / 2));
var pipxyOffset = new int2(0, 0);
var width = bounds.Width;
foreach (var pips in pipSources)
{
var thisRow = pips.GetPips(self);
if (thisRow == null)
continue;
foreach (var pip in thisRow)
{
if (pipxyOffset.X + pipSize.X >= width)
pipxyOffset = new int2(0, pipxyOffset.Y - pipSize.Y);
pipImages.PlayRepeating(PipStrings[(int)pip]);
pipxyOffset += new int2(pipSize.X, 0);
yield return new UISpriteRenderable(pipImages.Image, self.CenterPosition, pipxyBase + pipxyOffset, 0, palette, 1f);
}
// Increment row
pipxyOffset = new int2(0, pipxyOffset.Y - (pipSize.Y + 1));
}
}
void ITick.Tick(Actor self)
{
pipImages.Tick();
} }
} }
} }

View File

@@ -0,0 +1,145 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render
{
public abstract class SelectionDecorationsBaseInfo : ITraitInfo
{
public readonly Color SelectionBoxColor = Color.White;
public abstract object Create(ActorInitializer init);
}
public abstract class SelectionDecorationsBase : ISelectionDecorations, IRenderAnnotations, INotifyCreated
{
Dictionary<DecorationPosition, IDecoration[]> decorations;
Dictionary<DecorationPosition, IDecoration[]> selectedDecorations;
protected readonly SelectionDecorationsBaseInfo info;
public SelectionDecorationsBase(SelectionDecorationsBaseInfo info)
{
this.info = info;
}
void INotifyCreated.Created(Actor self)
{
var groupedDecorations = new Dictionary<DecorationPosition, List<IDecoration>>();
var groupedSelectionDecorations = new Dictionary<DecorationPosition, List<IDecoration>>();
foreach (var d in self.TraitsImplementing<IDecoration>())
{
groupedSelectionDecorations.GetOrAdd(d.Position).Add(d);
if (!d.RequiresSelection)
groupedDecorations.GetOrAdd(d.Position).Add(d);
}
decorations = groupedDecorations.ToDictionary(
d => d.Key,
d => d.Value.ToArray());
selectedDecorations = groupedSelectionDecorations.ToDictionary(
d => d.Key,
d => d.Value.ToArray());
}
IEnumerable<WPos> ActivityTargetPath(Actor self)
{
if (!self.IsInWorld || self.IsDead)
yield break;
var activity = self.CurrentActivity;
if (activity != null)
{
var targets = activity.GetTargets(self);
yield return self.CenterPosition;
foreach (var t in targets.Where(t => t.Type != TargetType.Invalid))
yield return t.CenterPosition;
}
}
IEnumerable<IRenderable> IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr)
{
if (self.World.FogObscures(self))
return Enumerable.Empty<IRenderable>();
return DrawDecorations(self, wr);
}
bool IRenderAnnotations.SpatiallyPartitionable { get { return true; } }
IEnumerable<IRenderable> DrawDecorations(Actor self, WorldRenderer wr)
{
var selected = self.World.Selection.Contains(self);
var regularWorld = self.World.Type == WorldType.Regular;
var statusBars = Game.Settings.Game.StatusBars;
// Health bars are shown when:
// * actor is selected
// * status bar preference is set to "always show"
// * status bar preference is set to "when damaged" and actor is damaged
var displayHealth = selected || (regularWorld && statusBars == StatusBarsType.AlwaysShow)
|| (regularWorld && statusBars == StatusBarsType.DamageShow && self.GetDamageState() != DamageState.Undamaged);
// Extra bars are shown when:
// * actor is selected
// * status bar preference is set to "always show" or "when damaged"
var displayExtra = selected || (regularWorld && statusBars != StatusBarsType.Standard);
if (selected)
foreach (var r in RenderSelectionBox(self, wr, info.SelectionBoxColor))
yield return r;
if (displayHealth || displayExtra)
foreach (var r in RenderSelectionBars(self, wr, displayHealth, displayExtra))
yield return r;
var renderDecorations = self.World.Selection.Contains(self) ? selectedDecorations : decorations;
foreach (var kv in renderDecorations)
{
var pos = GetDecorationPosition(self, wr, kv.Key);
foreach (var r in kv.Value)
foreach (var rr in r.RenderDecoration(self, wr, pos))
yield return rr;
}
// Target lines and pips are always only displayed for selected allied actors
if (!selected || !self.Owner.IsAlliedWith(wr.World.RenderPlayer))
yield break;
if (self.World.LocalPlayer != null && self.World.LocalPlayer.PlayerActor.Trait<DeveloperMode>().PathDebug)
yield return new TargetLineRenderable(ActivityTargetPath(self), Color.Green);
}
IEnumerable<IRenderable> ISelectionDecorations.RenderRolloverAnnotations(Actor self, WorldRenderer worldRenderer)
{
if (self.World.Selection.Contains(self))
return Enumerable.Empty<IRenderable>();
return RenderSelectionBars(self, worldRenderer, true, true);
}
IEnumerable<IRenderable> ISelectionDecorations.RenderSelectionAnnotations(Actor self, WorldRenderer worldRenderer, Color color)
{
return RenderSelectionBox(self, worldRenderer, color);
}
protected abstract int2 GetDecorationPosition(Actor self, WorldRenderer wr, DecorationPosition pos);
protected abstract IEnumerable<IRenderable> RenderSelectionBox(Actor self, WorldRenderer wr, Color color);
protected abstract IEnumerable<IRenderable> RenderSelectionBars(Actor self, WorldRenderer wr, bool displayHealth, bool displayExtra);
}
}

View File

@@ -32,10 +32,6 @@ namespace OpenRA.Mods.Common.Traits.Render
[Desc("Palette to render the sprite in. Reference the world actor's PaletteFrom* traits.")] [Desc("Palette to render the sprite in. Reference the world actor's PaletteFrom* traits.")]
public readonly string Palette = "chrome"; public readonly string Palette = "chrome";
[Desc("Point on the production icon's used as reference for offsetting the overlay. ",
"Possible values are combinations of Center, Top, Bottom, Left, Right.")]
public readonly ReferencePoints ReferencePoint = ReferencePoints.Top | ReferencePoints.Left;
public object Create(ActorInitializer init) { return new VeteranProductionIconOverlay(init, this); } public object Create(ActorInitializer init) { return new VeteranProductionIconOverlay(init, this); }
} }
@@ -77,18 +73,8 @@ namespace OpenRA.Mods.Common.Traits.Render
string IProductionIconOverlay.Palette { get { return info.Palette; } } string IProductionIconOverlay.Palette { get { return info.Palette; } }
float2 IProductionIconOverlay.Offset(float2 iconSize) float2 IProductionIconOverlay.Offset(float2 iconSize)
{ {
float x = 0; var x = (sprite.Size.X - iconSize.X) / 2;
float y = 0; var y = (sprite.Size.Y - iconSize.Y) / 2;
if (info.ReferencePoint.HasFlag(ReferencePoints.Top))
y -= iconSize.Y / 2 - sprite.Size.Y / 2;
else if (info.ReferencePoint.HasFlag(ReferencePoints.Bottom))
y += iconSize.Y / 2 - sprite.Size.Y / 2;
if (info.ReferencePoint.HasFlag(ReferencePoints.Left))
x -= iconSize.X / 2 - sprite.Size.X / 2;
else if (info.ReferencePoint.HasFlag(ReferencePoints.Right))
x += iconSize.X / 2 - sprite.Size.X / 2;
return new float2(x, y); return new float2(x, y);
} }

View File

@@ -0,0 +1,83 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render
{
public class WithAmmoPipsDecorationInfo : WithDecorationBaseInfo, Requires<AmmoPoolInfo>
{
[Desc("Number of pips to display. Defaults to the sum of the enabled AmmoPool.Ammo.")]
public readonly int PipCount = -1;
[Desc("If non-zero, override the spacing between adjacent pips.")]
public readonly int2 PipStride = int2.Zero;
[Desc("Image that defines the pip sequences.")]
public readonly string Image = "pips";
[SequenceReference("Image")]
[Desc("Sequence used for empty pips.")]
public readonly string EmptySequence = "pip-empty";
[SequenceReference("Image")]
[Desc("Sequence used for full pips.")]
public readonly string FullSequence = "pip-green";
[PaletteReference]
public readonly string Palette = "chrome";
public override object Create(ActorInitializer init) { return new WithAmmoPipsDecoration(init.Self, this); }
}
public class WithAmmoPipsDecoration : WithDecorationBase<WithAmmoPipsDecorationInfo>
{
readonly AmmoPool[] ammo;
readonly Animation pips;
public WithAmmoPipsDecoration(Actor self, WithAmmoPipsDecorationInfo info)
: base(self, info)
{
ammo = self.TraitsImplementing<AmmoPool>().ToArray();
pips = new Animation(self.World, info.Image);
}
protected override IEnumerable<IRenderable> RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos)
{
pips.PlayRepeating(Info.EmptySequence);
var palette = wr.Palette(Info.Palette);
var pipSize = pips.Image.Size.XY.ToInt2();
var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0);
screenPos -= pipSize / 2;
var currentAmmo = 0;
var totalAmmo = 0;
foreach (var a in ammo)
{
currentAmmo += a.CurrentAmmoCount;
totalAmmo += a.Info.Ammo;
}
var pipCount = Info.PipCount > 0 ? Info.PipCount : totalAmmo;
for (var i = 0; i < pipCount; i++)
{
pips.PlayRepeating(currentAmmo * pipCount > i * totalAmmo ? Info.FullSequence : Info.EmptySequence);
yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f);
screenPos += pipStride;
}
}
}
}

View File

@@ -0,0 +1,101 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Collections.Generic;
using OpenRA.Graphics;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render
{
public class WithCargoPipsDecorationInfo : WithDecorationBaseInfo, Requires<CargoInfo>
{
[Desc("Number of pips to display. Defaults to Cargo.MaxWeight.")]
public readonly int PipCount = -1;
[Desc("If non-zero, override the spacing between adjacent pips.")]
public readonly int2 PipStride = int2.Zero;
[Desc("Image that defines the pip sequences.")]
public readonly string Image = "pips";
[SequenceReference("Image")]
[Desc("Sequence used for empty pips.")]
public readonly string EmptySequence = "pip-empty";
[SequenceReference("Image")]
[Desc("Sequence used for full pips that aren't defined in CustomPipSequences.")]
public readonly string FullSequence = "pip-green";
// TODO: [SequenceReference] isn't smart enough to use Dictionaries.
[Desc("Pip sequence to use for specific passenger actors.")]
public readonly Dictionary<string, string> CustomPipSequences = new Dictionary<string, string>();
[PaletteReference]
public readonly string Palette = "chrome";
public override object Create(ActorInitializer init) { return new WithCargoPipsDecoration(init.Self, this); }
}
public class WithCargoPipsDecoration : WithDecorationBase<WithCargoPipsDecorationInfo>
{
readonly Cargo cargo;
readonly Animation pips;
readonly int pipCount;
public WithCargoPipsDecoration(Actor self, WithCargoPipsDecorationInfo info)
: base(self, info)
{
cargo = self.Trait<Cargo>();
pipCount = info.PipCount > 0 ? info.PipCount : cargo.Info.MaxWeight;
pips = new Animation(self.World, info.Image);
}
string GetPipSequence(int i)
{
var n = i * cargo.Info.MaxWeight / pipCount;
foreach (var c in cargo.Passengers)
{
var pi = c.Info.TraitInfo<PassengerInfo>();
if (n < pi.Weight)
{
var sequence = Info.FullSequence;
if (pi.CustomPipType != null && !Info.CustomPipSequences.TryGetValue(pi.CustomPipType, out sequence))
Log.Write("debug", "Actor type {0} defines a custom pip type {1} that is not defined for actor type {2}".F(c.Info.Name, pi.CustomPipType, self.Info.Name));
return sequence;
}
n -= pi.Weight;
}
return Info.EmptySequence;
}
protected override IEnumerable<IRenderable> RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos)
{
pips.PlayRepeating(Info.EmptySequence);
var palette = wr.Palette(Info.Palette);
var pipSize = pips.Image.Size.XY.ToInt2();
var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0);
screenPos -= pipSize / 2;
for (var i = 0; i < pipCount; i++)
{
pips.PlayRepeating(GetPipSequence(i));
yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f);
screenPos += pipStride;
}
}
}
}

View File

@@ -18,24 +18,15 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render namespace OpenRA.Mods.Common.Traits.Render
{ {
[Flags]
public enum ReferencePoints
{
Center = 0,
Top = 1,
Bottom = 2,
Left = 4,
Right = 8,
}
public enum BlinkState { Off, On }
[Desc("Displays a custom UI overlay relative to the actor's mouseover bounds.")] [Desc("Displays a custom UI overlay relative to the actor's mouseover bounds.")]
public class WithDecorationInfo : ConditionalTraitInfo, Requires<IDecorationBoundsInfo> public class WithDecorationInfo : WithDecorationBaseInfo
{ {
[FieldLoader.Require]
[Desc("Image used for this decoration. Defaults to the actor's type.")] [Desc("Image used for this decoration. Defaults to the actor's type.")]
public readonly string Image = null; public readonly string Image = null;
[FieldLoader.Require]
[SequenceReference("Image")]
[Desc("Sequence used for this decoration (can be animated).")] [Desc("Sequence used for this decoration (can be animated).")]
public readonly string Sequence = null; public readonly string Sequence = null;
@@ -46,77 +37,20 @@ namespace OpenRA.Mods.Common.Traits.Render
[Desc("Custom palette is a player palette BaseName")] [Desc("Custom palette is a player palette BaseName")]
public readonly bool IsPlayerPalette = false; public readonly bool IsPlayerPalette = false;
[Desc("Point in the actor's selection box used as reference for offsetting the decoration image. " +
"Possible values are combinations of Center, Top, Bottom, Left, Right.")]
public readonly ReferencePoints ReferencePoint = ReferencePoints.Top | ReferencePoints.Left;
[Desc("The Z offset to apply when rendering this decoration.")]
public readonly int ZOffset = 1;
[Desc("Player stances who can view the decoration.")]
public readonly Stance ValidStances = Stance.Ally;
[Desc("Should this be visible only when selected?")]
public readonly bool RequiresSelection = false;
[Desc("Screen-space offsets to apply when defined conditions are enabled.",
"A dictionary of [condition string]: [x, y offset].")]
public readonly Dictionary<BooleanExpression, int2> Offsets = new Dictionary<BooleanExpression, int2>();
[Desc("The number of ticks that each step in the blink pattern in active.")]
public readonly int BlinkInterval = 5;
[Desc("A pattern of ticks (BlinkInterval long) where the decoration is visible or hidden.")]
public readonly BlinkState[] BlinkPattern = { };
[Desc("Override blink conditions to use when defined conditions are enabled.",
"A dictionary of [condition string]: [pattern].")]
public readonly Dictionary<BooleanExpression, BlinkState[]> BlinkPatterns = new Dictionary<BooleanExpression, BlinkState[]>();
[ConsumedConditionReference]
public IEnumerable<string> ConsumedConditions
{
get { return Offsets.Keys.Concat(BlinkPatterns.Keys).SelectMany(r => r.Variables).Distinct(); }
}
public override object Create(ActorInitializer init) { return new WithDecoration(init.Self, this); } public override object Create(ActorInitializer init) { return new WithDecoration(init.Self, this); }
} }
public class WithDecoration : ConditionalTrait<WithDecorationInfo>, ITick, IRenderAnnotations, IRenderAnnotationsWhenSelected public class WithDecoration : WithDecorationBase<WithDecorationInfo>, ITick
{ {
protected Animation anim; protected Animation anim;
readonly IDecorationBounds[] decorationBounds;
readonly string image; readonly string image;
int2 conditionalOffset;
BlinkState[] blinkPattern;
public WithDecoration(Actor self, WithDecorationInfo info) public WithDecoration(Actor self, WithDecorationInfo info)
: base(info) : base(self, info)
{ {
image = info.Image ?? self.Info.Name; image = info.Image ?? self.Info.Name;
anim = new Animation(self.World, image, () => self.World.Paused); anim = new Animation(self.World, image, () => self.World.Paused);
anim.PlayRepeating(info.Sequence); anim.PlayRepeating(info.Sequence);
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
blinkPattern = info.BlinkPattern;
}
protected virtual bool ShouldRender(Actor self)
{
if (blinkPattern != null && blinkPattern.Any())
{
var i = (self.World.WorldTick / Info.BlinkInterval) % blinkPattern.Length;
if (blinkPattern[i] != BlinkState.On)
return false;
}
if (self.World.RenderPlayer != null)
{
var stance = self.Owner.Stances[self.World.RenderPlayer];
if (!Info.ValidStances.HasStance(stance))
return false;
}
return true;
} }
protected virtual PaletteReference GetPalette(Actor self, WorldRenderer wr) protected virtual PaletteReference GetPalette(Actor self, WorldRenderer wr)
@@ -124,99 +58,17 @@ namespace OpenRA.Mods.Common.Traits.Render
return wr.Palette(Info.Palette + (Info.IsPlayerPalette ? self.Owner.InternalName : "")); return wr.Palette(Info.Palette + (Info.IsPlayerPalette ? self.Owner.InternalName : ""));
} }
IEnumerable<IRenderable> IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr) protected override IEnumerable<IRenderable> RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos)
{ {
return !Info.RequiresSelection ? RenderInner(self, wr) : SpriteRenderable.None; if (anim == null)
}
IEnumerable<IRenderable> IRenderAnnotationsWhenSelected.RenderAnnotations(Actor self, WorldRenderer wr)
{
return Info.RequiresSelection ? RenderInner(self, wr) : SpriteRenderable.None;
}
bool IRenderAnnotations.SpatiallyPartitionable { get { return true; } }
bool IRenderAnnotationsWhenSelected.SpatiallyPartitionable { get { return true; } }
IEnumerable<IRenderable> RenderInner(Actor self, WorldRenderer wr)
{
if (IsTraitDisabled || self.IsDead || !self.IsInWorld || anim == null)
return Enumerable.Empty<IRenderable>(); return Enumerable.Empty<IRenderable>();
if (!ShouldRender(self) || self.World.FogObscures(self))
return Enumerable.Empty<IRenderable>();
var bounds = decorationBounds.FirstNonEmptyBounds(self, wr);
var halfSize = (0.5f * anim.Image.Size.XY).ToInt2();
var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2;
var sizeOffset = -halfSize;
if (Info.ReferencePoint.HasFlag(ReferencePoints.Top))
{
boundsOffset -= new int2(0, bounds.Height / 2);
sizeOffset += new int2(0, halfSize.Y);
}
else if (Info.ReferencePoint.HasFlag(ReferencePoints.Bottom))
{
boundsOffset += new int2(0, bounds.Height / 2);
sizeOffset -= new int2(0, halfSize.Y);
}
if (Info.ReferencePoint.HasFlag(ReferencePoints.Left))
{
boundsOffset -= new int2(bounds.Width / 2, 0);
sizeOffset += new int2(halfSize.X, 0);
}
else if (Info.ReferencePoint.HasFlag(ReferencePoints.Right))
{
boundsOffset += new int2(bounds.Width / 2, 0);
sizeOffset -= new int2(halfSize.X, 0);
}
var pxPos = wr.Viewport.WorldToViewPx(boundsOffset) + sizeOffset + conditionalOffset;
return new IRenderable[] return new IRenderable[]
{ {
new UISpriteRenderable(anim.Image, self.CenterPosition, pxPos, Info.ZOffset, GetPalette(self, wr), 1f) new UISpriteRenderable(anim.Image, self.CenterPosition, screenPos - (0.5f * anim.Image.Size.XY).ToInt2(), 0, GetPalette(self, wr), 1f)
}; };
} }
void ITick.Tick(Actor self) { anim.Tick(); } void ITick.Tick(Actor self) { anim.Tick(); }
public override IEnumerable<VariableObserver> GetVariableObservers()
{
foreach (var observer in base.GetVariableObservers())
yield return observer;
foreach (var condition in Info.Offsets.Keys)
yield return new VariableObserver(OffsetConditionChanged, condition.Variables);
foreach (var condition in Info.BlinkPatterns.Keys)
yield return new VariableObserver(BlinkConditionsChanged, condition.Variables);
}
void OffsetConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
{
conditionalOffset = int2.Zero;
foreach (var kv in Info.Offsets)
{
if (kv.Key.Evaluate(conditions))
{
conditionalOffset = kv.Value;
break;
}
}
}
void BlinkConditionsChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
{
blinkPattern = Info.BlinkPattern;
foreach (var kv in Info.BlinkPatterns)
{
if (kv.Key.Evaluate(conditions))
{
blinkPattern = kv.Value;
return;
}
}
}
} }
} }

View File

@@ -0,0 +1,147 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Support;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render
{
public enum BlinkState { Off, On }
public abstract class WithDecorationBaseInfo : ConditionalTraitInfo
{
[Desc("Position in the actor's selection box to draw the decoration.")]
public readonly DecorationPosition Position = DecorationPosition.TopLeft;
[Desc("Player stances who can view the decoration.")]
public readonly Stance ValidStances = Stance.Ally;
[Desc("Should this be visible only when selected?")]
public readonly bool RequiresSelection = false;
[Desc("Offset sprite center position from the selection box edge.")]
public readonly int2 Margin = int2.Zero;
[Desc("Screen-space offsets to apply when defined conditions are enabled.",
"A dictionary of [condition string]: [x, y offset].")]
public readonly Dictionary<BooleanExpression, int2> Offsets = new Dictionary<BooleanExpression, int2>();
[Desc("The number of ticks that each step in the blink pattern in active.")]
public readonly int BlinkInterval = 5;
[Desc("A pattern of ticks (BlinkInterval long) where the decoration is visible or hidden.")]
public readonly BlinkState[] BlinkPattern = { };
[Desc("Override blink conditions to use when defined conditions are enabled.",
"A dictionary of [condition string]: [pattern].")]
public readonly Dictionary<BooleanExpression, BlinkState[]> BlinkPatterns = new Dictionary<BooleanExpression, BlinkState[]>();
[ConsumedConditionReference]
public IEnumerable<string> ConsumedConditions
{
get { return Offsets.Keys.Concat(BlinkPatterns.Keys).SelectMany(r => r.Variables).Distinct(); }
}
}
public abstract class WithDecorationBase<InfoType> : ConditionalTrait<InfoType>, IDecoration where InfoType : WithDecorationBaseInfo
{
protected readonly Actor self;
int2 conditionalOffset;
BlinkState[] blinkPattern;
public WithDecorationBase(Actor self, InfoType info)
: base(info)
{
this.self = self;
blinkPattern = info.BlinkPattern;
}
protected virtual bool ShouldRender(Actor self)
{
if (self.World.FogObscures(self))
return false;
if (blinkPattern != null && blinkPattern.Any())
{
var i = (self.World.WorldTick / Info.BlinkInterval) % blinkPattern.Length;
if (blinkPattern[i] != BlinkState.On)
return false;
}
if (self.World.RenderPlayer != null)
{
var stance = self.Owner.Stances[self.World.RenderPlayer];
if (!Info.ValidStances.HasStance(stance))
return false;
}
return true;
}
DecorationPosition IDecoration.Position { get { return Info.Position; } }
bool IDecoration.Enabled { get { return !IsTraitDisabled && self.IsInWorld && ShouldRender(self); } }
bool IDecoration.RequiresSelection { get { return Info.RequiresSelection; } }
protected abstract IEnumerable<IRenderable> RenderDecoration(Actor self, WorldRenderer wr, int2 pos);
IEnumerable<IRenderable> IDecoration.RenderDecoration(Actor self, WorldRenderer wr, int2 pos)
{
if (IsTraitDisabled || self.IsDead || !self.IsInWorld || !ShouldRender(self))
return Enumerable.Empty<IRenderable>();
var screenPos = wr.Viewport.WorldToViewPx(pos) + Info.Position.CreateMargin(Info.Margin) + conditionalOffset;
return RenderDecoration(self, wr, screenPos);
}
public override IEnumerable<VariableObserver> GetVariableObservers()
{
foreach (var observer in base.GetVariableObservers())
yield return observer;
foreach (var condition in Info.Offsets.Keys)
yield return new VariableObserver(OffsetConditionChanged, condition.Variables);
foreach (var condition in Info.BlinkPatterns.Keys)
yield return new VariableObserver(BlinkConditionsChanged, condition.Variables);
}
void OffsetConditionChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
{
conditionalOffset = int2.Zero;
foreach (var kv in Info.Offsets)
{
if (kv.Key.Evaluate(conditions))
{
conditionalOffset = kv.Value;
break;
}
}
}
void BlinkConditionsChanged(Actor self, IReadOnlyDictionary<string, int> conditions)
{
blinkPattern = Info.BlinkPattern;
foreach (var kv in Info.BlinkPatterns)
{
if (kv.Key.Evaluate(conditions))
{
blinkPattern = kv.Value;
return;
}
}
}
}
}

View File

@@ -0,0 +1,99 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Collections.Generic;
using OpenRA.Graphics;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render
{
public class WithHarvesterPipsDecorationInfo : WithDecorationBaseInfo, Requires<HarvesterInfo>
{
[FieldLoader.Require]
[Desc("Number of pips to display how filled unit is.")]
public readonly int PipCount = 0;
[Desc("If non-zero, override the spacing between adjacent pips.")]
public readonly int2 PipStride = int2.Zero;
[Desc("Image that defines the pip sequences.")]
public readonly string Image = "pips";
[SequenceReference("Image")]
[Desc("Sequence used for empty pips.")]
public readonly string EmptySequence = "pip-empty";
[SequenceReference("Image")]
[Desc("Sequence used for full pips that aren't defined in ResourceSequences.")]
public readonly string FullSequence = "pip-green";
// TODO: [SequenceReference] isn't smart enough to use Dictionaries.
[Desc("Pip sequence to use for specific resource types.")]
public readonly Dictionary<string, string> ResourceSequences = new Dictionary<string, string>();
[PaletteReference]
public readonly string Palette = "chrome";
public override object Create(ActorInitializer init) { return new WithHarvesterPipsDecoration(init.Self, this); }
}
public class WithHarvesterPipsDecoration : WithDecorationBase<WithHarvesterPipsDecorationInfo>
{
readonly Harvester harvester;
readonly Animation pips;
public WithHarvesterPipsDecoration(Actor self, WithHarvesterPipsDecorationInfo info)
: base(self, info)
{
harvester = self.Trait<Harvester>();
pips = new Animation(self.World, info.Image);
}
string GetPipSequence(int i)
{
var n = i * harvester.Info.Capacity / Info.PipCount;
string sequence;
foreach (var rt in harvester.Contents)
{
if (n < rt.Value)
{
if (!Info.ResourceSequences.TryGetValue(rt.Key.Type, out sequence))
sequence = Info.FullSequence;
return sequence;
}
n -= rt.Value;
}
return Info.EmptySequence;
}
protected override IEnumerable<IRenderable> RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos)
{
pips.PlayRepeating(Info.EmptySequence);
var palette = wr.Palette(Info.Palette);
var pipSize = pips.Image.Size.XY.ToInt2();
var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0);
screenPos -= pipSize / 2;
for (var i = 0; i < Info.PipCount; i++)
{
pips.PlayRepeating(GetPipSequence(i));
yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f);
screenPos += pipStride;
}
}
}
}

View File

@@ -0,0 +1,84 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render
{
[Desc("Displays the player name above the unit")]
public class WithNameTagDecorationInfo : WithDecorationBaseInfo
{
public readonly int MaxLength = 10;
public readonly string Font = "TinyBold";
[Desc("Display in this color when not using the player color.")]
public readonly Color Color = Color.White;
[Desc("Use the player color of the current owner.")]
public readonly bool UsePlayerColor = false;
public override object Create(ActorInitializer init) { return new WithNameTagDecoration(init.Self, this); }
public override void RulesetLoaded(Ruleset rules, ActorInfo ai)
{
if (!Game.ModData.Manifest.Get<Fonts>().FontList.ContainsKey(Font))
throw new YamlException("Font '{0}' is not listed in the mod.yaml's Fonts section".F(Font));
base.RulesetLoaded(rules, ai);
}
}
public class WithNameTagDecoration : WithDecorationBase<WithNameTagDecorationInfo>, INotifyOwnerChanged
{
readonly SpriteFont font;
string name;
Color color;
public WithNameTagDecoration(Actor self, WithNameTagDecorationInfo info)
: base(self, info)
{
font = Game.Renderer.Fonts[info.Font];
color = info.UsePlayerColor ? self.Owner.Color : info.Color;
name = self.Owner.PlayerName;
if (name.Length > info.MaxLength)
name = name.Substring(0, info.MaxLength);
}
protected override IEnumerable<IRenderable> RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos)
{
if (IsTraitDisabled || self.IsDead || !self.IsInWorld || !ShouldRender(self))
return Enumerable.Empty<IRenderable>();
var size = font.Measure(name);
return new IRenderable[]
{
new UITextRenderable(font, self.CenterPosition, screenPos - size / 2, 0, color, name)
};
}
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
{
if (Info.UsePlayerColor)
color = newOwner.Color;
name = self.Owner.PlayerName;
if (name.Length > Info.MaxLength)
name = name.Substring(0, Info.MaxLength);
}
}
}

View File

@@ -0,0 +1,80 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render
{
public class WithResourceStoragePipsDecorationInfo : WithDecorationBaseInfo
{
[FieldLoader.Require]
[Desc("Number of pips to display how filled unit is.")]
public readonly int PipCount = 0;
[Desc("If non-zero, override the spacing between adjacing pips.")]
public readonly int2 PipStride = int2.Zero;
[Desc("Image that defines the pip sequences.")]
public readonly string Image = "pips";
[SequenceReference("Image")]
[Desc("Sequence used for empty pips.")]
public readonly string EmptySequence = "pip-empty";
[SequenceReference("Image")]
[Desc("Sequence used for full pips.")]
public readonly string FullSequence = "pip-green";
[PaletteReference]
public readonly string Palette = "chrome";
public override object Create(ActorInitializer init) { return new WithResourceStoragePipsDecoration(init.Self, this); }
}
public class WithResourceStoragePipsDecoration : WithDecorationBase<WithResourceStoragePipsDecorationInfo>, INotifyOwnerChanged
{
readonly Animation pips;
PlayerResources player;
public WithResourceStoragePipsDecoration(Actor self, WithResourceStoragePipsDecorationInfo info)
: base(self, info)
{
player = self.Owner.PlayerActor.Trait<PlayerResources>();
pips = new Animation(self.World, info.Image);
}
protected override IEnumerable<IRenderable> RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos)
{
pips.PlayRepeating(Info.EmptySequence);
var palette = wr.Palette(Info.Palette);
var pipSize = pips.Image.Size.XY.ToInt2();
var pipStride = Info.PipStride != int2.Zero ? Info.PipStride : new int2(pipSize.X, 0);
screenPos -= pipSize / 2;
for (var i = 0; i < Info.PipCount; i++)
{
pips.PlayRepeating(player.Resources * Info.PipCount > i * player.ResourceCapacity ? Info.FullSequence : Info.EmptySequence);
yield return new UISpriteRenderable(pips.Image, self.CenterPosition, screenPos, 0, palette, 1f);
screenPos += pipStride;
}
}
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)
{
player = newOwner.PlayerActor.Trait<PlayerResources>();
}
}
}

View File

@@ -17,7 +17,7 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render namespace OpenRA.Mods.Common.Traits.Render
{ {
[Desc("Renders Ctrl groups using pixel art.")] [Desc("Renders Ctrl groups using pixel art.")]
public class WithSpriteControlGroupDecorationInfo : ITraitInfo, Requires<IDecorationBoundsInfo> public class WithSpriteControlGroupDecorationInfo : ITraitInfo
{ {
[PaletteReference] [PaletteReference]
public readonly string Palette = "chrome"; public readonly string Palette = "chrome";
@@ -28,66 +28,49 @@ namespace OpenRA.Mods.Common.Traits.Render
[Desc("Sprite sequence used to render the control group 0-9 numbers.")] [Desc("Sprite sequence used to render the control group 0-9 numbers.")]
public readonly string GroupSequence = "groups"; public readonly string GroupSequence = "groups";
[Desc("Point in the actor's selection box used as reference for offsetting the decoration image. " + [Desc("Position in the actor's selection box to draw the decoration.")]
"Possible values are combinations of Center, Top, Bottom, Left, Right.")] public readonly DecorationPosition Position = DecorationPosition.TopLeft;
public readonly ReferencePoints ReferencePoint = ReferencePoints.Top | ReferencePoints.Left;
[Desc("Offset sprite center position from the selection box edge.")]
public readonly int2 Margin = int2.Zero;
public object Create(ActorInitializer init) { return new WithSpriteControlGroupDecoration(init.Self, this); } public object Create(ActorInitializer init) { return new WithSpriteControlGroupDecoration(init.Self, this); }
} }
public class WithSpriteControlGroupDecoration : IRenderAnnotationsWhenSelected public class WithSpriteControlGroupDecoration : IDecoration
{ {
public readonly WithSpriteControlGroupDecorationInfo Info; public readonly WithSpriteControlGroupDecorationInfo Info;
readonly IDecorationBounds[] decorationBounds; readonly Actor self;
readonly Animation pipImages; readonly Animation anim;
public WithSpriteControlGroupDecoration(Actor self, WithSpriteControlGroupDecorationInfo info) public WithSpriteControlGroupDecoration(Actor self, WithSpriteControlGroupDecorationInfo info)
{ {
Info = info; Info = info;
this.self = self;
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray(); anim = new Animation(self.World, Info.Image);
pipImages = new Animation(self.World, Info.Image);
} }
IEnumerable<IRenderable> IRenderAnnotationsWhenSelected.RenderAnnotations(Actor self, WorldRenderer wr) DecorationPosition IDecoration.Position { get { return Info.Position; } }
{
if (self.Owner != wr.World.LocalPlayer)
yield break;
if (self.World.FogObscures(self)) bool IDecoration.Enabled { get { return self.Owner == self.World.LocalPlayer && self.World.Selection.GetControlGroupForActor(self) != null; } }
yield break;
var pal = wr.Palette(Info.Palette); bool IDecoration.RequiresSelection { get { return true; } }
foreach (var r in DrawControlGroup(self, wr, pal))
yield return r;
}
bool IRenderAnnotationsWhenSelected.SpatiallyPartitionable { get { return true; } } IEnumerable<IRenderable> IDecoration.RenderDecoration(Actor self, WorldRenderer wr, int2 pos)
IEnumerable<IRenderable> DrawControlGroup(Actor self, WorldRenderer wr, PaletteReference palette)
{ {
var group = self.World.Selection.GetControlGroupForActor(self); var group = self.World.Selection.GetControlGroupForActor(self);
if (group == null) if (group == null)
yield break; return Enumerable.Empty<IRenderable>();
pipImages.PlayFetchIndex(Info.GroupSequence, () => (int)group); anim.PlayFetchIndex(Info.GroupSequence, () => (int)group);
var bounds = decorationBounds.FirstNonEmptyBounds(self, wr); var screenPos = wr.Viewport.WorldToViewPx(pos) + Info.Position.CreateMargin(Info.Margin) - (0.5f * anim.Image.Size.XY).ToInt2();
var boundsOffset = 0.5f * new float2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom); var palette = wr.Palette(Info.Palette);
if (Info.ReferencePoint.HasFlag(ReferencePoints.Top)) return new IRenderable[]
boundsOffset -= new float2(0, 0.5f * bounds.Height); {
new UISpriteRenderable(anim.Image, self.CenterPosition, screenPos, 0, palette, 1f)
if (Info.ReferencePoint.HasFlag(ReferencePoints.Bottom)) };
boundsOffset += new float2(0, 0.5f * bounds.Height);
if (Info.ReferencePoint.HasFlag(ReferencePoints.Left))
boundsOffset -= new float2(0.5f * bounds.Width, 0);
if (Info.ReferencePoint.HasFlag(ReferencePoints.Right))
boundsOffset += new float2(0.5f * bounds.Width, 0);
var pxPos = wr.Viewport.WorldToViewPx(boundsOffset.ToInt2()) - (0.5f * pipImages.Image.Size.XY).ToInt2();
yield return new UISpriteRenderable(pipImages.Image, self.CenterPosition, pxPos, 0, palette, 1f);
} }
} }
} }

View File

@@ -13,13 +13,14 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Mods.Common.Graphics; using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Widgets;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Traits; using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render namespace OpenRA.Mods.Common.Traits.Render
{ {
[Desc("Renders Ctrl groups using typeface.")] [Desc("Renders Ctrl groups using typeface.")]
public class WithTextControlGroupDecorationInfo : ITraitInfo, IRulesetLoaded, Requires<IDecorationBoundsInfo> public class WithTextControlGroupDecorationInfo : ITraitInfo, IRulesetLoaded
{ {
public readonly string Font = "TinyBold"; public readonly string Font = "TinyBold";
@@ -29,15 +30,11 @@ namespace OpenRA.Mods.Common.Traits.Render
[Desc("Use the player color of the current owner.")] [Desc("Use the player color of the current owner.")]
public readonly bool UsePlayerColor = false; public readonly bool UsePlayerColor = false;
[Desc("The Z offset to apply when rendering this decoration.")] [Desc("Position in the actor's selection box to draw the decoration.")]
public readonly int ZOffset = 1; public readonly DecorationPosition Position = DecorationPosition.TopLeft;
[Desc("Point in the actor's selection box used as reference for offsetting the decoration image. " + [Desc("Offset text center position from the selection box edge.")]
"Possible values are combinations of Center, Top, Bottom, Left, Right.")] public readonly int2 Margin = int2.Zero;
public readonly ReferencePoints ReferencePoint = ReferencePoints.Bottom | ReferencePoints.Left;
[Desc("Manual offset in screen pixel.")]
public readonly int2 ScreenOffset = new int2(2, -2);
void IRulesetLoaded<ActorInfo>.RulesetLoaded(Ruleset rules, ActorInfo info) void IRulesetLoaded<ActorInfo>.RulesetLoaded(Ruleset rules, ActorInfo info)
{ {
@@ -48,76 +45,42 @@ namespace OpenRA.Mods.Common.Traits.Render
public object Create(ActorInitializer init) { return new WithTextControlGroupDecoration(init.Self, this); } public object Create(ActorInitializer init) { return new WithTextControlGroupDecoration(init.Self, this); }
} }
public class WithTextControlGroupDecoration : IRenderAnnotationsWhenSelected, INotifyOwnerChanged public class WithTextControlGroupDecoration : IDecoration, INotifyOwnerChanged
{ {
readonly WithTextControlGroupDecorationInfo info; readonly WithTextControlGroupDecorationInfo info;
readonly IDecorationBounds[] decorationBounds;
readonly SpriteFont font; readonly SpriteFont font;
readonly Actor self;
readonly CachedTransform<int, string> label;
Color color; Color color;
public WithTextControlGroupDecoration(Actor self, WithTextControlGroupDecorationInfo info) public WithTextControlGroupDecoration(Actor self, WithTextControlGroupDecorationInfo info)
{ {
this.info = info; this.info = info;
this.self = self;
if (!Game.Renderer.Fonts.TryGetValue(info.Font, out font)) font = Game.Renderer.Fonts[info.Font];
throw new YamlException("Font '{0}' is not listed in the mod.yaml's Fonts section".F(info.Font));
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
color = info.UsePlayerColor ? self.Owner.Color : info.Color; color = info.UsePlayerColor ? self.Owner.Color : info.Color;
label = new CachedTransform<int, string>(g => g.ToString());
} }
IEnumerable<IRenderable> IRenderAnnotationsWhenSelected.RenderAnnotations(Actor self, WorldRenderer wr) DecorationPosition IDecoration.Position { get { return info.Position; } }
{
if (self.Owner != wr.World.LocalPlayer)
yield break;
if (self.World.FogObscures(self)) bool IDecoration.Enabled { get { return self.Owner == self.World.LocalPlayer && self.World.Selection.GetControlGroupForActor(self) != null; } }
yield break;
foreach (var r in DrawControlGroup(self, wr)) bool IDecoration.RequiresSelection { get { return true; } }
yield return r;
}
bool IRenderAnnotationsWhenSelected.SpatiallyPartitionable { get { return true; } } IEnumerable<IRenderable> IDecoration.RenderDecoration(Actor self, WorldRenderer wr, int2 pos)
IEnumerable<IRenderable> DrawControlGroup(Actor self, WorldRenderer wr)
{ {
var group = self.World.Selection.GetControlGroupForActor(self); var group = self.World.Selection.GetControlGroupForActor(self);
if (group == null) if (group == null)
yield break; return Enumerable.Empty<IRenderable>();
var bounds = decorationBounds.FirstNonEmptyBounds(self, wr); var text = label.Update(group.Value);
var number = group.Value.ToString(); var screenPos = wr.Viewport.WorldToViewPx(pos) + info.Position.CreateMargin(info.Margin);
var halfSize = font.Measure(number) / 2; return new IRenderable[]
var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2;
var sizeOffset = int2.Zero;
if (info.ReferencePoint.HasFlag(ReferencePoints.Top))
{ {
boundsOffset -= new int2(0, bounds.Height / 2); new UITextRenderable(font, self.CenterPosition, screenPos, 0, color, text)
sizeOffset += new int2(0, halfSize.Y); };
}
else if (info.ReferencePoint.HasFlag(ReferencePoints.Bottom))
{
boundsOffset += new int2(0, bounds.Height / 2);
sizeOffset -= new int2(0, halfSize.Y);
}
if (info.ReferencePoint.HasFlag(ReferencePoints.Left))
{
boundsOffset -= new int2(bounds.Width / 2, 0);
sizeOffset += new int2(halfSize.X, 0);
}
else if (info.ReferencePoint.HasFlag(ReferencePoints.Right))
{
boundsOffset += new int2(bounds.Width / 2, 0);
sizeOffset -= new int2(halfSize.X, 0);
}
var screenPos = boundsOffset + sizeOffset + info.ScreenOffset;
yield return new TextAnnotationRenderable(font, wr.ProjectedPosition(screenPos), info.ZOffset, color, number);
} }
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)

View File

@@ -19,7 +19,7 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render namespace OpenRA.Mods.Common.Traits.Render
{ {
[Desc("Displays a text overlay relative to the selection box.")] [Desc("Displays a text overlay relative to the selection box.")]
public class WithTextDecorationInfo : ConditionalTraitInfo, Requires<IDecorationBoundsInfo> public class WithTextDecorationInfo : WithDecorationBaseInfo
{ {
[Translate] [Translate]
[FieldLoader.Require] [FieldLoader.Require]
@@ -33,19 +33,6 @@ namespace OpenRA.Mods.Common.Traits.Render
[Desc("Use the player color of the current owner.")] [Desc("Use the player color of the current owner.")]
public readonly bool UsePlayerColor = false; public readonly bool UsePlayerColor = false;
[Desc("Point in the actor's selection box used as reference for offsetting the decoration image. " +
"Possible values are combinations of Center, Top, Bottom, Left, Right.")]
public readonly ReferencePoints ReferencePoint = ReferencePoints.Top | ReferencePoints.Left;
[Desc("The Z offset to apply when rendering this decoration.")]
public readonly int ZOffset = 1;
[Desc("Player stances who can view the decoration.")]
public readonly Stance ValidStances = Stance.Ally;
[Desc("Should this be visible only when selected?")]
public readonly bool RequiresSelection = false;
public override object Create(ActorInitializer init) { return new WithTextDecoration(init.Self, this); } public override object Create(ActorInitializer init) { return new WithTextDecoration(init.Self, this); }
public override void RulesetLoaded(Ruleset rules, ActorInfo ai) public override void RulesetLoaded(Ruleset rules, ActorInfo ai)
@@ -57,79 +44,28 @@ namespace OpenRA.Mods.Common.Traits.Render
} }
} }
public class WithTextDecoration : ConditionalTrait<WithTextDecorationInfo>, IRenderAnnotations, IRenderAnnotationsWhenSelected, INotifyOwnerChanged public class WithTextDecoration : WithDecorationBase<WithTextDecorationInfo>, INotifyOwnerChanged
{ {
readonly SpriteFont font; readonly SpriteFont font;
readonly IDecorationBounds[] decorationBounds;
Color color; Color color;
public WithTextDecoration(Actor self, WithTextDecorationInfo info) public WithTextDecoration(Actor self, WithTextDecorationInfo info)
: base(info) : base(self, info)
{ {
font = Game.Renderer.Fonts[info.Font]; font = Game.Renderer.Fonts[info.Font];
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray(); color = info.UsePlayerColor ? self.Owner.Color : info.Color;
color = Info.UsePlayerColor ? self.Owner.Color : Info.Color;
} }
public virtual bool ShouldRender(Actor self) { return true; } protected override IEnumerable<IRenderable> RenderDecoration(Actor self, WorldRenderer wr, int2 screenPos)
IEnumerable<IRenderable> IRenderAnnotations.RenderAnnotations(Actor self, WorldRenderer wr)
{ {
return !Info.RequiresSelection ? RenderInner(self, wr) : SpriteRenderable.None; if (IsTraitDisabled || self.IsDead || !self.IsInWorld || !ShouldRender(self))
}
public bool SpatiallyPartitionable { get { return true; } }
IEnumerable<IRenderable> IRenderAnnotationsWhenSelected.RenderAnnotations(Actor self, WorldRenderer wr)
{
return Info.RequiresSelection ? RenderInner(self, wr) : SpriteRenderable.None;
}
bool IRenderAnnotationsWhenSelected.SpatiallyPartitionable { get { return true; } }
IEnumerable<IRenderable> RenderInner(Actor self, WorldRenderer wr)
{
if (IsTraitDisabled || self.IsDead || !self.IsInWorld)
return Enumerable.Empty<IRenderable>(); return Enumerable.Empty<IRenderable>();
if (self.World.RenderPlayer != null) var size = font.Measure(Info.Text);
return new IRenderable[]
{ {
var stance = self.Owner.Stances[self.World.RenderPlayer]; new UITextRenderable(font, self.CenterPosition, screenPos - size / 2, 0, color, Info.Text)
if (!Info.ValidStances.HasStance(stance)) };
return Enumerable.Empty<IRenderable>();
}
if (!ShouldRender(self) || self.World.FogObscures(self))
return Enumerable.Empty<IRenderable>();
var bounds = decorationBounds.FirstNonEmptyBounds(self, wr);
var halfSize = font.Measure(Info.Text) / 2;
var boundsOffset = new int2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom) / 2;
var sizeOffset = int2.Zero;
if (Info.ReferencePoint.HasFlag(ReferencePoints.Top))
{
boundsOffset -= new int2(0, bounds.Height / 2);
sizeOffset += new int2(0, halfSize.Y);
}
else if (Info.ReferencePoint.HasFlag(ReferencePoints.Bottom))
{
boundsOffset += new int2(0, bounds.Height / 2);
sizeOffset -= new int2(0, halfSize.Y);
}
if (Info.ReferencePoint.HasFlag(ReferencePoints.Left))
{
boundsOffset -= new int2(bounds.Width / 2, 0);
sizeOffset += new int2(halfSize.X, 0);
}
else if (Info.ReferencePoint.HasFlag(ReferencePoints.Right))
{
boundsOffset += new int2(bounds.Width / 2, 0);
sizeOffset -= new int2(halfSize.X, 0);
}
return new IRenderable[] { new TextAnnotationRenderable(font, wr.ProjectedPosition(boundsOffset + sizeOffset), Info.ZOffset, color, Info.Text) };
} }
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner) void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)

View File

@@ -22,16 +22,10 @@ namespace OpenRA.Mods.Common.Traits
[FieldLoader.Require] [FieldLoader.Require]
public readonly int Capacity = 0; public readonly int Capacity = 0;
[FieldLoader.Require]
[Desc("Number of little squares used to display how filled unit is.")]
public readonly int PipCount = 0;
public readonly PipType PipColor = PipType.Yellow;
public object Create(ActorInitializer init) { return new StoresResources(init.Self, this); } public object Create(ActorInitializer init) { return new StoresResources(init.Self, this); }
} }
public class StoresResources : IPips, INotifyOwnerChanged, INotifyCapture, IStoreResources, ISync, INotifyKilled, INotifyAddedToWorld, INotifyRemovedFromWorld public class StoresResources : INotifyOwnerChanged, INotifyCapture, IStoreResources, ISync, INotifyKilled, INotifyAddedToWorld, INotifyRemovedFromWorld
{ {
readonly StoresResourcesInfo info; readonly StoresResourcesInfo info;
PlayerResources player; PlayerResources player;
@@ -65,13 +59,6 @@ namespace OpenRA.Mods.Common.Traits
player.TakeResources(Stored); player.TakeResources(Stored);
} }
IEnumerable<PipType> IPips.GetPips(Actor self)
{
return Enumerable.Range(0, info.PipCount).Select(i =>
player.Resources * info.PipCount > i * player.ResourceCapacity
? info.PipColor : PipType.Transparent);
}
void INotifyAddedToWorld.AddedToWorld(Actor self) void INotifyAddedToWorld.AddedToWorld(Actor self)
{ {
player.AddStorage(info.Capacity); player.AddStorage(info.Capacity);

View File

@@ -146,8 +146,9 @@ namespace OpenRA.Mods.Common.Traits
var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos); var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos);
foreach (var unit in power.UnitsInRange(xy)) foreach (var unit in power.UnitsInRange(xy))
{ {
var bounds = unit.TraitsImplementing<IDecorationBounds>().FirstNonEmptyBounds(unit, wr); var decorations = unit.TraitsImplementing<ISelectionDecorations>().FirstEnabledTraitOrDefault();
yield return new SelectionBoxAnnotationRenderable(unit, bounds, Color.Red); foreach (var d in decorations.RenderSelectionAnnotations(unit, wr, Color.Red))
yield return d;
} }
} }

View File

@@ -63,9 +63,6 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Allow resource to spawn on ramp tiles.")] [Desc("Allow resource to spawn on ramp tiles.")]
public readonly bool AllowOnRamps = false; public readonly bool AllowOnRamps = false;
[Desc("Harvester content pip color.")]
public PipType PipColor = PipType.Yellow;
void IMapPreviewSignatureInfo.PopulateMapPreviewSignatureCells(Map map, ActorInfo ai, ActorReference s, List<Pair<MPos, Color>> destinationBuffer) void IMapPreviewSignatureInfo.PopulateMapPreviewSignatureCells(Map map, ActorInfo ai, ActorReference s, List<Pair<MPos, Color>> destinationBuffer)
{ {
var tileSet = map.Rules.TileSet; var tileSet = map.Rules.TileSet;

View File

@@ -15,6 +15,7 @@ using OpenRA.Activities;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Mods.Common.Activities; using OpenRA.Mods.Common.Activities;
using OpenRA.Mods.Common.Graphics; using OpenRA.Mods.Common.Graphics;
using OpenRA.Mods.Common.Traits.Render;
using OpenRA.Primitives; using OpenRA.Primitives;
using OpenRA.Traits; using OpenRA.Traits;
@@ -642,4 +643,40 @@ namespace OpenRA.Mods.Common.Traits
{ {
string Class { get; } string Class { get; }
} }
public interface IDecoration
{
DecorationPosition Position { get; }
bool RequiresSelection { get; }
bool Enabled { get; }
IEnumerable<IRenderable> RenderDecoration(Actor self, WorldRenderer wr, int2 pos);
}
public enum DecorationPosition
{
Center,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Top
}
public static class DecorationExtensions
{
public static int2 CreateMargin(this DecorationPosition pos, int2 margin)
{
switch (pos)
{
case DecorationPosition.TopLeft: return margin;
case DecorationPosition.TopRight: return new int2(-margin.X, margin.Y);
case DecorationPosition.BottomLeft: return new int2(margin.X, -margin.Y);
case DecorationPosition.BottomRight: return -margin;
case DecorationPosition.Top: return new int2(0, margin.Y);
default: return int2.Zero;
}
}
}
} }

View File

@@ -0,0 +1,273 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Collections.Generic;
using System.Linq;
namespace OpenRA.Mods.Common.UpdateRules.Rules
{
public class AddPipDecorationTraits : UpdateRule
{
public override string Name { get { return "Add decoration traits for selection pips."; } }
public override string Description
{
get
{
return "The AmmoPool, Cargo, Harvester, and StoresResources traits no longer\n" +
"automatically add pips to the selection box. New traits WithAmmoPipsDecoration,\n" +
"WithCargoPipsDecoration, WithHarvesterPipsDecoration,\n" +
"WithResourceStoragePipsDecoration are added to provide the same functionality.\n\n" +
"Passenger.PipType has been replaced with CustomPipType, which now references a\n" +
"sequence defined in WithCargoDecoration.CustomPipTypeSequences.\n\n" +
"ResourceType.PipColor has been removed and resource pip colours are now defined\n" +
"in WithHarvesterPipsDecoration.ResourceSequences.";
}
}
static readonly Dictionary<string, string> PipReplacements = new Dictionary<string, string>
{
{ "transparent", "pip-empty" },
{ "green", "pip-green" },
{ "yellow", "pip-yellow" },
{ "red", "pip-red" },
{ "gray", "pip-gray" },
{ "blue", "pip-blue" },
{ "ammo", "pip-ammo" },
{ "ammoempty", "pip-ammoempty" },
};
bool customPips;
readonly List<string> locations = new List<string>();
readonly List<string> cargoPipLocations = new List<string>();
readonly HashSet<string> cargoCustomPips = new HashSet<string>();
readonly List<string> harvesterPipLocations = new List<string>();
readonly Dictionary<string, string> harvesterCustomPips = new Dictionary<string, string>();
public override IEnumerable<string> AfterUpdate(ModData modData)
{
if (customPips && locations.Any())
yield return "Custom pip Images and Palettes are now defined on the individual With*PipsDecoration traits.\n" +
"You should review the following definitions and manually define the Image and Palette properties as required:\n" +
UpdateUtils.FormatMessageList(locations);
if (cargoCustomPips.Any() && cargoPipLocations.Any())
yield return "Some passenger types define custom cargo pips. Review the following definitions:\n" +
UpdateUtils.FormatMessageList(cargoPipLocations) +
"\nand, if required, add the following to the WithCargoPipsDecoration traits:\n" +
"CustomPipSequences:\n" + cargoCustomPips.Select(p => "\t{0}: {1}".F(p, PipReplacements[p])).JoinWith("\n");
if (harvesterCustomPips.Any() && harvesterPipLocations.Any())
yield return "Review the following definitions:\n" +
UpdateUtils.FormatMessageList(harvesterPipLocations) +
"\nand, if required, add the following to the WithHarvesterPipsDecoration traits:\n" +
"ResourceSequences:\n" + harvesterCustomPips.Select(kv => "\t{0}: {1}".F(kv.Key, PipReplacements[kv.Value])).JoinWith("\n");
customPips = false;
locations.Clear();
cargoPipLocations.Clear();
harvesterPipLocations.Clear();
}
public override IEnumerable<string> UpdateActorNode(ModData modData, MiniYamlNode actorNode)
{
var addNodes = new List<MiniYamlNode>();
foreach (var selectionDecorations in actorNode.ChildrenMatching("SelectionDecorations"))
{
customPips |= selectionDecorations.RemoveNodes("Palette") > 0;
customPips |= selectionDecorations.RemoveNodes("Image") > 0;
}
foreach (var ammoPool in actorNode.ChildrenMatching("AmmoPool"))
{
var ammoPips = new MiniYamlNode("WithAmmoPipsDecoration", "");
ammoPips.AddNode("Position", "BottomLeft");
ammoPips.AddNode("RequiresSelection", "true");
var pipCountNode = ammoPool.LastChildMatching("PipCount");
if (pipCountNode != null)
{
ammoPool.RemoveNode(pipCountNode);
var pipCount = pipCountNode.NodeValue<int>();
if (pipCount == 0)
{
addNodes.Add(new MiniYamlNode("-" + ammoPips.Key, ""));
continue;
}
var ammoNode = ammoPool.LastChildMatching("Ammo");
var maxAmmo = ammoNode != null ? ammoNode.NodeValue<int>() : 0;
if (pipCount != maxAmmo)
ammoPips.AddNode("PipCount", pipCount);
}
var pipTypeNode = ammoPool.LastChildMatching("PipType");
if (pipTypeNode != null)
{
ammoPool.RemoveNode(pipTypeNode);
string sequence;
if (PipReplacements.TryGetValue(pipTypeNode.Value.Value.ToLowerInvariant(), out sequence))
ammoPips.AddNode("FullSequence", sequence);
}
var pipTypeEmptyNode = ammoPool.LastChildMatching("PipTypeEmpty");
if (pipTypeEmptyNode != null)
{
ammoPool.RemoveNode(pipTypeEmptyNode);
string sequence;
if (PipReplacements.TryGetValue(pipTypeEmptyNode.Value.Value.ToLowerInvariant(), out sequence))
ammoPips.AddNode("EmptySequence", sequence);
}
addNodes.Add(ammoPips);
locations.Add("{0}: {1} ({2})".F(actorNode.Key, ammoPips.Key, actorNode.Location.Filename));
}
foreach (var cargo in actorNode.ChildrenMatching("Cargo"))
{
var cargoPips = new MiniYamlNode("WithCargoPipsDecoration", "");
cargoPips.AddNode("Position", "BottomLeft");
cargoPips.AddNode("RequiresSelection", "true");
var pipCountNode = cargo.LastChildMatching("PipCount");
if (pipCountNode != null)
{
cargo.RemoveNode(pipCountNode);
var pipCount = pipCountNode.NodeValue<int>();
if (pipCount == 0)
{
addNodes.Add(new MiniYamlNode("-" + cargoPips.Key, ""));
continue;
}
var maxWeightNode = cargo.LastChildMatching("MaxWeight");
var maxWeight = maxWeightNode != null ? maxWeightNode.NodeValue<int>() : 0;
if (pipCount != maxWeight)
cargoPips.AddNode("PipCount", pipCount);
}
else
continue;
addNodes.Add(cargoPips);
locations.Add("{0}: {1} ({2})".F(actorNode.Key, cargoPips.Key, actorNode.Location.Filename));
cargoPipLocations.Add("{0} ({1})".F(actorNode.Key, actorNode.Location.Filename));
}
foreach (var passenger in actorNode.ChildrenMatching("Passenger"))
{
var pipTypeNode = passenger.LastChildMatching("PipType");
if (pipTypeNode != null)
{
pipTypeNode.RenameKey("CustomPipType");
pipTypeNode.Value.Value = pipTypeNode.Value.Value.ToLowerInvariant();
cargoCustomPips.Add(pipTypeNode.Value.Value);
}
}
foreach (var harvester in actorNode.ChildrenMatching("Harvester"))
{
var harvesterPips = new MiniYamlNode("WithHarvesterPipsDecoration", "");
harvesterPips.AddNode("Position", "BottomLeft");
harvesterPips.AddNode("RequiresSelection", "true");
// Harvester hardcoded a default PipCount > 0 so we can't use that to determine whether
// this is a definition or an override. Resources isn't ideal either, but is better than nothing
var resourcesNode = harvester.LastChildMatching("Resources");
if (resourcesNode == null)
continue;
var pipCountNode = harvester.LastChildMatching("PipCount");
if (pipCountNode != null)
{
harvester.RemoveNode(pipCountNode);
var pipCount = pipCountNode.NodeValue<int>();
if (pipCount == 0)
{
addNodes.Add(new MiniYamlNode("-" + harvesterPips.Key, ""));
continue;
}
harvesterPips.AddNode("PipCount", pipCount);
}
else
harvesterPips.AddNode("PipCount", 7);
addNodes.Add(harvesterPips);
locations.Add("{0}: {1} ({2})".F(actorNode.Key, harvesterPips.Key, actorNode.Location.Filename));
harvesterPipLocations.Add("{0} ({1})".F(actorNode.Key, actorNode.Location.Filename));
}
foreach (var resourceType in actorNode.ChildrenMatching("ResourceType"))
{
var pipColor = "yellow";
var pipCountNode = resourceType.LastChildMatching("PipColor");
if (pipCountNode != null)
{
pipColor = pipCountNode.Value.Value.ToLowerInvariant();
resourceType.RemoveNode(pipCountNode);
}
var typeNode = resourceType.LastChildMatching("Type");
if (typeNode != null)
harvesterCustomPips.Add(typeNode.Value.Value, pipColor);
}
foreach (var storesResources in actorNode.ChildrenMatching("StoresResources"))
{
var storagePips = new MiniYamlNode("WithResourceStoragePipsDecoration", "");
storagePips.AddNode("Position", "BottomLeft");
storagePips.AddNode("RequiresSelection", "true");
var pipCountNode = storesResources.LastChildMatching("PipCount");
if (pipCountNode != null)
{
storesResources.RemoveNode(pipCountNode);
var pipCount = pipCountNode.NodeValue<int>();
if (pipCount == 0)
{
addNodes.Add(new MiniYamlNode("-" + storagePips.Key, ""));
continue;
}
storagePips.AddNode("PipCount", pipCount);
}
else
continue;
// Default pip color changed from yellow to green for consistency with other pip traits
var pipColorNode = storesResources.LastChildMatching("PipColor");
if (pipColorNode != null)
{
storesResources.RemoveNode(pipColorNode);
string sequence;
var type = pipColorNode.Value.Value.ToLowerInvariant();
if (type != "green" && PipReplacements.TryGetValue(type, out sequence))
storagePips.AddNode("FullSequence", sequence);
}
else
storagePips.AddNode("FullSequence", PipReplacements["yellow"]);
addNodes.Add(storagePips);
locations.Add("{0}: {1} ({2})".F(actorNode.Key, storagePips.Key, actorNode.Location.Filename));
}
foreach (var addNode in addNodes)
actorNode.AddNode(addNode);
yield break;
}
}
}

View File

@@ -0,0 +1,123 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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.Mods.Common.Traits;
namespace OpenRA.Mods.Common.UpdateRules.Rules
{
public class ModernizeDecorationTraits : UpdateRule
{
public override string Name { get { return "Modernize SelectionDecorations and With*Decoration traits."; } }
public override string Description
{
get
{
return "The configuration properties exposed on the SelectionDecorations and With*Decoration\n" +
"traits have been reworked. RenderSelectionBars and RenderSelectionBox have been removed from\n" +
"SelectionDecorations. The obsolete ZOffset and ScreenOffset has been removed from With*Decoration, and ReferencePoint has\n" +
"been replaced by Position which takes a single value (TopLeft, TopRight, BottomLeft, BottomRight, Center, or Top).\n" +
"A new Margin property is available to control the decoration offset relative to the edges of the selection box.\n" +
"RenderNameTag has been renamed to WithNameTagDecoration and now behaves like a normal decoration trait.\n";
}
}
static readonly string[] LegacyDecorationTraits = { "WithDecoration", "WithSpriteControlGroupDecoration", "WithTextControlGroupDecoration", "WithTextDecoration", "WithBuildingRepairDecoration", "InfiltrateForDecoration" };
static readonly string[] ModernDecorationTraits = { "WithAmmoPipsDecoration", "WithCargoPipsDecoration", "WithHarvesterPipsDecoration", "WithResourceStoragePipsDecoration", "WithNameTagDecoration" };
[Flags]
public enum LegacyReferencePoints
{
Center = 0,
Top = 1,
Bottom = 2,
Left = 4,
Right = 8,
}
static readonly Dictionary<LegacyReferencePoints, DecorationPosition> PositionMap = new Dictionary<LegacyReferencePoints, DecorationPosition>()
{
{ LegacyReferencePoints.Center, DecorationPosition.Center },
{ LegacyReferencePoints.Top, DecorationPosition.Top },
{ LegacyReferencePoints.Top | LegacyReferencePoints.Left, DecorationPosition.TopLeft },
{ LegacyReferencePoints.Top | LegacyReferencePoints.Right, DecorationPosition.TopRight },
{ LegacyReferencePoints.Bottom | LegacyReferencePoints.Left, DecorationPosition.BottomLeft },
{ LegacyReferencePoints.Bottom | LegacyReferencePoints.Right, DecorationPosition.BottomRight }
};
readonly Dictionary<string, List<string>> locations = new Dictionary<string, List<string>>();
public override IEnumerable<string> AfterUpdate(ModData modData)
{
if (locations.Any())
yield return "The way that decorations are positioned relative to the selection box has changed.\n" +
"Review the following definitions and define Margin properties as required:\n" +
UpdateUtils.FormatMessageList(locations.Select(
kv => kv.Key + ":\n" + UpdateUtils.FormatMessageList(kv.Value)));
locations.Clear();
}
public override IEnumerable<string> UpdateActorNode(ModData modData, MiniYamlNode actorNode)
{
var locationKey = "{0} ({1})".F(actorNode.Key, actorNode.Location.Filename);
foreach (var trait in LegacyDecorationTraits)
{
foreach (var node in actorNode.ChildrenMatching(trait))
{
node.RemoveNodes("ZOffset");
node.RemoveNodes("ScreenOffset");
var positionNode = node.LastChildMatching("ReferencePoint");
if (positionNode != null)
{
DecorationPosition value;
if (!PositionMap.TryGetValue(positionNode.NodeValue<LegacyReferencePoints>(), out value))
value = DecorationPosition.TopLeft;
if (value != DecorationPosition.TopLeft)
{
positionNode.RenameKey("Position");
positionNode.ReplaceValue(FieldSaver.FormatValue(value));
}
else
node.RemoveNode(positionNode);
}
locations.GetOrAdd(locationKey).Add(node.Key);
}
}
foreach (var trait in ModernDecorationTraits)
foreach (var node in actorNode.ChildrenMatching(trait))
locations.GetOrAdd(locationKey).Add(node.Key);
foreach (var selection in actorNode.ChildrenMatching("SelectionDecorations"))
{
selection.RemoveNodes("RenderSelectionBars");
selection.RemoveNodes("RenderSelectionBox");
}
foreach (var nameTag in actorNode.ChildrenMatching("RenderNameTag"))
{
nameTag.RenameKey("WithNameTagDecoration");
nameTag.AddNode("Position", "Top");
nameTag.AddNode("UsePlayerColor", "true");
locations.GetOrAdd(locationKey).Add(nameTag.Key);
}
yield break;
}
}
}

View File

@@ -61,9 +61,8 @@ namespace OpenRA.Mods.Common.UpdateRules
new ReplaceAttackTypeStrafe() new ReplaceAttackTypeStrafe()
}), }),
new UpdatePath("release-20200202", new UpdateRule[] new UpdatePath("release-20200202", "playtest-20200303", new UpdateRule[]
{ {
// Bleed only changes here
new RemoveYesNo(), new RemoveYesNo(),
new RemoveInitialFacingHardcoding(), new RemoveInitialFacingHardcoding(),
new RemoveAirdropActorTypeDefault(), new RemoveAirdropActorTypeDefault(),
@@ -74,6 +73,13 @@ namespace OpenRA.Mods.Common.UpdateRules
new RenameSpins(), new RenameSpins(),
new CreateScreenShakeWarhead(), new CreateScreenShakeWarhead(),
new RenameRallyPointPath(), new RenameRallyPointPath(),
}),
new UpdatePath("playtest-20200303", new UpdateRule[]
{
// Bleed only changes here
new AddPipDecorationTraits(),
new ModernizeDecorationTraits(),
}) })
}; };

View File

@@ -64,7 +64,8 @@ namespace OpenRA.Mods.Common.Widgets
if (selectionDecorations == null) if (selectionDecorations == null)
return; return;
selectionDecorations.DrawRollover(unit, worldRenderer); foreach (var r in selectionDecorations.RenderRolloverAnnotations(unit, worldRenderer))
r.PrepareRender(worldRenderer).Render(worldRenderer);
} }
public override void Draw() public override void Draw()