Cursed Mask (#29659)
* Cursed Mask * extra expressions * block ingestion * mind returning * okay fix the removal shit
6
Content.Client/Clothing/Systems/CursedMaskSystem.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
using Content.Shared.Clothing;
|
||||||
|
|
||||||
|
namespace Content.Client.Clothing.Systems;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public sealed class CursedMaskSystem : SharedCursedMaskSystem;
|
||||||
92
Content.Server/Clothing/Systems/CursedMaskSystem.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using Content.Server.Administration.Logs;
|
||||||
|
using Content.Server.GameTicking;
|
||||||
|
using Content.Server.Mind;
|
||||||
|
using Content.Server.NPC;
|
||||||
|
using Content.Server.NPC.HTN;
|
||||||
|
using Content.Server.NPC.Systems;
|
||||||
|
using Content.Server.Popups;
|
||||||
|
using Content.Shared.Clothing;
|
||||||
|
using Content.Shared.Clothing.Components;
|
||||||
|
using Content.Shared.Database;
|
||||||
|
using Content.Shared.NPC.Components;
|
||||||
|
using Content.Shared.NPC.Systems;
|
||||||
|
using Content.Shared.Players;
|
||||||
|
using Content.Shared.Popups;
|
||||||
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Server.Clothing.Systems;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public sealed class CursedMaskSystem : SharedCursedMaskSystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IAdminLogManager _adminLog = default!;
|
||||||
|
[Dependency] private readonly GameTicker _ticker = default!;
|
||||||
|
[Dependency] private readonly HTNSystem _htn = default!;
|
||||||
|
[Dependency] private readonly MindSystem _mind = default!;
|
||||||
|
[Dependency] private readonly NPCSystem _npc = default!;
|
||||||
|
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||||
|
[Dependency] private readonly PopupSystem _popup = default!;
|
||||||
|
|
||||||
|
// We can't store this info on the component easily
|
||||||
|
private static readonly ProtoId<HTNCompoundPrototype> TakeoverRootTask = "SimpleHostileCompound";
|
||||||
|
|
||||||
|
protected override void TryTakeover(Entity<CursedMaskComponent> ent, EntityUid wearer)
|
||||||
|
{
|
||||||
|
if (ent.Comp.CurrentState != CursedMaskExpression.Anger)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (TryComp<ActorComponent>(wearer, out var actor) && actor.PlayerSession.GetMind() is { } mind)
|
||||||
|
{
|
||||||
|
var session = actor.PlayerSession;
|
||||||
|
if (!_ticker.OnGhostAttempt(mind, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ent.Comp.StolenMind = mind;
|
||||||
|
|
||||||
|
_popup.PopupEntity(Loc.GetString("cursed-mask-takeover-popup"), wearer, session, PopupType.LargeCaution);
|
||||||
|
_adminLog.Add(LogType.Action,
|
||||||
|
LogImpact.Extreme,
|
||||||
|
$"{ToPrettyString(wearer):player} had their body taken over and turned into an enemy through the cursed mask {ToPrettyString(ent):entity}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var npcFaction = EnsureComp<NpcFactionMemberComponent>(wearer);
|
||||||
|
ent.Comp.OldFactions = npcFaction.Factions;
|
||||||
|
_npcFaction.ClearFactions((wearer, npcFaction), false);
|
||||||
|
_npcFaction.AddFaction((wearer, npcFaction), ent.Comp.CursedMaskFaction);
|
||||||
|
|
||||||
|
ent.Comp.HasNpc = !EnsureComp<HTNComponent>(wearer, out var htn);
|
||||||
|
htn.RootTask = new HTNCompoundTask { Task = TakeoverRootTask };
|
||||||
|
htn.Blackboard.SetValue(NPCBlackboard.Owner, wearer);
|
||||||
|
_npc.WakeNPC(wearer, htn);
|
||||||
|
_htn.Replan(htn);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClothingUnequip(Entity<CursedMaskComponent> ent, ref ClothingGotUnequippedEvent args)
|
||||||
|
{
|
||||||
|
// If we are taking off the cursed mask
|
||||||
|
if (ent.Comp.CurrentState == CursedMaskExpression.Anger)
|
||||||
|
{
|
||||||
|
if (ent.Comp.HasNpc)
|
||||||
|
RemComp<HTNComponent>(args.Wearer);
|
||||||
|
|
||||||
|
var npcFaction = EnsureComp<NpcFactionMemberComponent>(args.Wearer);
|
||||||
|
_npcFaction.RemoveFaction((args.Wearer, npcFaction), ent.Comp.CursedMaskFaction, false);
|
||||||
|
_npcFaction.AddFactions((args.Wearer, npcFaction), ent.Comp.OldFactions);
|
||||||
|
|
||||||
|
ent.Comp.HasNpc = false;
|
||||||
|
ent.Comp.OldFactions.Clear();
|
||||||
|
|
||||||
|
if (Exists(ent.Comp.StolenMind))
|
||||||
|
{
|
||||||
|
_mind.TransferTo(ent.Comp.StolenMind.Value, args.Wearer);
|
||||||
|
_adminLog.Add(LogType.Action,
|
||||||
|
LogImpact.Extreme,
|
||||||
|
$"{ToPrettyString(args.Wearer):player} was restored to their body after the removal of {ToPrettyString(ent):entity}.");
|
||||||
|
ent.Comp.StolenMind = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RandomizeCursedMask(ent, args.Wearer);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
Content.Shared/Clothing/Components/CursedMaskComponent.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using Content.Shared.Damage;
|
||||||
|
using Content.Shared.NPC.Prototypes;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Clothing.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is used for a mask that takes over the host when worn.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, NetworkedComponent, Access(typeof(SharedCursedMaskSystem))]
|
||||||
|
public sealed partial class CursedMaskComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The current expression shown. Used to determine which effect is applied.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public CursedMaskExpression CurrentState = CursedMaskExpression.Neutral;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Speed modifier applied when the "Joy" expression is present.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public float JoySpeedModifier = 1.15f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Damage modifier applied when the "Despair" expression is present.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public DamageModifierSet DespairDamageModifier = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not the mask is currently attached to an NPC.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool HasNpc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The mind that was booted from the wearer when the mask took over.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public EntityUid? StolenMind;
|
||||||
|
|
||||||
|
[DataField]
|
||||||
|
public ProtoId<NpcFactionPrototype> CursedMaskFaction = "SimpleHostile";
|
||||||
|
|
||||||
|
[DataField]
|
||||||
|
public HashSet<ProtoId<NpcFactionPrototype>> OldFactions = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum CursedMaskVisuals : byte
|
||||||
|
{
|
||||||
|
State
|
||||||
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum CursedMaskExpression : byte
|
||||||
|
{
|
||||||
|
Neutral,
|
||||||
|
Joy,
|
||||||
|
Despair,
|
||||||
|
Anger
|
||||||
|
}
|
||||||
73
Content.Shared/Clothing/SharedCursedMaskSystem.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using Content.Shared.Clothing.Components;
|
||||||
|
using Content.Shared.Damage;
|
||||||
|
using Content.Shared.Examine;
|
||||||
|
using Content.Shared.Inventory;
|
||||||
|
using Content.Shared.Movement.Systems;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
|
|
||||||
|
namespace Content.Shared.Clothing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This handles <see cref="CursedMaskComponent"/>
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SharedCursedMaskSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
|
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||||
|
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<CursedMaskComponent, ClothingGotEquippedEvent>(OnClothingEquip);
|
||||||
|
SubscribeLocalEvent<CursedMaskComponent, ClothingGotUnequippedEvent>(OnClothingUnequip);
|
||||||
|
SubscribeLocalEvent<CursedMaskComponent, ExaminedEvent>(OnExamine);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<CursedMaskComponent, InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent>>(OnMovementSpeedModifier);
|
||||||
|
SubscribeLocalEvent<CursedMaskComponent, InventoryRelayedEvent<DamageModifyEvent>>(OnModifyDamage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClothingEquip(Entity<CursedMaskComponent> ent, ref ClothingGotEquippedEvent args)
|
||||||
|
{
|
||||||
|
RandomizeCursedMask(ent, args.Wearer);
|
||||||
|
TryTakeover(ent, args.Wearer);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnClothingUnequip(Entity<CursedMaskComponent> ent, ref ClothingGotUnequippedEvent args)
|
||||||
|
{
|
||||||
|
RandomizeCursedMask(ent, args.Wearer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExamine(Entity<CursedMaskComponent> ent, ref ExaminedEvent args)
|
||||||
|
{
|
||||||
|
args.PushMarkup(Loc.GetString($"cursed-mask-examine-{ent.Comp.CurrentState.ToString()}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMovementSpeedModifier(Entity<CursedMaskComponent> ent, ref InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent> args)
|
||||||
|
{
|
||||||
|
if (ent.Comp.CurrentState == CursedMaskExpression.Joy)
|
||||||
|
args.Args.ModifySpeed(ent.Comp.JoySpeedModifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnModifyDamage(Entity<CursedMaskComponent> ent, ref InventoryRelayedEvent<DamageModifyEvent> args)
|
||||||
|
{
|
||||||
|
if (ent.Comp.CurrentState == CursedMaskExpression.Despair)
|
||||||
|
args.Args.Damage = DamageSpecifier.ApplyModifierSet(args.Args.Damage, ent.Comp.DespairDamageModifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void RandomizeCursedMask(Entity<CursedMaskComponent> ent, EntityUid wearer)
|
||||||
|
{
|
||||||
|
var random = new System.Random((int) _timing.CurTick.Value);
|
||||||
|
ent.Comp.CurrentState = random.Pick(Enum.GetValues<CursedMaskExpression>());
|
||||||
|
_appearance.SetData(ent, CursedMaskVisuals.State, ent.Comp.CurrentState);
|
||||||
|
_movementSpeedModifier.RefreshMovementSpeedModifiers(wearer);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void TryTakeover(Entity<CursedMaskComponent> ent, EntityUid wearer)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,11 @@ public abstract class UnequipAttemptEventBase : CancellableEntityEventArgs
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly EntityUid Equipment;
|
public readonly EntityUid Equipment;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The slotFlags of the slot this item is being removed from.
|
||||||
|
/// </summary>
|
||||||
|
public readonly SlotFlags SlotFlags;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The slot the entity is being unequipped from.
|
/// The slot the entity is being unequipped from.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -33,6 +38,7 @@ public abstract class UnequipAttemptEventBase : CancellableEntityEventArgs
|
|||||||
UnEquipTarget = unEquipTarget;
|
UnEquipTarget = unEquipTarget;
|
||||||
Equipment = equipment;
|
Equipment = equipment;
|
||||||
Unequipee = unequipee;
|
Unequipee = unequipee;
|
||||||
|
SlotFlags = slotDefinition.SlotFlags;
|
||||||
Slot = slotDefinition.Name;
|
Slot = slotDefinition.Name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
Content.Shared/Inventory/SelfEquipOnlyComponent.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
|
namespace Content.Shared.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is used for an item that can only be equipped/unequipped by the user.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, NetworkedComponent, Access(typeof(SelfEquipOnlySystem))]
|
||||||
|
public sealed partial class SelfEquipOnlyComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not the self-equip only condition requires the person to be conscious.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool UnequipRequireConscious = true;
|
||||||
|
}
|
||||||
45
Content.Shared/Inventory/SelfEquipOnlySystem.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using Content.Shared.ActionBlocker;
|
||||||
|
using Content.Shared.Clothing.Components;
|
||||||
|
using Content.Shared.Inventory.Events;
|
||||||
|
|
||||||
|
namespace Content.Shared.Inventory;
|
||||||
|
|
||||||
|
public sealed class SelfEquipOnlySystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
SubscribeLocalEvent<SelfEquipOnlyComponent, BeingEquippedAttemptEvent>(OnBeingEquipped);
|
||||||
|
SubscribeLocalEvent<SelfEquipOnlyComponent, BeingUnequippedAttemptEvent>(OnBeingUnequipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBeingEquipped(Entity<SelfEquipOnlyComponent> ent, ref BeingEquippedAttemptEvent args)
|
||||||
|
{
|
||||||
|
if (args.Cancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (TryComp<ClothingComponent>(ent, out var clothing) && (clothing.Slots & args.SlotFlags) == SlotFlags.NONE)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Equipee != args.EquipTarget)
|
||||||
|
args.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBeingUnequipped(Entity<SelfEquipOnlyComponent> ent, ref BeingUnequippedAttemptEvent args)
|
||||||
|
{
|
||||||
|
if (args.Cancelled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Unequipee == args.UnEquipTarget)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (TryComp<ClothingComponent>(ent, out var clothing) && (clothing.Slots & args.SlotFlags) == SlotFlags.NONE)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ent.Comp.UnequipRequireConscious && !_actionBlocker.CanConsciouslyPerformAction(args.UnEquipTarget))
|
||||||
|
return;
|
||||||
|
args.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,5 +69,10 @@ namespace Content.Shared.Movement.Systems
|
|||||||
WalkSpeedModifier *= walk;
|
WalkSpeedModifier *= walk;
|
||||||
SprintSpeedModifier *= sprint;
|
SprintSpeedModifier *= sprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ModifySpeed(float mod)
|
||||||
|
{
|
||||||
|
ModifySpeed(mod, mod);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,28 @@ public sealed partial class NpcFactionSystem : EntitySystem
|
|||||||
RefreshFactions((ent, ent.Comp));
|
RefreshFactions((ent, ent.Comp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds this entity to the particular faction.
|
||||||
|
/// </summary>
|
||||||
|
public void AddFactions(Entity<NpcFactionMemberComponent?> ent, HashSet<ProtoId<NpcFactionPrototype>> factions, bool dirty = true)
|
||||||
|
{
|
||||||
|
ent.Comp ??= EnsureComp<NpcFactionMemberComponent>(ent);
|
||||||
|
|
||||||
|
foreach (var faction in factions)
|
||||||
|
{
|
||||||
|
if (!_proto.HasIndex(faction))
|
||||||
|
{
|
||||||
|
Log.Error($"Unable to find faction {faction}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.Comp.Factions.Add(faction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirty)
|
||||||
|
RefreshFactions((ent, ent.Comp));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes this entity from the particular faction.
|
/// Removes this entity from the particular faction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
cursed-mask-examine-Neutral = It depicts an entirely unremarkable visage.
|
||||||
|
cursed-mask-examine-Joy = It depicts a face basking in joy.
|
||||||
|
cursed-mask-examine-Despair = It depicts a face wraught with despair.
|
||||||
|
cursed-mask-examine-Anger = It depicts a furious expression locked in rage.
|
||||||
|
cursed-mask-takeover-popup = The mask seizes control over your body!
|
||||||
@@ -62,3 +62,41 @@
|
|||||||
- type: HideLayerClothing
|
- type: HideLayerClothing
|
||||||
slots:
|
slots:
|
||||||
- Snout
|
- Snout
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
parent: ClothingMaskBase
|
||||||
|
id: ClothingMaskGoldenCursed
|
||||||
|
name: golden mask
|
||||||
|
description: Previously used in strange pantomimes, after one of the actors went mad on stage these masks have avoided use. You swear its face contorts when you're not looking.
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Clothing/Mask/goldenmask.rsi
|
||||||
|
layers:
|
||||||
|
- state: icon
|
||||||
|
map: [ "mask" ]
|
||||||
|
- type: Clothing
|
||||||
|
sprite: Clothing/Mask/goldenmask.rsi
|
||||||
|
- type: Appearance
|
||||||
|
- type: GenericVisualizer
|
||||||
|
visuals:
|
||||||
|
enum.CursedMaskVisuals.State:
|
||||||
|
mask:
|
||||||
|
Neutral: { state: icon }
|
||||||
|
Despair: { state: icon-despair }
|
||||||
|
Joy: { state: icon-joy }
|
||||||
|
Anger: { state: icon-anger }
|
||||||
|
- type: Tag
|
||||||
|
tags: [] # ignore "WhitelistChameleon" tag
|
||||||
|
- type: SelfEquipOnly
|
||||||
|
- type: CursedMask
|
||||||
|
despairDamageModifier:
|
||||||
|
coefficients:
|
||||||
|
Blunt: 0.6
|
||||||
|
Slash: 0.6
|
||||||
|
Piercing: 0.4
|
||||||
|
- type: HideLayerClothing
|
||||||
|
slots:
|
||||||
|
- Snout
|
||||||
|
- type: IngestionBlocker
|
||||||
|
- type: StaticPrice
|
||||||
|
price: 5000
|
||||||
|
|||||||
|
After Width: | Height: | Size: 453 B |
|
After Width: | Height: | Size: 401 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-anger.png
Normal file
|
After Width: | Height: | Size: 422 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-despair.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-joy.png
Normal file
|
After Width: | Height: | Size: 414 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon.png
Normal file
|
After Width: | Height: | Size: 402 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-left.png
Normal file
|
After Width: | Height: | Size: 360 B |
BIN
Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-right.png
Normal file
|
After Width: | Height: | Size: 353 B |
39
Resources/Textures/Clothing/Mask/goldenmask.rsi/meta.json
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "Taken from tgstation at commit https://github.com/vgstation-coders/vgstation13/blob/HEAD/icons/obj/clothing/masks.dmi. Vox and Reptilian edits by EmoGarbage404 (Github)",
|
||||||
|
"size": {
|
||||||
|
"x": 32,
|
||||||
|
"y": 32
|
||||||
|
},
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon-joy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon-despair"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "icon-anger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "equipped-MASK",
|
||||||
|
"directions": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "equipped-MASK-vox",
|
||||||
|
"directions": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inhand-left",
|
||||||
|
"directions": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "inhand-right",
|
||||||
|
"directions": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||