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

@@ -30,15 +30,6 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Initial ammo the actor is created with. Defaults to Ammo.")]
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.")]
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 class AmmoPool : INotifyCreated, INotifyAttack, IPips, ISync
public class AmmoPool : INotifyCreated, INotifyAttack, ISync
{
public readonly AmmoPoolInfo Info;
readonly Stack<int> tokens = new Stack<int>();
@@ -126,14 +117,5 @@ namespace OpenRA.Mods.Common.Traits
while (CurrentAmmoCount < tokens.Count && tokens.Count > 0)
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.")]
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.")]
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 class Cargo : IPips, IIssueOrder, IResolveOrder, IOrderVoice, INotifyCreated, INotifyKilled,
public class Cargo : IIssueOrder, IResolveOrder, IOrderVoice, INotifyCreated, INotifyKilled,
INotifyOwnerChanged, INotifySold, INotifyActorDisposing, IIssueDeployOrder,
ITransformActorInitModifier
{
@@ -379,30 +376,6 @@ namespace OpenRA.Mods.Common.Traits
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)
{
cargo.Add(a);

View File

@@ -42,9 +42,6 @@ namespace OpenRA.Mods.Common.Traits
[Desc("How many bales can it dump at once.")]
public readonly int BaleUnloadAmount = 1;
[Desc("How many squares to show the fill level.")]
public readonly int PipCount = 7;
public readonly int HarvestFacings = 0;
[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 class Harvester : IIssueOrder, IResolveOrder, IPips, IOrderVoice,
public class Harvester : IIssueOrder, IResolveOrder, IOrderVoice,
ISpeedModifier, ISync, INotifyCreated
{
public readonly HarvesterInfo Info;
public readonly IReadOnlyDictionary<ResourceTypeInfo, int> Contents;
readonly Mobile mobile;
readonly ResourceLayer resLayer;
readonly ResourceClaimLayer claimLayer;
readonly Dictionary<ResourceTypeInfo, int> contents = new Dictionary<ResourceTypeInfo, int>();
INotifyHarvesterAction[] notifyHarvesterAction;
ConditionManager conditionManager;
int conditionToken = ConditionManager.InvalidConditionToken;
HarvesterResourceMultiplier[] resourceMultipliers;
@@ -127,6 +125,8 @@ namespace OpenRA.Mods.Common.Traits
public Harvester(Actor self, HarvesterInfo info)
{
Info = info;
Contents = new ReadOnlyDictionary<ResourceTypeInfo, int>(contents);
mobile = self.Trait<Mobile>();
resLayer = self.World.WorldActor.Trait<ResourceLayer>();
claimLayer = self.World.WorldActor.Trait<ResourceClaimLayer>();
@@ -134,7 +134,6 @@ namespace OpenRA.Mods.Common.Traits
void INotifyCreated.Created(Actor self)
{
notifyHarvesterAction = self.TraitsImplementing<INotifyHarvesterAction>().ToArray();
resourceMultipliers = self.TraitsImplementing<HarvesterResourceMultiplier>().ToArray();
conditionManager = self.TraitOrDefault<ConditionManager>();
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()
{
return 100 - (100 - Info.FullyLoadedSpeed) * contents.Values.Sum() / Info.Capacity;

View File

@@ -17,7 +17,7 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[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.",
"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 class Interactable : INotifyCreated, IMouseBounds, IDecorationBounds
public class Interactable : INotifyCreated, IMouseBounds
{
readonly InteractableInfo info;
IAutoMouseBounds[] autoBounds;
@@ -72,7 +72,7 @@ namespace OpenRA.Mods.Common.Traits
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);
}

View File

@@ -23,7 +23,10 @@ namespace OpenRA.Mods.Common.Traits
public class PassengerInfo : ITraitInfo, IObservesVariablesInfo
{
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;
[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
{
public class SelectionDecorationsInfo : ITraitInfo, Requires<IDecorationBoundsInfo>
public class SelectionDecorationsInfo : SelectionDecorationsBaseInfo, Requires<InteractableInfo>
{
[PaletteReference]
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 override 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!
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;
readonly Interactable interactable;
public SelectionDecorations(Actor self, SelectionDecorationsInfo info)
: base(info)
{
Info = info;
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
pipImages = new Animation(self.World, Info.Image);
interactable = self.Trait<Interactable>();
}
void INotifyCreated.Created(Actor self)
protected override int2 GetDecorationPosition(Actor self, WorldRenderer wr, DecorationPosition pos)
{
pipSources = self.TraitsImplementing<IPips>().ToArray();
}
IEnumerable<WPos> ActivityTargetPath(Actor self)
{
if (!self.IsInWorld || self.IsDead)
yield break;
var activity = self.CurrentActivity;
if (activity != null)
var bounds = interactable.DecorationBounds(self, wr);
switch (pos)
{
var targets = activity.GetTargets(self);
yield return self.CenterPosition;
foreach (var t in targets.Where(t => t.Type != TargetType.Invalid))
yield return t.CenterPosition;
case DecorationPosition.TopLeft: return bounds.TopLeft;
case DecorationPosition.TopRight: return bounds.TopRight;
case DecorationPosition.BottomLeft: return bounds.BottomLeft;
case DecorationPosition.BottomRight: return bounds.BottomRight;
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))
return Enumerable.Empty<IRenderable>();
return DrawDecorations(self, wr);
var bounds = interactable.DecorationBounds(self, wr);
yield return new SelectionBoxAnnotationRenderable(self, bounds, color);
}
bool IRenderAnnotations.SpatiallyPartitionable { get { return true; } }
IEnumerable<IRenderable> DrawDecorations(Actor self, WorldRenderer wr)
protected override IEnumerable<IRenderable> RenderSelectionBars(Actor self, WorldRenderer wr, bool displayHealth, bool displayExtra)
{
var selected = self.World.Selection.Contains(self);
var regularWorld = self.World.Type == WorldType.Regular;
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))
// Don't render the selection bars for non-selectable actors
if (!(interactable is Selectable) || (!displayHealth && !displayExtra))
yield break;
if (self.World.LocalPlayer != null && self.World.LocalPlayer.PlayerActor.Trait<DeveloperMode>().PathDebug)
yield return new TargetLineRenderable(ActivityTargetPath(self), Color.Green);
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();
var bounds = interactable.DecorationBounds(self, wr);
yield return new SelectionBarsAnnotationRenderable(self, bounds, displayHealth, displayExtra);
}
}
}

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.")]
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); }
}
@@ -77,18 +73,8 @@ namespace OpenRA.Mods.Common.Traits.Render
string IProductionIconOverlay.Palette { get { return info.Palette; } }
float2 IProductionIconOverlay.Offset(float2 iconSize)
{
float x = 0;
float y = 0;
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;
var x = (sprite.Size.X - iconSize.X) / 2;
var y = (sprite.Size.Y - iconSize.Y) / 2;
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
{
[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.")]
public class WithDecorationInfo : ConditionalTraitInfo, Requires<IDecorationBoundsInfo>
public class WithDecorationInfo : WithDecorationBaseInfo
{
[FieldLoader.Require]
[Desc("Image used for this decoration. Defaults to the actor's type.")]
public readonly string Image = null;
[FieldLoader.Require]
[SequenceReference("Image")]
[Desc("Sequence used for this decoration (can be animated).")]
public readonly string Sequence = null;
@@ -46,77 +37,20 @@ namespace OpenRA.Mods.Common.Traits.Render
[Desc("Custom palette is a player palette BaseName")]
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 class WithDecoration : ConditionalTrait<WithDecorationInfo>, ITick, IRenderAnnotations, IRenderAnnotationsWhenSelected
public class WithDecoration : WithDecorationBase<WithDecorationInfo>, ITick
{
protected Animation anim;
readonly IDecorationBounds[] decorationBounds;
readonly string image;
int2 conditionalOffset;
BlinkState[] blinkPattern;
public WithDecoration(Actor self, WithDecorationInfo info)
: base(info)
: base(self, info)
{
image = info.Image ?? self.Info.Name;
anim = new Animation(self.World, image, () => self.World.Paused);
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)
@@ -124,99 +58,17 @@ namespace OpenRA.Mods.Common.Traits.Render
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;
}
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)
if (anim == null)
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[]
{
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(); }
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
{
[Desc("Renders Ctrl groups using pixel art.")]
public class WithSpriteControlGroupDecorationInfo : ITraitInfo, Requires<IDecorationBoundsInfo>
public class WithSpriteControlGroupDecorationInfo : ITraitInfo
{
[PaletteReference]
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.")]
public readonly string GroupSequence = "groups";
[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("Position in the actor's selection box to draw the decoration.")]
public readonly DecorationPosition Position = DecorationPosition.TopLeft;
[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 class WithSpriteControlGroupDecoration : IRenderAnnotationsWhenSelected
public class WithSpriteControlGroupDecoration : IDecoration
{
public readonly WithSpriteControlGroupDecorationInfo Info;
readonly IDecorationBounds[] decorationBounds;
readonly Animation pipImages;
readonly Actor self;
readonly Animation anim;
public WithSpriteControlGroupDecoration(Actor self, WithSpriteControlGroupDecorationInfo info)
{
Info = info;
this.self = self;
decorationBounds = self.TraitsImplementing<IDecorationBounds>().ToArray();
pipImages = new Animation(self.World, Info.Image);
anim = new Animation(self.World, Info.Image);
}
IEnumerable<IRenderable> IRenderAnnotationsWhenSelected.RenderAnnotations(Actor self, WorldRenderer wr)
{
if (self.Owner != wr.World.LocalPlayer)
yield break;
DecorationPosition IDecoration.Position { get { return Info.Position; } }
if (self.World.FogObscures(self))
yield break;
bool IDecoration.Enabled { get { return self.Owner == self.World.LocalPlayer && self.World.Selection.GetControlGroupForActor(self) != null; } }
var pal = wr.Palette(Info.Palette);
foreach (var r in DrawControlGroup(self, wr, pal))
yield return r;
}
bool IDecoration.RequiresSelection { get { return true; } }
bool IRenderAnnotationsWhenSelected.SpatiallyPartitionable { get { return true; } }
IEnumerable<IRenderable> DrawControlGroup(Actor self, WorldRenderer wr, PaletteReference palette)
IEnumerable<IRenderable> IDecoration.RenderDecoration(Actor self, WorldRenderer wr, int2 pos)
{
var group = self.World.Selection.GetControlGroupForActor(self);
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 boundsOffset = 0.5f * new float2(bounds.Left + bounds.Right, bounds.Top + bounds.Bottom);
if (Info.ReferencePoint.HasFlag(ReferencePoints.Top))
boundsOffset -= new float2(0, 0.5f * bounds.Height);
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);
var screenPos = wr.Viewport.WorldToViewPx(pos) + Info.Position.CreateMargin(Info.Margin) - (0.5f * anim.Image.Size.XY).ToInt2();
var palette = wr.Palette(Info.Palette);
return new IRenderable[]
{
new UISpriteRenderable(anim.Image, self.CenterPosition, screenPos, 0, palette, 1f)
};
}
}
}

View File

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

View File

@@ -19,7 +19,7 @@ using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits.Render
{
[Desc("Displays a text overlay relative to the selection box.")]
public class WithTextDecorationInfo : ConditionalTraitInfo, Requires<IDecorationBoundsInfo>
public class WithTextDecorationInfo : WithDecorationBaseInfo
{
[Translate]
[FieldLoader.Require]
@@ -33,19 +33,6 @@ namespace OpenRA.Mods.Common.Traits.Render
[Desc("Use the player color of the current owner.")]
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 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 IDecorationBounds[] decorationBounds;
Color color;
public WithTextDecoration(Actor self, WithTextDecorationInfo info)
: base(info)
: base(self, info)
{
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; }
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;
}
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)
if (IsTraitDisabled || self.IsDead || !self.IsInWorld || !ShouldRender(self))
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];
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) };
new UITextRenderable(font, self.CenterPosition, screenPos - size / 2, 0, color, Info.Text)
};
}
void INotifyOwnerChanged.OnOwnerChanged(Actor self, Player oldOwner, Player newOwner)

View File

@@ -22,16 +22,10 @@ namespace OpenRA.Mods.Common.Traits
[FieldLoader.Require]
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 class StoresResources : IPips, INotifyOwnerChanged, INotifyCapture, IStoreResources, ISync, INotifyKilled, INotifyAddedToWorld, INotifyRemovedFromWorld
public class StoresResources : INotifyOwnerChanged, INotifyCapture, IStoreResources, ISync, INotifyKilled, INotifyAddedToWorld, INotifyRemovedFromWorld
{
readonly StoresResourcesInfo info;
PlayerResources player;
@@ -65,13 +59,6 @@ namespace OpenRA.Mods.Common.Traits
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)
{
player.AddStorage(info.Capacity);

View File

@@ -146,8 +146,9 @@ namespace OpenRA.Mods.Common.Traits
var xy = wr.Viewport.ViewToWorld(Viewport.LastMousePos);
foreach (var unit in power.UnitsInRange(xy))
{
var bounds = unit.TraitsImplementing<IDecorationBounds>().FirstNonEmptyBounds(unit, wr);
yield return new SelectionBoxAnnotationRenderable(unit, bounds, Color.Red);
var decorations = unit.TraitsImplementing<ISelectionDecorations>().FirstEnabledTraitOrDefault();
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.")]
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)
{
var tileSet = map.Rules.TileSet;