Stacked sprite visualizer (#3096)

* Add Stack Visualizer

* Add cigarette pack resources

Adds transparent layers for visualizing cigarettes

* Add Bag Open/Close Visualizer

So storage opened in inventory can have different icons when opened
or closed.

* Create a component that only enumerates single item

Used for creating stuff like matchbox, or cigarettes. As a bonus.
It will only update stack visualizer for that particullar item.

* Refactoring stuff

* Fix other usage of stack in Resources

* Add docs

* Apply suggestions from code review

Apply metalgearsloth suggestions

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Applied suggestions from metalgearsloth

* Changed SingleItemStorageComponent to StorageCounterComponent

Difference. New component doesn't spawn items, merely counts them.

* Refactored StackVisualizer

* Fix breakage with master

* Update Resources/Prototypes/Entities/Objects/Consumable/fancy.yml

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* Update with MGS suggestions

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
Ygg01
2021-02-17 14:02:36 +01:00
committed by GitHub
parent 83f102ea75
commit 55d65889ae
20 changed files with 655 additions and 134 deletions

View File

@@ -1,6 +1,9 @@
using Content.Client.UserInterface.Stylesheets;
#nullable enable
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.GameObjects.Components;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
@@ -15,6 +18,7 @@ namespace Content.Client.GameObjects.Components
public class StackComponent : SharedStackComponent, IItemStatus
{
[ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
[ComponentDependency] private readonly AppearanceComponent? _appearanceComponent = default!;
public Control MakeControl() => new StatusControl(this);
@@ -23,12 +27,30 @@ namespace Content.Client.GameObjects.Components
get => base.Count;
set
{
var valueChanged = value != Count;
base.Count = value;
if (valueChanged)
{
_appearanceComponent?.SetData(StackVisuals.Actual, Count);
}
_uiUpdateNeeded = true;
}
}
public override void Initialize()
{
base.Initialize();
if (!Owner.Deleted)
{
_appearanceComponent?.SetData(StackVisuals.MaxCount, MaxCount);
_appearanceComponent?.SetData(StackVisuals.Hide, false);
}
}
private sealed class StatusControl : Control
{
private readonly StackComponent _parent;
@@ -58,4 +80,4 @@ namespace Content.Client.GameObjects.Components
}
}
}
}
}

View File

@@ -0,0 +1,174 @@
#nullable enable
using System.Collections.Generic;
using Content.Shared.GameObjects.Components;
using Content.Shared.Utility;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components
{
/// <summary>
/// Visualizer for items that come in stacks and have different appearance
/// depending on the size of the stack. Visualizer can work by switching between different
/// icons in <c>_spriteLayers</c> or if the sprite layers are supposed to be composed as transparent layers.
/// The former behavior is default and the latter behavior can be defined in prototypes.
///
/// <example>
/// <para>To define a Stack Visualizer prototype insert the following
/// snippet (you can skip Appearance if already defined)
/// </para>
/// <code>
/// - type: Appearance
/// visuals:
/// - type: StackVisualizer
/// stackLayers:
/// - goldbar_10
/// - goldbar_20
/// - goldbar_30
/// </code>
/// </example>
/// <example>
/// <para>Defining a stack visualizer with composable transparent layers</para>
/// <code>
/// - type: StackVisualizer
/// composite: true
/// stackLayers:
/// - cigarette_1
/// - cigarette_2
/// - cigarette_3
/// - cigarette_4
/// - cigarette_5
/// - cigarette_6
/// </code>
/// </example>
/// <seealso cref="_spriteLayers"/>
/// </summary>
[UsedImplicitly]
public class StackVisualizer : AppearanceVisualizer
{
/// <summary>
/// Default IconLayer stack.
/// </summary>
private const int IconLayer = 0;
/// <summary>
/// Sprite layers used in stack visualizer. Sprites first in layer correspond to lower stack states
/// e.g. <code>_spriteLayers[0]</code> is lower stack level than <code>_spriteLayers[1]</code>.
/// </summary>
private readonly List<string> _spriteLayers = new();
/// <summary>
/// Determines if the visualizer uses composite or non-composite layers for icons. Defaults to false.
///
/// <list type="bullet">
/// <item>
/// <description>false: they are opaque and mutually exclusive (e.g. sprites in a wire coil). <b>Default value</b></description>
/// </item>
/// <item>
/// <description>true: they are transparent and thus layered one over another in ascending order first</description>
/// </item>
/// </list>
///
/// </summary>
private bool _isComposite;
public override void LoadData(YamlMappingNode mapping)
{
base.LoadData(mapping);
if (mapping.TryGetNode<YamlSequenceNode>("stackLayers", out var spriteSequenceNode))
{
foreach (var yamlNode in spriteSequenceNode)
{
_spriteLayers.Add(((YamlScalarNode) yamlNode).Value!);
}
}
if (mapping.TryGetNode<YamlScalarNode>("composite", out var transparent))
{
_isComposite = transparent.AsBool();
}
}
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
if (_isComposite
&& _spriteLayers.Count > 0
&& entity.TryGetComponent<ISpriteComponent>(out var spriteComponent))
{
foreach (var sprite in _spriteLayers)
{
var rsiPath = spriteComponent.BaseRSI!.Path!;
spriteComponent.LayerMapReserveBlank(sprite);
spriteComponent.LayerSetSprite(sprite, new SpriteSpecifier.Rsi(rsiPath, sprite));
spriteComponent.LayerSetVisible(sprite, false);
}
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (component.Owner.TryGetComponent<ISpriteComponent>(out var spriteComponent))
{
if (_isComposite)
{
ProcessCompositeSprites(component, spriteComponent);
}
else
{
ProcessOpaqueSprites(component, spriteComponent);
}
}
}
private void ProcessOpaqueSprites(AppearanceComponent component, ISpriteComponent spriteComponent)
{
// Skip processing if no actual
if (!component.TryGetData<int>(StackVisuals.Actual, out var actual)) return;
if (!component.TryGetData<int>(StackVisuals.MaxCount, out var maxCount))
{
maxCount = _spriteLayers.Count;
}
var activeLayer = ContentHelpers.RoundToNearestLevels(actual, maxCount, _spriteLayers.Count - 1);
spriteComponent.LayerSetState(IconLayer, _spriteLayers[activeLayer]);
}
private void ProcessCompositeSprites(AppearanceComponent component, ISpriteComponent spriteComponent)
{
// If hidden, don't render any sprites
if (!component.TryGetData<bool>(StackVisuals.Hide, out var hide)
|| hide)
{
foreach (var transparentSprite in _spriteLayers)
{
spriteComponent.LayerSetVisible(transparentSprite, false);
}
return;
}
// Skip processing if no actual/maxCount
if (!component.TryGetData<int>(StackVisuals.Actual, out var actual)) return;
if (!component.TryGetData<int>(StackVisuals.MaxCount, out var maxCount))
{
maxCount = _spriteLayers.Count;
}
var activeTill = ContentHelpers.RoundToNearestLevels(actual, maxCount, _spriteLayers.Count);
for (var i = 0; i < _spriteLayers.Count; i++)
{
spriteComponent.LayerSetVisible(_spriteLayers[i], i < activeTill);
}
}
}
}

View File

@@ -0,0 +1,68 @@
#nullable enable
using Content.Shared.GameObjects.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
namespace Content.Client.GameObjects.Components.Storage
{
[UsedImplicitly]
public class BagOpenCloseVisualizer : AppearanceVisualizer
{
private const string OpenIcon = "openIcon";
private string? _openIcon;
public override void LoadData(YamlMappingNode node)
{
base.LoadData(node);
if (node.TryGetNode<YamlScalarNode>(OpenIcon, out var openIconNode))
{
_openIcon = openIconNode.Value;
}
else
{
Logger.Warning("BagOpenCloseVisualizer is useless with no `openIcon`");
}
}
public override void InitializeEntity(IEntity entity)
{
base.InitializeEntity(entity);
if (_openIcon != null && entity.TryGetComponent<SpriteComponent>(out var spriteComponent))
{
var rsiPath = spriteComponent.BaseRSI!.Path!;
spriteComponent.LayerMapReserveBlank(OpenIcon);
spriteComponent.LayerSetSprite(OpenIcon, new SpriteSpecifier.Rsi(rsiPath, _openIcon));
spriteComponent.LayerSetVisible(OpenIcon, false);
}
}
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (_openIcon != null
&& component.Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
{
if (component.TryGetData<SharedBagState>(SharedBagOpenVisuals.BagState, out var bagState))
{
switch (bagState)
{
case SharedBagState.Open:
spriteComponent.LayerSetVisible(OpenIcon, true);
break;
default:
spriteComponent.LayerSetVisible(OpenIcon, false);
break;
}
}
}
}
}
}

View File

@@ -1,8 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Client.Animations;
using Content.Client.GameObjects.Components.Items;
using Content.Shared.GameObjects.Components;
using Content.Shared.GameObjects.Components.Storage;
using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Client.GameObjects;
@@ -29,9 +30,18 @@ namespace Content.Client.GameObjects.Components.Storage
private int StorageSizeUsed;
private int StorageCapacityMax;
private StorageWindow Window;
private SharedBagState _bagState;
public override IReadOnlyList<IEntity> StoredEntities => _storedEntities;
public override void Initialize()
{
base.Initialize();
// Hide stackVisualizer on start
_bagState = SharedBagState.Close;
}
public override void OnAdd()
{
base.OnAdd();
@@ -69,12 +79,15 @@ namespace Content.Client.GameObjects.Components.Storage
//Updates what we are storing for the UI
case StorageHeldItemsMessage msg:
HandleStorageMessage(msg);
ChangeStorageVisualization(_bagState);
break;
//Opens the UI
case OpenStorageUIMessage _:
ChangeStorageVisualization(SharedBagState.Open);
ToggleUI();
break;
case CloseStorageUIMessage _:
ChangeStorageVisualization(SharedBagState.Close);
CloseUI();
break;
case AnimateInsertingEntitiesMessage msg:
@@ -119,6 +132,7 @@ namespace Content.Client.GameObjects.Components.Storage
private void ToggleUI()
{
if (Window.IsOpen)
Window.Close();
else
Window.Open();
@@ -128,6 +142,16 @@ namespace Content.Client.GameObjects.Components.Storage
{
Window.Close();
}
private void ChangeStorageVisualization(SharedBagState state)
{
_bagState = state;
if (Owner.TryGetComponent<AppearanceComponent>(out var appearanceComponent))
{
appearanceComponent.SetData(SharedBagOpenVisuals.BagState, state);
appearanceComponent.SetData(StackVisuals.Hide, state == SharedBagState.Close);
}
}
/// <summary>
/// Function for clicking one of the stored entity buttons in the UI, tells server to remove that entity