]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Cursed Mask (#29659)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Sat, 10 Aug 2024 15:14:58 +0000 (11:14 -0400)
committerGitHub <noreply@github.com>
Sat, 10 Aug 2024 15:14:58 +0000 (11:14 -0400)
* Cursed Mask

* extra expressions

* block ingestion

* mind returning

* okay fix the removal shit

20 files changed:
Content.Client/Clothing/Systems/CursedMaskSystem.cs [new file with mode: 0644]
Content.Server/Clothing/Systems/CursedMaskSystem.cs [new file with mode: 0644]
Content.Shared/Clothing/Components/CursedMaskComponent.cs [new file with mode: 0644]
Content.Shared/Clothing/SharedCursedMaskSystem.cs [new file with mode: 0644]
Content.Shared/Inventory/Events/UnequipAttemptEvent.cs
Content.Shared/Inventory/SelfEquipOnlyComponent.cs [new file with mode: 0644]
Content.Shared/Inventory/SelfEquipOnlySystem.cs [new file with mode: 0644]
Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs
Content.Shared/NPC/Systems/NpcFactionSystem.cs
Resources/Locale/en-US/clothing/components/cursed-mask.ftl [new file with mode: 0644]
Resources/Prototypes/Entities/Clothing/Masks/specific.yml
Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK-vox.png [new file with mode: 0644]
Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK.png [new file with mode: 0644]
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-anger.png [new file with mode: 0644]
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-despair.png [new file with mode: 0644]
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-joy.png [new file with mode: 0644]
Resources/Textures/Clothing/Mask/goldenmask.rsi/icon.png [new file with mode: 0644]
Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-left.png [new file with mode: 0644]
Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-right.png [new file with mode: 0644]
Resources/Textures/Clothing/Mask/goldenmask.rsi/meta.json [new file with mode: 0644]

diff --git a/Content.Client/Clothing/Systems/CursedMaskSystem.cs b/Content.Client/Clothing/Systems/CursedMaskSystem.cs
new file mode 100644 (file)
index 0000000..bc931d1
--- /dev/null
@@ -0,0 +1,6 @@
+using Content.Shared.Clothing;
+
+namespace Content.Client.Clothing.Systems;
+
+/// <inheritdoc/>
+public sealed class CursedMaskSystem : SharedCursedMaskSystem;
diff --git a/Content.Server/Clothing/Systems/CursedMaskSystem.cs b/Content.Server/Clothing/Systems/CursedMaskSystem.cs
new file mode 100644 (file)
index 0000000..2045ff5
--- /dev/null
@@ -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);
+    }
+}
diff --git a/Content.Shared/Clothing/Components/CursedMaskComponent.cs b/Content.Shared/Clothing/Components/CursedMaskComponent.cs
new file mode 100644 (file)
index 0000000..6073bdf
--- /dev/null
@@ -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
+}
diff --git a/Content.Shared/Clothing/SharedCursedMaskSystem.cs b/Content.Shared/Clothing/SharedCursedMaskSystem.cs
new file mode 100644 (file)
index 0000000..8ba83be
--- /dev/null
@@ -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)
+    {
+
+    }
+}
index d8d0a2a23b2a8149d409aaacd7d931c2fd56e991..b647ee8e92b58a89fd7310dfa89f8adf1adc8814 100644 (file)
@@ -17,6 +17,11 @@ public abstract class UnequipAttemptEventBase : CancellableEntityEventArgs
     /// </summary>
     public readonly EntityUid Equipment;
 
+    /// <summary>
+    /// The slotFlags of the slot this item is being removed from.
+    /// </summary>
+    public readonly SlotFlags SlotFlags;
+
     /// <summary>
     /// The slot the entity is being unequipped from.
     /// </summary>
@@ -33,6 +38,7 @@ public abstract class UnequipAttemptEventBase : CancellableEntityEventArgs
         UnEquipTarget = unEquipTarget;
         Equipment = equipment;
         Unequipee = unequipee;
+        SlotFlags = slotDefinition.SlotFlags;
         Slot = slotDefinition.Name;
     }
 }
diff --git a/Content.Shared/Inventory/SelfEquipOnlyComponent.cs b/Content.Shared/Inventory/SelfEquipOnlyComponent.cs
new file mode 100644 (file)
index 0000000..ee1980e
--- /dev/null
@@ -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;
+}
diff --git a/Content.Shared/Inventory/SelfEquipOnlySystem.cs b/Content.Shared/Inventory/SelfEquipOnlySystem.cs
new file mode 100644 (file)
index 0000000..2bd113e
--- /dev/null
@@ -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();
+    }
+}
index 7c793d5eb898949aa10bc1e13d6fdd7bcae961ee..8e89e4b62b3762bb9af64697f620ab79c2f258a4 100644 (file)
@@ -69,5 +69,10 @@ namespace Content.Shared.Movement.Systems
             WalkSpeedModifier *= walk;
             SprintSpeedModifier *= sprint;
         }
+
+        public void ModifySpeed(float mod)
+        {
+            ModifySpeed(mod, mod);
+        }
     }
 }
index 355f5bbb3a9433e82cf9b546adae12e05861e160..98f14afe2a6f140fa962b6ba9fde4945e378d8c8 100644 (file)
@@ -100,6 +100,28 @@ public sealed partial class NpcFactionSystem : EntitySystem
             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>
     /// Removes this entity from the particular faction.
     /// </summary>
diff --git a/Resources/Locale/en-US/clothing/components/cursed-mask.ftl b/Resources/Locale/en-US/clothing/components/cursed-mask.ftl
new file mode 100644 (file)
index 0000000..c93a6cf
--- /dev/null
@@ -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!
index c3a07fa8e9d850d9f9927ac1bb3a26e2778cd396..64a1adcebdf8a93770005b0013f2e8b9a1d2132d 100644 (file)
   - type: HideLayerClothing
     slots:
     - 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
diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK-vox.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK-vox.png
new file mode 100644 (file)
index 0000000..1b4db3d
Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK-vox.png differ
diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK.png
new file mode 100644 (file)
index 0000000..d1353c4
Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/equipped-MASK.png differ
diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-anger.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-anger.png
new file mode 100644 (file)
index 0000000..5e002ca
Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-anger.png differ
diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-despair.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-despair.png
new file mode 100644 (file)
index 0000000..71bdd72
Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-despair.png differ
diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-joy.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-joy.png
new file mode 100644 (file)
index 0000000..93d0e26
Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon-joy.png differ
diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon.png
new file mode 100644 (file)
index 0000000..1da86e3
Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/icon.png differ
diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-left.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-left.png
new file mode 100644 (file)
index 0000000..3ce2895
Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-left.png differ
diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-right.png b/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-right.png
new file mode 100644 (file)
index 0000000..ba71330
Binary files /dev/null and b/Resources/Textures/Clothing/Mask/goldenmask.rsi/inhand-right.png differ
diff --git a/Resources/Textures/Clothing/Mask/goldenmask.rsi/meta.json b/Resources/Textures/Clothing/Mask/goldenmask.rsi/meta.json
new file mode 100644 (file)
index 0000000..62072e7
--- /dev/null
@@ -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
+    }
+  ]
+}