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:
@@ -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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -129,6 +143,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
|
||||
/// </summary>
|
||||
|
||||
@@ -164,6 +164,7 @@ namespace Content.Client
|
||||
"Firelock",
|
||||
"AtmosPlaque",
|
||||
"Spillable",
|
||||
"StorageCounter",
|
||||
"SpaceVillainArcade",
|
||||
"Flammable",
|
||||
"CreamPie",
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Content.Shared.GameObjects.Components.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Items.Storage
|
||||
{
|
||||
/// <summary>
|
||||
/// Storage that spawns and counts a single item.
|
||||
/// Usually used for things like matchboxes, cigarette packs,
|
||||
/// cigar cases etc.
|
||||
/// </summary>
|
||||
/// <code>
|
||||
/// - type: StorageCounter
|
||||
/// amount: 6 # Note: this field can be omitted
|
||||
/// countTag: Cigarette # Note: field doesn't point to entity Id, but its tag
|
||||
/// </code>
|
||||
[RegisterComponent]
|
||||
public class StorageCounterComponent : Component
|
||||
{
|
||||
private string? _countTag;
|
||||
private int? _maxAmount;
|
||||
|
||||
/// <summary>
|
||||
/// Single item storage component usually have an attached StackedVisualizer.
|
||||
/// </summary>
|
||||
[ComponentDependency] private readonly AppearanceComponent? _appearanceComponent = default;
|
||||
|
||||
public override string Name => "StorageCounter";
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _countTag, "countTag", null);
|
||||
if (_countTag == null)
|
||||
{
|
||||
Logger.Warning("StorageCounterComponent without a `countTag` is useless");
|
||||
}
|
||||
serializer.DataField(ref _maxAmount, "amount", null);
|
||||
}
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
|
||||
if (_appearanceComponent != null)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case ContainerContentsModifiedMessage msg:
|
||||
var actual = Count(msg.Container.ContainedEntities);
|
||||
_appearanceComponent.SetData(StackVisuals.Actual, actual);
|
||||
if (_maxAmount != null)
|
||||
{
|
||||
_appearanceComponent.SetData(StackVisuals.MaxCount, _maxAmount);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int Count(IReadOnlyList<IEntity> containerContainedEntities)
|
||||
{
|
||||
var count = 0;
|
||||
if (_countTag != null)
|
||||
{
|
||||
foreach (var entity in containerContainedEntities)
|
||||
{
|
||||
if (entity.HasTag(_countTag))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public enum SharedBagOpenVisuals : byte
|
||||
{
|
||||
BagState,
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum SharedBagState : byte
|
||||
{
|
||||
Open,
|
||||
Close,
|
||||
}
|
||||
}
|
||||
21
Content.Shared/GameObjects/Components/StackVisuals.cs
Normal file
21
Content.Shared/GameObjects/Components/StackVisuals.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
public enum StackVisuals : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of elements in the stack
|
||||
/// </summary>
|
||||
Actual,
|
||||
/// <summary>
|
||||
/// The total amount of elements in the stack. If unspecified, the visualizer assumes
|
||||
/// its
|
||||
/// </summary>
|
||||
MaxCount,
|
||||
Hide
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,17 @@ namespace Content.Shared.Utility
|
||||
{
|
||||
throw new ArgumentException("Levels must be greater than 0.", nameof(levels));
|
||||
}
|
||||
|
||||
if (actual >= max)
|
||||
{
|
||||
return levels - 1;
|
||||
}
|
||||
|
||||
if (actual <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var toOne = actual / max;
|
||||
double threshold;
|
||||
if (levels % 2 == 0)
|
||||
@@ -49,11 +52,11 @@ namespace Content.Shared.Utility
|
||||
var preround = toOne * (levels - 1);
|
||||
if (toOne <= threshold || levels <= 2)
|
||||
{
|
||||
return (int)Math.Ceiling(preround);
|
||||
return (int) Math.Ceiling(preround);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (int)Math.Floor(preround);
|
||||
return (int) Math.Floor(preround);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,28 +84,18 @@ namespace Content.Shared.Utility
|
||||
{
|
||||
throw new ArgumentException("Levels must be greater than 1.", nameof(levels));
|
||||
}
|
||||
|
||||
if (actual >= max)
|
||||
{
|
||||
return levels;
|
||||
}
|
||||
|
||||
if (actual <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
double step = max / levels;
|
||||
|
||||
int nearest = 0;
|
||||
double nearestDiff = actual;
|
||||
for (var i = 1; i <= levels; i++)
|
||||
{
|
||||
var diff = Math.Abs(actual - i * step);
|
||||
if (diff < nearestDiff)
|
||||
{
|
||||
nearestDiff = diff;
|
||||
nearest = i;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
return (int) Math.Round(actual / max * levels, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,42 +9,79 @@ namespace Content.Tests.Shared.Utility
|
||||
[TestOf(typeof(ContentHelpers))]
|
||||
public class ContentHelpers_Test
|
||||
{
|
||||
|
||||
public static readonly IEnumerable<(double val, double max, int levels, int expected)> TestData = new(double, double, int, int)[]
|
||||
public static readonly IEnumerable<(double val, double max, int levels, int expected)> TestData =
|
||||
new (double, double, int, int)[]
|
||||
{
|
||||
// Testing odd level counts. These are easy.
|
||||
(-1, 10, 5, 0),
|
||||
( 0, 10, 5, 0),
|
||||
( 0.01f, 10, 5, 1),
|
||||
( 1, 10, 5, 1),
|
||||
( 2, 10, 5, 1),
|
||||
( 2.5f, 10, 5, 1),
|
||||
( 2.51f, 10, 5, 2),
|
||||
( 3, 10, 5, 2),
|
||||
( 4, 10, 5, 2),
|
||||
( 5, 10, 5, 2),
|
||||
( 6, 10, 5, 2),
|
||||
( 7, 10, 5, 2),
|
||||
( 7.49f, 10, 5, 2),
|
||||
( 7.5f, 10, 5, 3),
|
||||
( 8, 10, 5, 3),
|
||||
( 9, 10, 5, 3),
|
||||
(0, 10, 5, 0),
|
||||
(0.01f, 10, 5, 1),
|
||||
(1, 10, 5, 1),
|
||||
(2, 10, 5, 1),
|
||||
(2.5f, 10, 5, 1),
|
||||
(2.51f, 10, 5, 2),
|
||||
(3, 10, 5, 2),
|
||||
(4, 10, 5, 2),
|
||||
(5, 10, 5, 2),
|
||||
(6, 10, 5, 2),
|
||||
(7, 10, 5, 2),
|
||||
(7.49f, 10, 5, 2),
|
||||
(7.5f, 10, 5, 3),
|
||||
(8, 10, 5, 3),
|
||||
(9, 10, 5, 3),
|
||||
(10, 10, 5, 4),
|
||||
(11, 10, 5, 4),
|
||||
|
||||
// Even level counts though..
|
||||
( 1, 10, 6, 1),
|
||||
( 2, 10, 6, 1),
|
||||
( 3, 10, 6, 2),
|
||||
( 4, 10, 6, 2),
|
||||
( 5, 10, 6, 2),
|
||||
( 6, 10, 6, 3),
|
||||
( 7, 10, 6, 3),
|
||||
( 8, 10, 6, 4),
|
||||
( 9, 10, 6, 4),
|
||||
(1, 10, 6, 1),
|
||||
(2, 10, 6, 1),
|
||||
(3, 10, 6, 2),
|
||||
(4, 10, 6, 2),
|
||||
(5, 10, 6, 2),
|
||||
(6, 10, 6, 3),
|
||||
(7, 10, 6, 3),
|
||||
(8, 10, 6, 4),
|
||||
(9, 10, 6, 4),
|
||||
(10, 10, 6, 5),
|
||||
};
|
||||
|
||||
public static readonly IEnumerable<(double val, double max, int levels, int expected)> TestNear =
|
||||
new (double, double, int, int)[]
|
||||
{
|
||||
// Testing odd counts
|
||||
(0, 5, 2, 0),
|
||||
(1, 5, 2, 0),
|
||||
(2, 5, 2, 1),
|
||||
(3, 5, 2, 1),
|
||||
(4, 5, 2, 2),
|
||||
(5, 5, 2, 2),
|
||||
|
||||
// Testing even counts
|
||||
(0, 6, 5, 0),
|
||||
(1, 6, 5, 1),
|
||||
(2, 6, 5, 2),
|
||||
(3, 6, 5, 3),
|
||||
(4, 6, 5, 3),
|
||||
(5, 6, 5, 4),
|
||||
(6, 6, 5, 5),
|
||||
|
||||
// Testing transparency disable use case
|
||||
(0, 6, 6, 0),
|
||||
(1, 6, 6, 1),
|
||||
(2, 6, 6, 2),
|
||||
(3, 6, 6, 3),
|
||||
(4, 6, 6, 4),
|
||||
(5, 6, 6, 5),
|
||||
(6, 6, 6, 6),
|
||||
|
||||
// Testing edge cases
|
||||
(0.1, 6, 5, 0),
|
||||
(-32, 6, 5, 0),
|
||||
(2.4, 6, 5, 2),
|
||||
(2.5, 6, 5, 2),
|
||||
(320, 6, 5, 5),
|
||||
};
|
||||
|
||||
[Parallelizable]
|
||||
[Test]
|
||||
public void Test([ValueSource(nameof(TestData))] (double val, double max, int levels, int expected) data)
|
||||
@@ -52,5 +89,13 @@ namespace Content.Tests.Shared.Utility
|
||||
(double val, double max, int levels, int expected) = data;
|
||||
Assert.That(ContentHelpers.RoundToLevels(val, max, levels), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Parallelizable]
|
||||
[Test]
|
||||
public void TestNearest([ValueSource(nameof(TestNear))] (double val, double max, int size, int expected) data)
|
||||
{
|
||||
(double val, double max, int size, int expected) = data;
|
||||
Assert.That(ContentHelpers.RoundToNearestLevels(val, max, size), Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
- type: entity
|
||||
- type: Tag
|
||||
id: Cigarette
|
||||
- type: entity
|
||||
name: "Base Cigarette"
|
||||
id: BaseCigarette
|
||||
parent: BaseItem
|
||||
@@ -13,6 +15,9 @@
|
||||
Slots: [ mask ]
|
||||
HeldPrefix: unlit
|
||||
size: 1
|
||||
- type: Tag
|
||||
tags:
|
||||
- Cigarette
|
||||
- type: Smoking
|
||||
duration: 30
|
||||
- type: Appearance
|
||||
@@ -33,10 +38,25 @@
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Objects/Consumable/Fancy/cigarettes.rsi
|
||||
netsync: false
|
||||
layers:
|
||||
- state: cig
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- name: Cigarette
|
||||
amount: 6
|
||||
|
||||
- type: StorageCounter
|
||||
countTag: Cigarette
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: BagOpenCloseVisualizer
|
||||
openIcon: cig_open
|
||||
- type: StackVisualizer
|
||||
composite: true
|
||||
stackLayers:
|
||||
- cigarette_1
|
||||
- cigarette_2
|
||||
- cigarette_3
|
||||
- cigarette_4
|
||||
- cigarette_5
|
||||
- cigarette_6
|
||||
|
||||
@@ -143,6 +143,14 @@
|
||||
- type: Sprite
|
||||
sprite: Objects/Materials/materials.rsi
|
||||
state: goldbar_30
|
||||
netsync: false
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: StackVisualizer
|
||||
stackLayers:
|
||||
- goldbar_10
|
||||
- goldbar_20
|
||||
- goldbar_30
|
||||
|
||||
- type: entity
|
||||
id: GoldStack1
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
# If you're looking at the rsi for this file, you'll probably be confused why
|
||||
# I didn't just use an alpha for most of this stuff. Well icons don't have the
|
||||
# ability to have applied colors yet in GUIs. -Swept
|
||||
@@ -14,11 +13,13 @@
|
||||
stacktype: enum.StackType.Cable
|
||||
- type: Sprite
|
||||
sprite: Objects/Tools/cables.rsi
|
||||
netsync: false
|
||||
- type: Item
|
||||
sprite: Objects/Tools/cables.rsi
|
||||
- type: WirePlacer
|
||||
- type: Clickable
|
||||
|
||||
|
||||
- type: entity
|
||||
id: HVWireStack
|
||||
parent: CableStack
|
||||
@@ -35,6 +36,13 @@
|
||||
- type: WirePlacer
|
||||
wirePrototypeID: HVWire
|
||||
blockingWireType: HighVoltage
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: StackVisualizer
|
||||
stackLayers:
|
||||
- coilhv-10
|
||||
- coilhv-20
|
||||
- coilhv-30
|
||||
|
||||
- type: entity
|
||||
parent: HVWireStack
|
||||
@@ -63,6 +71,13 @@
|
||||
- type: WirePlacer
|
||||
wirePrototypeID: ApcExtensionCable
|
||||
blockingWireType: Apc
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: StackVisualizer
|
||||
stackLayers:
|
||||
- coillv-10
|
||||
- coillv-20
|
||||
- coillv-30
|
||||
|
||||
- type: entity
|
||||
parent: ApcExtensionCableStack
|
||||
@@ -92,6 +107,13 @@
|
||||
- type: WirePlacer
|
||||
wirePrototypeID: MVWire
|
||||
blockingWireType: MediumVoltage
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: StackVisualizer
|
||||
stackLayers:
|
||||
- coilmv-10
|
||||
- coilmv-20
|
||||
- coilmv-30
|
||||
|
||||
- type: entity
|
||||
parent: MVWireStack
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 117 B |
Binary file not shown.
|
After Width: | Height: | Size: 116 B |
Binary file not shown.
|
After Width: | Height: | Size: 116 B |
Binary file not shown.
|
After Width: | Height: | Size: 116 B |
Binary file not shown.
|
After Width: | Height: | Size: 114 B |
Binary file not shown.
|
After Width: | Height: | Size: 114 B |
@@ -15,6 +15,24 @@
|
||||
},
|
||||
{
|
||||
"name": "cig_open"
|
||||
},
|
||||
{
|
||||
"name": "cigarette_1"
|
||||
},
|
||||
{
|
||||
"name": "cigarette_2"
|
||||
},
|
||||
{
|
||||
"name": "cigarette_3"
|
||||
},
|
||||
{
|
||||
"name": "cigarette_4"
|
||||
},
|
||||
{
|
||||
"name": "cigarette_5"
|
||||
},
|
||||
{
|
||||
"name": "cigarette_6"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user