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:
72
OpenRA.Mods.Common/Graphics/UITextRenderable.cs
Normal file
72
OpenRA.Mods.Common/Graphics/UITextRenderable.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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; } }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
145
OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs
Normal file
145
OpenRA.Mods.Common/Traits/Render/SelectionDecorationsBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
83
OpenRA.Mods.Common/Traits/Render/WithAmmoPipsDecoration.cs
Normal file
83
OpenRA.Mods.Common/Traits/Render/WithAmmoPipsDecoration.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs
Normal file
101
OpenRA.Mods.Common/Traits/Render/WithCargoPipsDecoration.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
147
OpenRA.Mods.Common/Traits/Render/WithDecorationBase.cs
Normal file
147
OpenRA.Mods.Common/Traits/Render/WithDecorationBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs
Normal file
84
OpenRA.Mods.Common/Traits/Render/WithNameTagDecoration.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,6 +15,7 @@ using OpenRA.Activities;
|
||||
using OpenRA.Graphics;
|
||||
using OpenRA.Mods.Common.Activities;
|
||||
using OpenRA.Mods.Common.Graphics;
|
||||
using OpenRA.Mods.Common.Traits.Render;
|
||||
using OpenRA.Primitives;
|
||||
using OpenRA.Traits;
|
||||
|
||||
@@ -642,4 +643,40 @@ namespace OpenRA.Mods.Common.Traits
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
273
OpenRA.Mods.Common/UpdateRules/Rules/AddPipDecorationTraits.cs
Normal file
273
OpenRA.Mods.Common/UpdateRules/Rules/AddPipDecorationTraits.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,9 +61,8 @@ namespace OpenRA.Mods.Common.UpdateRules
|
||||
new ReplaceAttackTypeStrafe()
|
||||
}),
|
||||
|
||||
new UpdatePath("release-20200202", new UpdateRule[]
|
||||
new UpdatePath("release-20200202", "playtest-20200303", new UpdateRule[]
|
||||
{
|
||||
// Bleed only changes here
|
||||
new RemoveYesNo(),
|
||||
new RemoveInitialFacingHardcoding(),
|
||||
new RemoveAirdropActorTypeDefault(),
|
||||
@@ -74,6 +73,13 @@ namespace OpenRA.Mods.Common.UpdateRules
|
||||
new RenameSpins(),
|
||||
new CreateScreenShakeWarhead(),
|
||||
new RenameRallyPointPath(),
|
||||
}),
|
||||
|
||||
new UpdatePath("playtest-20200303", new UpdateRule[]
|
||||
{
|
||||
// Bleed only changes here
|
||||
new AddPipDecorationTraits(),
|
||||
new ModernizeDecorationTraits(),
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@ namespace OpenRA.Mods.Common.Widgets
|
||||
if (selectionDecorations == null)
|
||||
return;
|
||||
|
||||
selectionDecorations.DrawRollover(unit, worldRenderer);
|
||||
foreach (var r in selectionDecorations.RenderRolloverAnnotations(unit, worldRenderer))
|
||||
r.PrepareRender(worldRenderer).Render(worldRenderer);
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
|
||||
Reference in New Issue
Block a user