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.Client.Utility;
using Content.Shared.GameObjects.Components; using Content.Shared.GameObjects.Components;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
@@ -15,6 +18,7 @@ namespace Content.Client.GameObjects.Components
public class StackComponent : SharedStackComponent, IItemStatus public class StackComponent : SharedStackComponent, IItemStatus
{ {
[ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded; [ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
[ComponentDependency] private readonly AppearanceComponent? _appearanceComponent = default!;
public Control MakeControl() => new StatusControl(this); public Control MakeControl() => new StatusControl(this);
@@ -23,12 +27,30 @@ namespace Content.Client.GameObjects.Components
get => base.Count; get => base.Count;
set set
{ {
var valueChanged = value != Count;
base.Count = value; base.Count = value;
if (valueChanged)
{
_appearanceComponent?.SetData(StackVisuals.Actual, Count);
}
_uiUpdateNeeded = true; _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 sealed class StatusControl : Control
{ {
private readonly StackComponent _parent; private readonly StackComponent _parent;

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

View File

@@ -164,6 +164,7 @@ namespace Content.Client
"Firelock", "Firelock",
"AtmosPlaque", "AtmosPlaque",
"Spillable", "Spillable",
"StorageCounter",
"SpaceVillainArcade", "SpaceVillainArcade",
"Flammable", "Flammable",
"CreamPie", "CreamPie",

View File

@@ -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;
}
}
}

View File

@@ -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,
}
}

View 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
}
}

View File

@@ -25,14 +25,17 @@ namespace Content.Shared.Utility
{ {
throw new ArgumentException("Levels must be greater than 0.", nameof(levels)); throw new ArgumentException("Levels must be greater than 0.", nameof(levels));
} }
if (actual >= max) if (actual >= max)
{ {
return levels - 1; return levels - 1;
} }
if (actual <= 0) if (actual <= 0)
{ {
return 0; return 0;
} }
var toOne = actual / max; var toOne = actual / max;
double threshold; double threshold;
if (levels % 2 == 0) if (levels % 2 == 0)
@@ -81,28 +84,18 @@ namespace Content.Shared.Utility
{ {
throw new ArgumentException("Levels must be greater than 1.", nameof(levels)); throw new ArgumentException("Levels must be greater than 1.", nameof(levels));
} }
if (actual >= max) if (actual >= max)
{ {
return levels; return levels;
} }
if (actual <= 0) if (actual <= 0)
{ {
return 0; return 0;
} }
double step = max / levels;
int nearest = 0; return (int) Math.Round(actual / max * levels, MidpointRounding.AwayFromZero);
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;
} }
} }
} }

View File

@@ -9,8 +9,8 @@ namespace Content.Tests.Shared.Utility
[TestOf(typeof(ContentHelpers))] [TestOf(typeof(ContentHelpers))]
public class ContentHelpers_Test public class ContentHelpers_Test
{ {
public static readonly IEnumerable<(double val, double max, int levels, int expected)> TestData =
public static readonly IEnumerable<(double val, double max, int levels, int expected)> TestData = new(double, double, int, int)[] new (double, double, int, int)[]
{ {
// Testing odd level counts. These are easy. // Testing odd level counts. These are easy.
(-1, 10, 5, 0), (-1, 10, 5, 0),
@@ -45,6 +45,43 @@ namespace Content.Tests.Shared.Utility
(10, 10, 6, 5), (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] [Parallelizable]
[Test] [Test]
public void Test([ValueSource(nameof(TestData))] (double val, double max, int levels, int expected) data) 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; (double val, double max, int levels, int expected) = data;
Assert.That(ContentHelpers.RoundToLevels(val, max, levels), Is.EqualTo(expected)); 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));
}
} }
} }

View File

@@ -1,4 +1,6 @@
- type: entity - type: Tag
id: Cigarette
- type: entity
name: "Base Cigarette" name: "Base Cigarette"
id: BaseCigarette id: BaseCigarette
parent: BaseItem parent: BaseItem
@@ -13,6 +15,9 @@
Slots: [ mask ] Slots: [ mask ]
HeldPrefix: unlit HeldPrefix: unlit
size: 1 size: 1
- type: Tag
tags:
- Cigarette
- type: Smoking - type: Smoking
duration: 30 duration: 30
- type: Appearance - type: Appearance
@@ -33,10 +38,25 @@
components: components:
- type: Sprite - type: Sprite
sprite: Objects/Consumable/Fancy/cigarettes.rsi sprite: Objects/Consumable/Fancy/cigarettes.rsi
netsync: false
layers: layers:
- state: cig - state: cig
- type: StorageFill - type: StorageFill
contents: contents:
- name: Cigarette - name: Cigarette
amount: 6 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

View File

@@ -143,6 +143,14 @@
- type: Sprite - type: Sprite
sprite: Objects/Materials/materials.rsi sprite: Objects/Materials/materials.rsi
state: goldbar_30 state: goldbar_30
netsync: false
- type: Appearance
visuals:
- type: StackVisualizer
stackLayers:
- goldbar_10
- goldbar_20
- goldbar_30
- type: entity - type: entity
id: GoldStack1 id: GoldStack1

View File

@@ -1,4 +1,3 @@
# If you're looking at the rsi for this file, you'll probably be confused why # 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 # 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 # ability to have applied colors yet in GUIs. -Swept
@@ -14,11 +13,13 @@
stacktype: enum.StackType.Cable stacktype: enum.StackType.Cable
- type: Sprite - type: Sprite
sprite: Objects/Tools/cables.rsi sprite: Objects/Tools/cables.rsi
netsync: false
- type: Item - type: Item
sprite: Objects/Tools/cables.rsi sprite: Objects/Tools/cables.rsi
- type: WirePlacer - type: WirePlacer
- type: Clickable - type: Clickable
- type: entity - type: entity
id: HVWireStack id: HVWireStack
parent: CableStack parent: CableStack
@@ -35,6 +36,13 @@
- type: WirePlacer - type: WirePlacer
wirePrototypeID: HVWire wirePrototypeID: HVWire
blockingWireType: HighVoltage blockingWireType: HighVoltage
- type: Appearance
visuals:
- type: StackVisualizer
stackLayers:
- coilhv-10
- coilhv-20
- coilhv-30
- type: entity - type: entity
parent: HVWireStack parent: HVWireStack
@@ -63,6 +71,13 @@
- type: WirePlacer - type: WirePlacer
wirePrototypeID: ApcExtensionCable wirePrototypeID: ApcExtensionCable
blockingWireType: Apc blockingWireType: Apc
- type: Appearance
visuals:
- type: StackVisualizer
stackLayers:
- coillv-10
- coillv-20
- coillv-30
- type: entity - type: entity
parent: ApcExtensionCableStack parent: ApcExtensionCableStack
@@ -92,6 +107,13 @@
- type: WirePlacer - type: WirePlacer
wirePrototypeID: MVWire wirePrototypeID: MVWire
blockingWireType: MediumVoltage blockingWireType: MediumVoltage
- type: Appearance
visuals:
- type: StackVisualizer
stackLayers:
- coilmv-10
- coilmv-20
- coilmv-30
- type: entity - type: entity
parent: MVWireStack 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

View File

@@ -15,6 +15,24 @@
}, },
{ {
"name": "cig_open" "name": "cig_open"
},
{
"name": "cigarette_1"
},
{
"name": "cigarette_2"
},
{
"name": "cigarette_3"
},
{
"name": "cigarette_4"
},
{
"name": "cigarette_5"
},
{
"name": "cigarette_6"
} }
] ]
} }