Implement isometric selection boxes for TS structures.

This commit is contained in:
Paul Chote
2020-03-21 16:00:52 +00:00
committed by atlimit8
parent 88cdad4189
commit 9f3254dbd1
14 changed files with 686 additions and 106 deletions

View File

@@ -0,0 +1,168 @@
#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.Traits;
namespace OpenRA.Mods.Common.Graphics
{
public struct IsometricSelectionBarsAnnotationRenderable : IRenderable, IFinalizedRenderable
{
const int BarWidth = 3;
const int BarHeight = 4;
const int BarStride = 5;
static readonly Color EmptyColor = Color.FromArgb(160, 30, 30, 30);
static readonly Color DarkEmptyColor = Color.FromArgb(160, 15, 15, 15);
static readonly Color DarkenColor = Color.FromArgb(24, 0, 0, 0);
static readonly Color LightenColor = Color.FromArgb(24, 255, 255, 255);
readonly WPos pos;
readonly Actor actor;
readonly bool displayHealth;
readonly bool displayExtra;
readonly Polygon bounds;
public IsometricSelectionBarsAnnotationRenderable(Actor actor, Polygon bounds, bool displayHealth, bool displayExtra)
: this(actor.CenterPosition, actor, bounds)
{
this.displayHealth = displayHealth;
this.displayExtra = displayExtra;
}
public IsometricSelectionBarsAnnotationRenderable(WPos pos, Actor actor, Polygon bounds)
: this()
{
this.pos = pos;
this.actor = actor;
this.bounds = bounds;
}
public WPos Pos { get { return pos; } }
public bool DisplayHealth { get { return displayHealth; } }
public bool DisplayExtra { get { return displayExtra; } }
public PaletteReference Palette { get { return null; } }
public int ZOffset { get { return 0; } }
public bool IsDecoration { get { return true; } }
public IRenderable WithPalette(PaletteReference newPalette) { return this; }
public IRenderable WithZOffset(int newOffset) { return this; }
public IRenderable OffsetBy(WVec vec) { return new IsometricSelectionBarsAnnotationRenderable(pos + vec, actor, bounds); }
public IRenderable AsDecoration() { return this; }
void DrawExtraBars(WorldRenderer wr)
{
var i = 1;
foreach (var extraBar in actor.TraitsImplementing<ISelectionBar>())
{
var value = extraBar.GetValue();
if (value != 0 || extraBar.DisplayWhenEmpty)
DrawBar(wr, extraBar.GetValue(), extraBar.GetColor(), i++);
}
}
void DrawBar(WorldRenderer wr, float value, Color barColor, int barNum, float? secondValue = null, Color? secondColor = null)
{
var darkColor = Color.FromArgb(barColor.A, barColor.R / 2, barColor.G / 2, barColor.B / 2);
var barAspect = new float2(1f, 0.5f);
var stepAspect = new float2(1f, -0.5f);
var offset = barNum * BarStride * barAspect - new float2(0, BarHeight + 1);
var start = wr.Viewport.WorldToViewPx(bounds.Vertices[1]).ToFloat2() + offset;
var end = wr.Viewport.WorldToViewPx(bounds.Vertices[0]).ToFloat2() + offset;
// HACK: Work around rounding errors that may cause a few-px offset in the end relative to the start
// Force the bar to take a 45 degree angle
end = new float2(end.X, start.Y - (end.X - start.X) / 2);
// Round the cut point to the nearest pixel to avoid potential off-by-one pixel offsets distorting the bar
var cutX = (int)(float2.Lerp(start.X, end.X, value) + 0.5f);
var cut = new float2(cutX, start.Y - (cutX - start.X) / 2);
var cr = Game.Renderer.RgbaColorRenderer;
var da = BarWidth * barAspect;
var db = new int2(0, BarHeight);
var dc = da + db;
// Filled bar
cr.FillRect(start + da, start + dc, cut + dc, cut + da, darkColor);
cr.FillRect(start, start + da, start + dc, start + db, darkColor);
cr.FillRect(start, start + da, cut + da, cut, barColor);
// Faint marks to break the monotony of the solid bar
var dx = BarWidth;
while (dx < cut.X - start.X)
{
var step = start + dx * stepAspect;
cr.DrawLine(step, step + da, 1, DarkenColor);
cr.DrawLine(step + da, step + dc, 1, LightenColor);
dx += BarWidth;
}
// Second bar (e.g. applied damage)
if (secondValue.HasValue && secondColor.HasValue)
{
var secondCutX = (int)(float2.Lerp(start.X, end.X, secondValue.Value) + 0.5f);
var secondCut = new float2(secondCutX, start.Y - (secondCutX - start.X) / 2);
var darkSecond = Color.FromArgb(secondColor.Value.A, secondColor.Value.R / 2, secondColor.Value.G / 2, secondColor.Value.B / 2);
cr.FillRect(cut + da, cut + dc, secondCut + dc, secondCut + da, darkSecond);
cr.FillRect(cut, cut + da, secondCut + da, secondCut, secondColor.Value);
value = secondValue.Value;
cut = secondCut;
}
// Empty bar
if (value < 1)
{
cr.FillRect(cut + da, cut + dc, end + dc, end + da, DarkEmptyColor);
cr.FillRect(cut, cut + da, end + da, end, EmptyColor);
}
}
Color GetHealthColor(IHealth health)
{
if (Game.Settings.Game.UsePlayerStanceColors)
return actor.Owner.PlayerStanceColor(actor);
return health.DamageState == DamageState.Critical ? Color.Red :
health.DamageState == DamageState.Heavy ? Color.Yellow : Color.LimeGreen;
}
public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
public void Render(WorldRenderer wr)
{
if (!actor.IsInWorld || actor.IsDead)
return;
var health = actor.TraitOrDefault<IHealth>();
if (DisplayHealth)
{
if (health == null || health.IsDead)
return;
var displayValue = health.DisplayHP != health.HP ? (float?)health.DisplayHP / health.MaxHP : null;
DrawBar(wr, (float)health.HP / health.MaxHP, GetHealthColor(health), 0, displayValue, Color.OrangeRed);
}
if (DisplayExtra)
DrawExtraBars(wr);
}
public void RenderDebugGeometry(WorldRenderer wr) { }
public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; }
}
}

View File

@@ -0,0 +1,83 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
namespace OpenRA.Mods.Common.Graphics
{
public struct IsometricSelectionBoxAnnotationRenderable : IRenderable, IFinalizedRenderable
{
static readonly float2 TLOffset = new float2(-12, -6);
static readonly float2 TROffset = new float2(12, -6);
static readonly float2 TOffset = new float2(0, -13);
static readonly float2[] Offsets =
{
-TROffset, -TLOffset, -TOffset,
TROffset, -TOffset, -TLOffset,
-TLOffset, TOffset, TROffset,
TLOffset, TROffset, TOffset,
-TROffset, TOffset, TLOffset,
TLOffset, -TOffset, -TROffset
};
readonly WPos pos;
readonly Polygon bounds;
readonly Color color;
public IsometricSelectionBoxAnnotationRenderable(Actor actor, Polygon bounds, Color color)
{
pos = actor.CenterPosition;
this.bounds = bounds;
this.color = color;
}
public IsometricSelectionBoxAnnotationRenderable(WPos pos, Polygon bounds, Color color)
{
this.pos = pos;
this.bounds = bounds;
this.color = color;
}
public WPos Pos { get { return pos; } }
public PaletteReference Palette { get { return null; } }
public int ZOffset { get { return 0; } }
public bool IsDecoration { get { return true; } }
public IRenderable WithPalette(PaletteReference newPalette) { return this; }
public IRenderable WithZOffset(int newOffset) { return this; }
public IRenderable OffsetBy(WVec vec) { return new IsometricSelectionBoxAnnotationRenderable(pos + vec, bounds, color); }
public IRenderable AsDecoration() { return this; }
public IFinalizedRenderable PrepareRender(WorldRenderer wr) { return this; }
public void Render(WorldRenderer wr)
{
var screen = bounds.Vertices.Select(v => wr.Viewport.WorldToViewPx(v).ToFloat2()).ToArray();
var tl = new float2(-12, -6);
var tr = new float2(12, -6);
var t = new float2(0, -13);
var cr = Game.Renderer.RgbaColorRenderer;
for (var i = 0; i < 6; i++)
{
cr.DrawLine(new float3[] { screen[i] + Offsets[3 * i], screen[i], screen[i] + Offsets[3 * i + 1] }, 1, color, true);
cr.DrawLine(new float3[] { screen[i], screen[i] + Offsets[3 * i + 2] }, 1, color, true);
}
}
public void RenderDebugGeometry(WorldRenderer wr) { }
public Rectangle ScreenBounds(WorldRenderer wr) { return Rectangle.Empty; }
}
}

View File

@@ -0,0 +1,142 @@
#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.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[Desc("This actor is selectable. Defines bounds of selectable area, selection class, selection priority and selection priority modifiers.")]
public class IsometricSelectableInfo : ITraitInfo, IMouseBoundsInfo, ISelectableInfo, IRulesetLoaded, Requires<BuildingInfo>
{
[Desc("Defines a custom rectangle for mouse interaction with the actor.",
"If null, the engine will guess an appropriate size based on the building's footprint.",
"The first two numbers define the width and depth of the footprint rectangle.",
"The (optional) second two numbers define an x and y offset from the actor center.")]
public readonly int[] Bounds = null;
[Desc("Height above the footprint for the top of the interaction rectangle.")]
public readonly int Height = 24;
[Desc("Defines a custom rectangle for Decorations (e.g. the selection box).",
"If null, Bounds will be used instead.")]
public readonly int[] DecorationBounds = null;
[Desc("Defines a custom height for Decorations (e.g. the selection box).",
"If < 0, Height will be used instead.",
"If Height is 0, this must be defined with a value greater than 0.")]
public readonly int DecorationHeight = -1;
public readonly int Priority = 10;
[Desc("Allow selection priority to be modified using a hotkey.",
"Valid values are None (priority is not affected by modifiers)",
"Ctrl (priority is raised when Ctrl pressed) and",
"Alt (priority is raised when Alt pressed).")]
public readonly SelectionPriorityModifiers PriorityModifiers = SelectionPriorityModifiers.None;
[Desc("All units having the same selection class specified will be selected with select-by-type commands (e.g. double-click). ",
"Defaults to the actor name when not defined or inherited.")]
public readonly string Class = null;
[VoiceReference]
public readonly string Voice = "Select";
public object Create(ActorInitializer init) { return new IsometricSelectable(init.Self, this); }
int ISelectableInfo.Priority { get { return Priority; } }
SelectionPriorityModifiers ISelectableInfo.PriorityModifiers { get { return PriorityModifiers; } }
string ISelectableInfo.Voice { get { return Voice; } }
public virtual void RulesetLoaded(Ruleset rules, ActorInfo ai)
{
var grid = Game.ModData.Manifest.Get<MapGrid>();
if (grid.Type != MapGridType.RectangularIsometric)
throw new YamlException("IsometricSelectable can only be used in mods that use the RectangularIsometric MapGrid type.");
if (Height == 0 && DecorationHeight <= 0)
throw new YamlException("DecorationHeight must be defined and greater than 0 if Height is 0.");
}
}
public class IsometricSelectable : IMouseBounds, ISelectable
{
readonly IsometricSelectableInfo info;
readonly string selectionClass = null;
readonly BuildingInfo buildingInfo;
public IsometricSelectable(Actor self, IsometricSelectableInfo info)
{
this.info = info;
selectionClass = string.IsNullOrEmpty(info.Class) ? self.Info.Name : info.Class;
buildingInfo = self.Info.TraitInfo<BuildingInfo>();
}
Polygon Bounds(Actor self, WorldRenderer wr, int[] bounds, int height)
{
int2 left, right, top, bottom;
if (bounds != null)
{
var offset = bounds.Length >= 4 ? new int2(bounds[2], bounds[3]) : int2.Zero;
var center = wr.ScreenPxPosition(self.CenterPosition) + offset;
left = center - new int2(bounds[0] / 2, 0);
right = left + new int2(bounds[0], 0);
top = center - new int2(0, bounds[1] / 2);
bottom = top + new int2(0, bounds[1]);
}
else
{
var xMin = int.MaxValue;
var xMax = int.MinValue;
var yMin = int.MaxValue;
var yMax = int.MinValue;
foreach (var c in buildingInfo.OccupiedTiles(self.Location))
{
xMin = Math.Min(xMin, c.X);
xMax = Math.Max(xMax, c.X);
yMin = Math.Min(yMin, c.Y);
yMax = Math.Max(yMax, c.Y);
}
left = wr.ScreenPxPosition(self.World.Map.CenterOfCell(new CPos(xMin, yMax)) - new WVec(768, 0, 0));
right = wr.ScreenPxPosition(self.World.Map.CenterOfCell(new CPos(xMax, yMin)) + new WVec(768, 0, 0));
top = wr.ScreenPxPosition(self.World.Map.CenterOfCell(new CPos(xMin, yMin)) - new WVec(0, 768, 0));
bottom = wr.ScreenPxPosition(self.World.Map.CenterOfCell(new CPos(xMax, yMax)) + new WVec(0, 768, 0));
}
if (height == 0)
return new Polygon(new[] { top, left, bottom, right });
var h = new int2(0, height);
return new Polygon(new[] { top - h, left - h, left, bottom, right, right - h });
}
public Polygon Bounds(Actor self, WorldRenderer wr)
{
return Bounds(self, wr, info.Bounds, info.Height);
}
public Polygon DecorationBounds(Actor self, WorldRenderer wr)
{
return Bounds(self, wr, info.DecorationBounds ?? info.Bounds, info.DecorationHeight >= 0 ? info.DecorationHeight : info.Height);
}
Polygon IMouseBounds.MouseoverBounds(Actor self, WorldRenderer wr)
{
return Bounds(self, wr);
}
string ISelectable.Class { get { return selectionClass; } }
}
}

View File

@@ -0,0 +1,65 @@
#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
{
public class IsometricSelectionDecorationsInfo : SelectionDecorationsBaseInfo, Requires<IsometricSelectableInfo>
{
public override object Create(ActorInitializer init) { return new IsometricSelectionDecorations(init.Self, this); }
}
public class IsometricSelectionDecorations : SelectionDecorationsBase
{
readonly IsometricSelectable selectable;
public IsometricSelectionDecorations(Actor self, IsometricSelectionDecorationsInfo info)
: base(info)
{
selectable = self.Trait<IsometricSelectable>();
}
protected override int2 GetDecorationPosition(Actor self, WorldRenderer wr, DecorationPosition pos)
{
var bounds = selectable.DecorationBounds(self, wr);
switch (pos)
{
case DecorationPosition.TopLeft: return bounds.Vertices[1];
case DecorationPosition.TopRight: return bounds.Vertices[5];
case DecorationPosition.BottomLeft: return bounds.Vertices[2];
case DecorationPosition.BottomRight: return bounds.Vertices[4];
case DecorationPosition.Top: return new int2((bounds.Vertices[1].X + bounds.Vertices[5].X) / 2, bounds.Vertices[1].Y);
default: return bounds.BoundingRect.TopLeft + new int2(bounds.BoundingRect.Size.Width / 2, bounds.BoundingRect.Size.Height / 2);
}
}
protected override IEnumerable<IRenderable> RenderSelectionBox(Actor self, WorldRenderer wr, Color color)
{
var bounds = selectable.DecorationBounds(self, wr);
yield return new IsometricSelectionBoxAnnotationRenderable(self, bounds, color);
}
protected override IEnumerable<IRenderable> RenderSelectionBars(Actor self, WorldRenderer wr, bool displayHealth, bool displayExtra)
{
if (!displayHealth && !displayExtra)
yield break;
var bounds = selectable.DecorationBounds(self, wr);
yield return new IsometricSelectionBarsAnnotationRenderable(self, bounds, displayHealth, displayExtra);
}
}
}

View File

@@ -0,0 +1,87 @@
#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.IO;
using System.Linq;
using OpenRA.Mods.Common.FileFormats;
using OpenRA.Mods.Common.Traits;
namespace OpenRA.Mods.Common.UpdateRules.Rules
{
public class CopyIsometricSelectableHeight : UpdateRule
{
public override string Name { get { return "Copy IsometricSelectable.Height from art*.ini definitions."; } }
public override string Description
{
get
{
return "Reads building Height entries art.ini/artfs.ini/artmd.ini from the current working directory\n" +
"and adds IsometricSelectable definitions to matching actors.";
}
}
static readonly string[] SourceFiles = { "art.ini", "artfs.ini", "artmd.ini" };
readonly Dictionary<string, int> selectionHeight = new Dictionary<string, int>();
bool complete;
public override IEnumerable<string> BeforeUpdate(ModData modData)
{
if (complete)
yield break;
var grid = Game.ModData.Manifest.Get<MapGrid>();
foreach (var filename in SourceFiles)
{
if (!File.Exists(filename))
continue;
var file = new IniFile(File.Open(filename, FileMode.Open));
foreach (var section in file.Sections)
{
if (!section.Contains("Height"))
continue;
selectionHeight[section.Name] = (int)(float.Parse(section.GetValue("Height", "1")) * grid.TileSize.Height);
}
}
}
public override IEnumerable<string> AfterUpdate(ModData modData)
{
// Rule only applies to the default ruleset - skip maps
complete = true;
yield break;
}
public override IEnumerable<string> UpdateActorNode(ModData modData, MiniYamlNode actorNode)
{
if (complete || actorNode.LastChildMatching("IsometricSelectable") != null)
yield break;
var height = 0;
if (!selectionHeight.TryGetValue(actorNode.Key.ToLowerInvariant(), out height))
yield break;
// Don't redefine the default value
if (height == 24)
yield break;
var selection = new MiniYamlNode("IsometricSelectable", "");
selection.AddNode("Height", FieldSaver.FormatValue(height));
actorNode.AddNode(selection);
}
}
}