Various item status fixes/tweaks (#27267)

* Always display item status panel fully

Initial feedback from the UI changes seems to be that a lot of people go "why is there empty space" so let's fix that.

* Fix item status middle hand being on the wrong side

I think I switched this around when fixing the left/right being inverted in the UI code.

* Minor status panel UI tweaks

Bottom-align contents now that the panel itself doesn't dynamically expand, prevent weird gaps.

Clip contents for panel

* Fix clipping on implanters and network configurators.

Made them take less space. For implanters the name has to be cut off, which I did by adding a new ClipControl to achieve that in rich text.

* Update visibility of item status panels based on whether you have hands at all.

This avoids UI for borgs looking silly.

Added a new "HandUILocation" enum that doesn't have middle hands to avoid confusion in UI code.

* Use BulletRender for laser guns too.

Provides all the benefits like fixing layout overflow and allowing multi-line stuff. Looks great now.

This involved generalizing BulletRender a bit so it can be used for not-just-bullets.

* Fix geiger word wrapping if you're really fucked
This commit is contained in:
Pieter-Jan Briers
2024-04-24 16:01:31 +02:00
committed by GitHub
parent 38f490e5eb
commit 7b90c08a2c
16 changed files with 338 additions and 185 deletions

View File

@@ -1,5 +1,6 @@
using Content.Client.Message; using Content.Client.Message;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Implants.Components; using Content.Shared.Implants.Components;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
@@ -17,7 +18,7 @@ public sealed class ImplanterStatusControl : Control
_parent = parent; _parent = parent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
_label.MaxWidth = 350; _label.MaxWidth = 350;
AddChild(_label); AddChild(new ClipControl { Children = { _label } });
Update(); Update();
} }
@@ -42,17 +43,12 @@ public sealed class ImplanterStatusControl : Control
_ => Loc.GetString("injector-invalid-injector-toggle-mode") _ => Loc.GetString("injector-invalid-injector-toggle-mode")
}; };
var (implantName, implantDescription) = _parent.ImplanterSlot.HasItem switch var implantName = _parent.ImplanterSlot.HasItem
{ ? _parent.ImplantData.Item1
false => (Loc.GetString("implanter-empty-text"), ""), : Loc.GetString("implanter-empty-text");
true => (_parent.ImplantData.Item1, _parent.ImplantData.Item2),
};
_label.SetMarkup(Loc.GetString("implanter-label", _label.SetMarkup(Loc.GetString("implanter-label",
("implantName", implantName), ("implantName", implantName),
("implantDescription", implantDescription), ("modeString", modeStringLocalized)));
("modeString", modeStringLocalized),
("lineBreak", "\n")));
} }
} }

View File

@@ -136,6 +136,8 @@ namespace Content.Client.Stylesheets
public const string StyleClassPowerStateGood = "PowerStateGood"; public const string StyleClassPowerStateGood = "PowerStateGood";
public const string StyleClassItemStatus = "ItemStatus"; public const string StyleClassItemStatus = "ItemStatus";
public const string StyleClassItemStatusNotHeld = "ItemStatusNotHeld";
public static readonly Color ItemStatusNotHeldColor = Color.Gray;
//Background //Background
public const string StyleClassBackgroundBaseDark = "PanelBackgroundBaseDark"; public const string StyleClassBackgroundBaseDark = "PanelBackgroundBaseDark";
@@ -1234,6 +1236,11 @@ namespace Content.Client.Stylesheets
new StyleProperty("font", notoSans10), new StyleProperty("font", notoSans10),
}), }),
Element()
.Class(StyleClassItemStatusNotHeld)
.Prop("font", notoSansItalic10)
.Prop("font-color", ItemStatusNotHeldColor),
Element<RichTextLabel>() Element<RichTextLabel>()
.Class(StyleClassItemStatus) .Class(StyleClassItemStatus)
.Prop(nameof(RichTextLabel.LineHeightScale), 0.7f) .Prop(nameof(RichTextLabel.LineHeightScale), 0.7f)

View File

@@ -0,0 +1,55 @@
using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// Pretends to child controls that there's infinite space.
/// This can be used to make something like a <see cref="RichTextLabel"/> clip instead of wrapping.
/// </summary>
public sealed class ClipControl : Control
{
private bool _clipHorizontal = true;
private bool _clipVertical = true;
public bool ClipHorizontal
{
get => _clipHorizontal;
set
{
_clipHorizontal = value;
InvalidateMeasure();
}
}
public bool ClipVertical
{
get => _clipVertical;
set
{
_clipVertical = value;
InvalidateMeasure();
}
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
{
if (ClipHorizontal)
availableSize = availableSize with { X = float.PositiveInfinity };
if (ClipVertical)
availableSize = availableSize with { Y = float.PositiveInfinity };
return base.MeasureOverride(availableSize);
}
protected override Vector2 ArrangeOverride(Vector2 finalSize)
{
foreach (var child in Children)
{
child.Arrange(UIBox2.FromDimensions(Vector2.Zero, child.DesiredSize));
}
return finalSize;
}
}

View File

@@ -5,8 +5,11 @@ namespace Content.Client.UserInterface.Systems.Hands.Controls;
public sealed class HandButton : SlotControl public sealed class HandButton : SlotControl
{ {
public HandLocation HandLocation { get; }
public HandButton(string handName, HandLocation handLocation) public HandButton(string handName, HandLocation handLocation)
{ {
HandLocation = handLocation;
Name = "hand_" + handName; Name = "hand_" + handName;
SlotName = handName; SlotName = handName;
SetBackground(handLocation); SetBackground(handLocation);

View File

@@ -256,7 +256,8 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_player.LocalSession?.AttachedEntity is { } playerEntity && _player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand(playerEntity, handName, out var hand, _playerHandsComponent)) _handsSystem.TryGetHand(playerEntity, handName, out var hand, _playerHandsComponent))
{ {
if (hand.Location == HandLocation.Left) var foldedLocation = hand.Location.GetUILocation();
if (foldedLocation == HandUILocation.Left)
{ {
_statusHandLeft = handControl; _statusHandLeft = handControl;
HandsGui.UpdatePanelEntityLeft(hand.HeldEntity); HandsGui.UpdatePanelEntityLeft(hand.HeldEntity);
@@ -268,7 +269,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
HandsGui.UpdatePanelEntityRight(hand.HeldEntity); HandsGui.UpdatePanelEntityRight(hand.HeldEntity);
} }
HandsGui.SetHighlightHand(hand.Location); HandsGui.SetHighlightHand(foldedLocation);
} }
} }
@@ -299,11 +300,13 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
// If we don't have a status for this hand type yet, set it. // If we don't have a status for this hand type yet, set it.
// This means we have status filled by default in most scenarios, // This means we have status filled by default in most scenarios,
// otherwise the user'd need to switch hands to "activate" the hands the first time. // otherwise the user'd need to switch hands to "activate" the hands the first time.
if (location == HandLocation.Left) if (location.GetUILocation() == HandUILocation.Left)
_statusHandLeft ??= button; _statusHandLeft ??= button;
else else
_statusHandRight ??= button; _statusHandRight ??= button;
UpdateVisibleStatusPanels();
return button; return button;
} }
@@ -369,9 +372,30 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_handLookup.Remove(handName); _handLookup.Remove(handName);
handButton.Dispose(); handButton.Dispose();
UpdateVisibleStatusPanels();
return true; return true;
} }
private void UpdateVisibleStatusPanels()
{
var leftVisible = false;
var rightVisible = false;
foreach (var hand in _handLookup.Values)
{
if (hand.HandLocation.GetUILocation() == HandUILocation.Left)
{
leftVisible = true;
}
else
{
rightVisible = true;
}
}
HandsGui?.UpdateStatusVisibility(leftVisible, rightVisible);
}
public string RegisterHandContainer(HandsContainer handContainer) public string RegisterHandContainer(HandsContainer handContainer)
{ {
var name = "HandContainer_" + _backupSuffix; var name = "HandContainer_" + _backupSuffix;

View File

@@ -32,7 +32,7 @@
Name="StatusPanelRight" Name="StatusPanelRight"
HorizontalAlignment="Center" Margin="0 0 -2 2" HorizontalAlignment="Center" Margin="0 0 -2 2"
SetWidth="125" SetWidth="125"
MaxHeight="60"/> SetHeight="60"/>
<hands:HandsContainer <hands:HandsContainer
Name="HandContainer" Name="HandContainer"
Access="Public" Access="Public"
@@ -43,7 +43,7 @@
Name="StatusPanelLeft" Name="StatusPanelLeft"
HorizontalAlignment="Center" Margin="-2 0 0 2" HorizontalAlignment="Center" Margin="-2 0 0 2"
SetWidth="125" SetWidth="125"
MaxHeight="60"/> SetHeight="60"/>
<inventory:ItemSlotButtonContainer <inventory:ItemSlotButtonContainer
Name="MainHotbar" Name="MainHotbar"
SlotGroup="MainHotbar" SlotGroup="MainHotbar"

View File

@@ -11,8 +11,8 @@ public sealed partial class HotbarGui : UIWidget
public HotbarGui() public HotbarGui()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
StatusPanelRight.SetSide(HandLocation.Right); StatusPanelRight.SetSide(HandUILocation.Right);
StatusPanelLeft.SetSide(HandLocation.Left); StatusPanelLeft.SetSide(HandUILocation.Left);
var hotbarController = UserInterfaceManager.GetUIController<HotbarUIController>(); var hotbarController = UserInterfaceManager.GetUIController<HotbarUIController>();
hotbarController.Setup(HandContainer, StoragePanel); hotbarController.Setup(HandContainer, StoragePanel);
@@ -29,9 +29,15 @@ public sealed partial class HotbarGui : UIWidget
StatusPanelRight.Update(entity); StatusPanelRight.Update(entity);
} }
public void SetHighlightHand(HandLocation? hand) public void SetHighlightHand(HandUILocation? hand)
{ {
StatusPanelLeft.UpdateHighlight(hand is HandLocation.Left); StatusPanelLeft.UpdateHighlight(hand is HandUILocation.Left);
StatusPanelRight.UpdateHighlight(hand is HandLocation.Middle or HandLocation.Right); StatusPanelRight.UpdateHighlight(hand is HandUILocation.Right);
}
public void UpdateStatusVisibility(bool left, bool right)
{
StatusPanelLeft.Visible = left;
StatusPanelRight.Visible = right;
} }
} }

View File

@@ -4,25 +4,26 @@
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client" xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
HorizontalAlignment="Center"> HorizontalAlignment="Center">
<Control Name="VisWrapper" Visible="False"> <PanelContainer Name="Panel">
<PanelContainer Name="Panel"> <PanelContainer.PanelOverride>
<PanelContainer.PanelOverride> <graphics:StyleBoxTexture
<graphics:StyleBoxTexture PatchMarginBottom="4"
PatchMarginBottom="4" PatchMarginTop="6"
PatchMarginTop="6" TextureScale="2 2"
TextureScale="2 2" Mode="Tile"/>
Mode="Tile"/> </PanelContainer.PanelOverride>
</PanelContainer.PanelOverride> </PanelContainer>
</PanelContainer> <PanelContainer Name="HighlightPanel">
<PanelContainer Name="HighlightPanel"> <PanelContainer.PanelOverride>
<PanelContainer.PanelOverride> <graphics:StyleBoxTexture PatchMarginBottom="4" PatchMarginTop="6" TextureScale="2 2">
<graphics:StyleBoxTexture PatchMarginBottom="4" PatchMarginTop="6" TextureScale="2 2"> </graphics:StyleBoxTexture>
</graphics:StyleBoxTexture> </PanelContainer.PanelOverride>
</PanelContainer.PanelOverride> </PanelContainer>
</PanelContainer> <BoxContainer Name="Contents" Orientation="Vertical" Margin="0 6 0 4" RectClipContent="True">
<BoxContainer Name="Contents" Orientation="Vertical" Margin="0 6 0 4"> <BoxContainer Name="StatusContents" Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Bottom" />
<BoxContainer Name="StatusContents" Orientation="Vertical" /> <Control>
<Label Name="ItemNameLabel" ClipText="True" StyleClasses="ItemStatus" Align="Left" /> <Label Name="NoItemLabel" ClipText="True" StyleClasses="ItemStatusNotHeld" Align="Left" Text="{Loc 'item-status-not-held'}" />
</BoxContainer> <Label Name="ItemNameLabel" ClipText="True" StyleClasses="ItemStatus" Align="Left" Visible="False" />
</Control> </Control>
</BoxContainer>
</controls:ItemStatusPanel> </controls:ItemStatusPanel>

View File

@@ -1,17 +1,13 @@
using System.Numerics;
using Content.Client.Items; using Content.Client.Items;
using Content.Client.Resources;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Inventory.VirtualItem; using Content.Shared.Inventory.VirtualItem;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using static Content.Client.IoC.StaticIoC;
namespace Content.Client.UserInterface.Systems.Inventory.Controls; namespace Content.Client.UserInterface.Systems.Inventory.Controls;
@@ -23,17 +19,15 @@ public sealed partial class ItemStatusPanel : Control
[ViewVariables] private EntityUid? _entity; [ViewVariables] private EntityUid? _entity;
// Tracked so we can re-run SetSide() if the theme changes. // Tracked so we can re-run SetSide() if the theme changes.
private HandLocation _side; private HandUILocation _side;
public ItemStatusPanel() public ItemStatusPanel()
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
SetSide(HandLocation.Middle);
} }
public void SetSide(HandLocation location) public void SetSide(HandUILocation location)
{ {
// AN IMPORTANT REMINDER ABOUT THIS CODE: // AN IMPORTANT REMINDER ABOUT THIS CODE:
// In the UI, the RIGHT hand is on the LEFT on the screen. // In the UI, the RIGHT hand is on the LEFT on the screen.
@@ -47,15 +41,14 @@ public sealed partial class ItemStatusPanel : Control
switch (location) switch (location)
{ {
case HandLocation.Right: case HandUILocation.Right:
texture = Theme.ResolveTexture("item_status_right"); texture = Theme.ResolveTexture("item_status_right");
textureHighlight = Theme.ResolveTexture("item_status_right_highlight"); textureHighlight = Theme.ResolveTexture("item_status_right_highlight");
cutOut = StyleBox.Margin.Left; cutOut = StyleBox.Margin.Left;
flat = StyleBox.Margin.Right; flat = StyleBox.Margin.Right;
contentMargin = MarginFromThemeColor("_itemstatus_content_margin_right"); contentMargin = MarginFromThemeColor("_itemstatus_content_margin_right");
break; break;
case HandLocation.Middle: case HandUILocation.Left:
case HandLocation.Left:
texture = Theme.ResolveTexture("item_status_left"); texture = Theme.ResolveTexture("item_status_left");
textureHighlight = Theme.ResolveTexture("item_status_left_highlight"); textureHighlight = Theme.ResolveTexture("item_status_left_highlight");
cutOut = StyleBox.Margin.Right; cutOut = StyleBox.Margin.Right;
@@ -104,11 +97,14 @@ public sealed partial class ItemStatusPanel : Control
public void Update(EntityUid? entity) public void Update(EntityUid? entity)
{ {
ItemNameLabel.Visible = entity != null;
NoItemLabel.Visible = entity == null;
if (entity == null) if (entity == null)
{ {
ItemNameLabel.Text = "";
ClearOldStatus(); ClearOldStatus();
_entity = null; _entity = null;
VisWrapper.Visible = false;
return; return;
} }
@@ -119,8 +115,6 @@ public sealed partial class ItemStatusPanel : Control
UpdateItemName(); UpdateItemName();
} }
VisWrapper.Visible = true;
} }
public void UpdateHighlight(bool highlight) public void UpdateHighlight(bool highlight)

View File

@@ -6,40 +6,10 @@ using Robust.Client.UserInterface;
namespace Content.Client.Weapons.Ranged.ItemStatus; namespace Content.Client.Weapons.Ranged.ItemStatus;
/// <summary> public abstract class BaseBulletRenderer : Control
/// Renders one or more rows of bullets for item status.
/// </summary>
/// <remarks>
/// This is a custom control to allow complex responsive layout logic.
/// </remarks>
public sealed class BulletRender : Control
{ {
private static readonly Color ColorA = Color.FromHex("#b68f0e");
private static readonly Color ColorB = Color.FromHex("#d7df60");
private static readonly Color ColorGoneA = Color.FromHex("#000000");
private static readonly Color ColorGoneB = Color.FromHex("#222222");
/// <summary>
/// Try to ensure there's at least this many bullets on one row.
/// </summary>
/// <remarks>
/// For example, if there are two rows and the second row has only two bullets,
/// we "steal" some bullets from the row below it to make it look nicer.
/// </remarks>
public const int MinCountPerRow = 7;
public const int BulletHeight = 12;
public const int BulletSeparationNormal = 3;
public const int BulletSeparationTiny = 2;
public const int BulletWidthNormal = 5;
public const int BulletWidthTiny = 2;
public const int VerticalSeparation = 2;
private readonly Texture _bulletTiny;
private readonly Texture _bulletNormal;
private int _capacity; private int _capacity;
private BulletType _type = BulletType.Normal; private LayoutParameters _params;
public int Rows { get; set; } = 2; public int Rows { get; set; } = 2;
public int Count { get; set; } public int Count { get; set; }
@@ -49,35 +19,31 @@ public sealed class BulletRender : Control
get => _capacity; get => _capacity;
set set
{ {
if (_capacity == value)
return;
_capacity = value; _capacity = value;
InvalidateMeasure(); InvalidateMeasure();
} }
} }
public BulletType Type protected LayoutParameters Parameters
{ {
get => _type; get => _params;
set set
{ {
_type = value; _params = value;
InvalidateMeasure(); InvalidateMeasure();
} }
} }
public BulletRender()
{
var resC = IoCManager.Resolve<IResourceCache>();
_bulletTiny = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/tiny.png");
_bulletNormal = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/normal.png");
}
protected override Vector2 MeasureOverride(Vector2 availableSize) protected override Vector2 MeasureOverride(Vector2 availableSize)
{ {
var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X)); var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X));
var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows); var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows);
var height = BulletHeight * rows + (BulletSeparationNormal * rows - 1); var height = _params.ItemHeight * rows + (_params.VerticalSeparation * rows - 1);
var width = RowWidth(countPerRow); var width = RowWidth(countPerRow);
return new Vector2(width, height); return new Vector2(width, height);
@@ -91,13 +57,8 @@ public sealed class BulletRender : Control
var countPerRow = CountPerRow(Size.X); var countPerRow = CountPerRow(Size.X);
var (separation, _) = BulletParams();
var texture = Type == BulletType.Normal ? _bulletNormal : _bulletTiny;
var pos = new Vector2(); var pos = new Vector2();
var altColor = false;
var spent = Capacity - Count; var spent = Capacity - Count;
var bulletsDone = 0; var bulletsDone = 0;
@@ -105,7 +66,7 @@ public sealed class BulletRender : Control
// Draw by rows, bottom to top. // Draw by rows, bottom to top.
for (var row = 0; row < Rows; row++) for (var row = 0; row < Rows; row++)
{ {
altColor = false; var altColor = false;
var thisRowCount = Math.Min(countPerRow, Capacity - bulletsDone); var thisRowCount = Math.Min(countPerRow, Capacity - bulletsDone);
if (thisRowCount <= 0) if (thisRowCount <= 0)
@@ -116,9 +77,10 @@ public sealed class BulletRender : Control
// 1. The next row would have less than MinCountPerRow bullets. // 1. The next row would have less than MinCountPerRow bullets.
// 2. The next row is actually visible (we aren't the last row). // 2. The next row is actually visible (we aren't the last row).
// 3. MinCountPerRow is actually smaller than the count per row (avoid degenerate cases). // 3. MinCountPerRow is actually smaller than the count per row (avoid degenerate cases).
// 4. There's enough bullets that at least one will end up on the next row.
var nextRowCount = Capacity - bulletsDone - thisRowCount; var nextRowCount = Capacity - bulletsDone - thisRowCount;
if (nextRowCount < MinCountPerRow && row != Rows - 1 && MinCountPerRow < countPerRow) if (nextRowCount < _params.MinCountPerRow && row != Rows - 1 && _params.MinCountPerRow < countPerRow && nextRowCount > 0)
thisRowCount -= MinCountPerRow - nextRowCount; thisRowCount -= _params.MinCountPerRow - nextRowCount;
// Account for row width to right-align. // Account for row width to right-align.
var rowWidth = RowWidth(thisRowCount); var rowWidth = RowWidth(thisRowCount);
@@ -128,46 +90,130 @@ public sealed class BulletRender : Control
for (var bullet = 0; bullet < thisRowCount; bullet++) for (var bullet = 0; bullet < thisRowCount; bullet++)
{ {
var absIdx = Capacity - bulletsDone - thisRowCount + bullet; var absIdx = Capacity - bulletsDone - thisRowCount + bullet;
Color color;
if (absIdx >= spent)
color = altColor ? ColorA : ColorB;
else
color = altColor ? ColorGoneA : ColorGoneB;
var renderPos = pos; var renderPos = pos;
renderPos.Y = Size.Y - renderPos.Y - BulletHeight; renderPos.Y = Size.Y - renderPos.Y - _params.ItemHeight;
handle.DrawTexture(texture, renderPos, color);
pos.X += separation; DrawItem(handle, renderPos, absIdx < spent, altColor);
pos.X += _params.ItemSeparation;
altColor ^= true; altColor ^= true;
} }
bulletsDone += thisRowCount; bulletsDone += thisRowCount;
pos.X = 0; pos.X = 0;
pos.Y += BulletHeight + VerticalSeparation; pos.Y += _params.ItemHeight + _params.VerticalSeparation;
} }
} }
protected abstract void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor);
private int CountPerRow(float width) private int CountPerRow(float width)
{ {
var (separation, bulletWidth) = BulletParams(); return (int) ((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation);
return (int) ((width - bulletWidth + separation) / separation);
}
private (int separation, int width) BulletParams()
{
return Type switch
{
BulletType.Normal => (BulletSeparationNormal, BulletWidthNormal),
BulletType.Tiny => (BulletSeparationTiny, BulletWidthTiny),
_ => throw new ArgumentOutOfRangeException()
};
} }
private int RowWidth(int count) private int RowWidth(int count)
{ {
var (separation, bulletWidth) = BulletParams(); return (count - 1) * _params.ItemSeparation + _params.ItemWidth;
}
return (count - 1) * separation + bulletWidth; protected struct LayoutParameters
{
public int ItemHeight;
public int ItemSeparation;
public int ItemWidth;
public int VerticalSeparation;
/// <summary>
/// Try to ensure there's at least this many bullets on one row.
/// </summary>
/// <remarks>
/// For example, if there are two rows and the second row has only two bullets,
/// we "steal" some bullets from the row below it to make it look nicer.
/// </remarks>
public int MinCountPerRow;
}
}
/// <summary>
/// Renders one or more rows of bullets for item status.
/// </summary>
/// <remarks>
/// This is a custom control to allow complex responsive layout logic.
/// </remarks>
public sealed class BulletRender : BaseBulletRenderer
{
public const int MinCountPerRow = 7;
public const int BulletHeight = 12;
public const int VerticalSeparation = 2;
private static readonly LayoutParameters LayoutNormal = new LayoutParameters
{
ItemHeight = BulletHeight,
ItemSeparation = 3,
ItemWidth = 5,
VerticalSeparation = VerticalSeparation,
MinCountPerRow = MinCountPerRow
};
private static readonly LayoutParameters LayoutTiny = new LayoutParameters
{
ItemHeight = BulletHeight,
ItemSeparation = 2,
ItemWidth = 2,
VerticalSeparation = VerticalSeparation,
MinCountPerRow = MinCountPerRow
};
private static readonly Color ColorA = Color.FromHex("#b68f0e");
private static readonly Color ColorB = Color.FromHex("#d7df60");
private static readonly Color ColorGoneA = Color.FromHex("#000000");
private static readonly Color ColorGoneB = Color.FromHex("#222222");
private readonly Texture _bulletTiny;
private readonly Texture _bulletNormal;
private BulletType _type = BulletType.Normal;
public BulletType Type
{
get => _type;
set
{
if (_type == value)
return;
Parameters = _type switch
{
BulletType.Normal => LayoutNormal,
BulletType.Tiny => LayoutTiny,
_ => throw new ArgumentOutOfRangeException()
};
_type = value;
}
}
public BulletRender()
{
var resC = IoCManager.Resolve<IResourceCache>();
_bulletTiny = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/tiny.png");
_bulletNormal = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/normal.png");
Parameters = LayoutNormal;
}
protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor)
{
Color color;
if (spent)
color = altColor ? ColorGoneA : ColorGoneB;
else
color = altColor ? ColorA : ColorB;
var texture = _type == BulletType.Tiny ? _bulletTiny : _bulletNormal;
handle.DrawTexture(texture, renderPos, color);
} }
public enum BulletType public enum BulletType
@@ -176,3 +222,31 @@ public sealed class BulletRender : Control
Tiny Tiny
} }
} }
public sealed class BatteryBulletRenderer : BaseBulletRenderer
{
private static readonly Color ItemColor = Color.FromHex("#E00000");
private static readonly Color ItemColorGone = Color.Black;
private const int SizeH = 10;
private const int SizeV = 10;
private const int Separation = 4;
public BatteryBulletRenderer()
{
Parameters = new LayoutParameters
{
ItemWidth = SizeH,
ItemHeight = SizeV,
ItemSeparation = SizeH + Separation,
MinCountPerRow = 3,
VerticalSeparation = Separation
};
}
protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor)
{
var color = spent ? ItemColorGone : ItemColor;
handle.DrawRect(UIBox2.FromDimensions(renderPos, new Vector2(SizeH, SizeV)), color);
}
}

View File

@@ -116,7 +116,7 @@ public sealed partial class GunSystem
public sealed class BoxesStatusControl : Control public sealed class BoxesStatusControl : Control
{ {
private readonly BoxContainer _bulletsList; private readonly BatteryBulletRenderer _bullets;
private readonly Label _ammoCount; private readonly Label _ammoCount;
public BoxesStatusControl() public BoxesStatusControl()
@@ -128,27 +128,18 @@ public sealed partial class GunSystem
AddChild(new BoxContainer AddChild(new BoxContainer
{ {
Orientation = BoxContainer.LayoutOrientation.Horizontal, Orientation = BoxContainer.LayoutOrientation.Horizontal,
HorizontalExpand = true,
Children = Children =
{ {
new Control (_bullets = new BatteryBulletRenderer
{ {
HorizontalExpand = true, Margin = new Thickness(0, 0, 5, 0),
Children = HorizontalExpand = true
{ }),
(_bulletsList = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
VerticalAlignment = VAlignment.Center,
SeparationOverride = 4
}),
}
},
new Control() { MinSize = new Vector2(5, 0) },
(_ammoCount = new Label (_ammoCount = new Label
{ {
StyleClasses = { StyleNano.StyleClassItemStatus }, StyleClasses = { StyleNano.StyleClassItemStatus },
HorizontalAlignment = HAlignment.Right, HorizontalAlignment = HAlignment.Right,
VerticalAlignment = VAlignment.Bottom
}), }),
} }
}); });
@@ -156,46 +147,12 @@ public sealed partial class GunSystem
public void Update(int count, int max) public void Update(int count, int max)
{ {
_bulletsList.RemoveAllChildren();
_ammoCount.Visible = true; _ammoCount.Visible = true;
_ammoCount.Text = $"x{count:00}"; _ammoCount.Text = $"x{count:00}";
max = Math.Min(max, 8);
FillBulletRow(_bulletsList, count, max);
}
private static void FillBulletRow(Control container, int count, int capacity) _bullets.Capacity = max;
{ _bullets.Count = count;
var colorGone = Color.FromHex("#000000");
var color = Color.FromHex("#E00000");
// Draw the empty ones
for (var i = count; i < capacity; i++)
{
container.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat()
{
BackgroundColor = colorGone,
},
MinSize = new Vector2(10, 15),
});
}
// Draw the full ones, but limit the count to the capacity
count = Math.Min(count, capacity);
for (var i = 0; i < count; i++)
{
container.AddChild(new PanelContainer
{
PanelOverride = new StyleBoxFlat()
{
BackgroundColor = color,
},
MinSize = new Vector2(10, 15),
});
}
} }
} }

View File

@@ -126,9 +126,43 @@ public sealed class HandsComponentState : ComponentState
/// <summary> /// <summary>
/// What side of the body this hand is on. /// What side of the body this hand is on.
/// </summary> /// </summary>
/// <seealso cref="HandUILocation"/>
/// <seealso cref="HandLocationExt"/>
public enum HandLocation : byte public enum HandLocation : byte
{ {
Left, Left,
Middle, Middle,
Right Right
} }
/// <summary>
/// What side of the UI a hand is on.
/// </summary>
/// <seealso cref="HandLocationExt"/>
/// <seealso cref="HandLocation"/>
public enum HandUILocation : byte
{
Left,
Right
}
/// <summary>
/// Helper functions for working with <see cref="HandLocation"/>.
/// </summary>
public static class HandLocationExt
{
/// <summary>
/// Convert a <see cref="HandLocation"/> into the appropriate <see cref="HandUILocation"/>.
/// This maps "middle" hands to <see cref="HandUILocation.Right"/>.
/// </summary>
public static HandUILocation GetUILocation(this HandLocation location)
{
return location switch
{
HandLocation.Left => HandUILocation.Left,
HandLocation.Middle => HandUILocation.Right,
HandLocation.Right => HandUILocation.Right,
_ => throw new ArgumentOutOfRangeException(nameof(location), location, null)
};
}
}

View File

@@ -41,5 +41,5 @@ network-configurator-examine-current-mode = Current mode: {$mode}
network-configurator-examine-switch-modes = Press {$key} to switch modes network-configurator-examine-switch-modes = Press {$key} to switch modes
# item status # item status
network-configurator-item-status-label = Current mode: {$mode} network-configurator-item-status-label = Mode: {$mode}
{$keybinding} to switch mode Switch: {$keybinding}

View File

@@ -10,9 +10,10 @@ implanter-component-implant-already = {$target} already has the {$implant}!
implanter-draw-text = Draw implanter-draw-text = Draw
implanter-inject-text = Inject implanter-inject-text = Inject
implanter-empty-text = None implanter-empty-text = Empty
implanter-label = Implant: [color=green]{$implantName}[/color] | [color=white]{$modeString}[/color]{$lineBreak}{$implantDescription} implanter-label = [color=green]{$implantName}[/color]
Mode: [color=white]{$modeString}[/color]
implanter-contained-implant-text = [color=green]{$desc}[/color] implanter-contained-implant-text = [color=green]{$desc}[/color]

View File

@@ -0,0 +1 @@
item-status-not-held = No held item

View File

@@ -1,3 +1,3 @@
geiger-item-control-status = Radiation: [color={$color}]{$rads} rads[/color] geiger-item-control-status = [color={$color}]{$rads} rads[/color]
geiger-item-control-disabled = Disabled geiger-item-control-disabled = Disabled
geiger-component-examine = Current radiation: [color={$color}]{$rads} rads[/color] geiger-component-examine = Current radiation: [color={$color}]{$rads} rads[/color]