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:
174
Content.Client/GameObjects/Components/StackVisualizer.cs
Normal file
174
Content.Client/GameObjects/Components/StackVisualizer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user