Handcuff system (#1831)
* Implemented most serverside logic * All serverside cuff logic complete * SFX, Clientside HUD stuff, Other logic. * fffff * Cuffs 1.0 * missing loc string * Cuffs are stored in the balls now. * Basic integrationtest * Support stripping menu. * rrr * Fixes * properties * gun emoji * fixes * get rid of unused * reeee * Update Content.Shared/GameObjects/ContentNetIDs.cs Co-authored-by: Víctor Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
@@ -0,0 +1,58 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
using Content.Shared.Preferences.Appearance;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class CuffableComponent : SharedCuffableComponent
|
||||
{
|
||||
[ViewVariables]
|
||||
private string _currentRSI = default;
|
||||
|
||||
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
|
||||
{
|
||||
if (!(curState is CuffableComponentState cuffState))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CanStillInteract = cuffState.CanStillInteract;
|
||||
|
||||
if (Owner.TryGetComponent<SpriteComponent>(out var sprite))
|
||||
{
|
||||
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, cuffState.NumHandsCuffed > 0);
|
||||
sprite.LayerSetColor(HumanoidVisualLayers.Handcuffs, cuffState.Color);
|
||||
|
||||
if (cuffState.NumHandsCuffed > 0)
|
||||
{
|
||||
if (_currentRSI != cuffState.RSI) // we don't want to keep loading the same RSI
|
||||
{
|
||||
_currentRSI = cuffState.RSI;
|
||||
sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, new RSI.StateId(cuffState.IconState), new ResourcePath(cuffState.RSI));
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, new RSI.StateId(cuffState.IconState)); // TODO: safety check to see if RSI contains the state?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
|
||||
if (Owner.TryGetComponent<SpriteComponent>(out var sprite))
|
||||
{
|
||||
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class HandcuffComponent : SharedHandcuffComponent
|
||||
{
|
||||
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
|
||||
{
|
||||
var cuffState = curState as HandcuffedComponentState;
|
||||
|
||||
if (cuffState == null || cuffState.IconState == string.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Owner.TryGetComponent<SpriteComponent>(out var sprite))
|
||||
{
|
||||
sprite.LayerSetState(0, new RSI.StateId(cuffState.IconState)); // TODO: safety check to see if RSI contains the state?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,10 @@ using Content.Shared.GameObjects.Components.GUI;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects.Components.UserInterface;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components.UserInterface;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Localization;
|
||||
using static Content.Shared.GameObjects.Components.Inventory.EquipmentSlotDefines;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
@@ -15,6 +17,7 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
{
|
||||
public Dictionary<Slots, string> Inventory { get; private set; }
|
||||
public Dictionary<string, string> Hands { get; private set; }
|
||||
public Dictionary<EntityUid, string> Handcuffs { get; private set; }
|
||||
|
||||
[ViewVariables]
|
||||
private StrippingMenu _strippingMenu;
|
||||
@@ -49,7 +52,8 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
|
||||
_strippingMenu.ClearButtons();
|
||||
|
||||
if(Inventory != null)
|
||||
if (Inventory != null)
|
||||
{
|
||||
foreach (var (slot, name) in Inventory)
|
||||
{
|
||||
_strippingMenu.AddButton(EquipmentSlotDefines.SlotNames[slot], name, (ev) =>
|
||||
@@ -57,8 +61,10 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
SendMessage(new StrippingInventoryButtonPressed(slot));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(Hands != null)
|
||||
if (Hands != null)
|
||||
{
|
||||
foreach (var (hand, name) in Hands)
|
||||
{
|
||||
_strippingMenu.AddButton(hand, name, (ev) =>
|
||||
@@ -68,6 +74,18 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
}
|
||||
}
|
||||
|
||||
if (Handcuffs != null)
|
||||
{
|
||||
foreach (var (id, name) in Handcuffs)
|
||||
{
|
||||
_strippingMenu.AddButton(Loc.GetString("Restraints"), name, (ev) =>
|
||||
{
|
||||
SendMessage(new StrippingHandcuffButtonPressed(id));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
@@ -76,6 +94,7 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
|
||||
Inventory = stripState.Inventory;
|
||||
Hands = stripState.Hands;
|
||||
Handcuffs = stripState.Handcuffs;
|
||||
|
||||
UpdateMenu();
|
||||
}
|
||||
|
||||
@@ -156,7 +156,8 @@ namespace Content.Client.GameObjects.Components.Items
|
||||
}
|
||||
else
|
||||
{
|
||||
var (rsi, state) = maybeInHands.Value;
|
||||
var (rsi, state, color) = maybeInHands.Value;
|
||||
_sprite.LayerSetColor($"hand-{name}", color);
|
||||
_sprite.LayerSetVisible($"hand-{name}", true);
|
||||
_sprite.LayerSetState($"hand-{name}", state, rsi);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using Robust.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Items
|
||||
@@ -25,6 +26,8 @@ namespace Content.Client.GameObjects.Components.Items
|
||||
|
||||
[ViewVariables] protected ResourcePath RsiPath;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)] protected Color Color;
|
||||
|
||||
private string _equippedPrefix;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
@@ -40,7 +43,7 @@ namespace Content.Client.GameObjects.Components.Items
|
||||
}
|
||||
}
|
||||
|
||||
public (RSI rsi, RSI.StateId stateId)? GetInHandStateInfo(HandLocation hand)
|
||||
public (RSI rsi, RSI.StateId stateId, Color color)? GetInHandStateInfo(HandLocation hand)
|
||||
{
|
||||
if (RsiPath == null)
|
||||
{
|
||||
@@ -52,7 +55,7 @@ namespace Content.Client.GameObjects.Components.Items
|
||||
var stateId = EquippedPrefix != null ? $"{EquippedPrefix}-inhand-{handName}" : $"inhand-{handName}";
|
||||
if (rsi.TryGetState(stateId, out _))
|
||||
{
|
||||
return (rsi, stateId);
|
||||
return (rsi, stateId, Color);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -62,6 +65,7 @@ namespace Content.Client.GameObjects.Components.Items
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataFieldCached(ref Color, "color", Color.White);
|
||||
serializer.DataFieldCached(ref RsiPath, "sprite", null);
|
||||
serializer.DataFieldCached(ref _equippedPrefix, "HeldPrefix", null);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Preferences.Appearance;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Content.Client.GameObjects.Components.ActionBlocking;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Mobs
|
||||
{
|
||||
@@ -49,6 +50,15 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
|
||||
sprite.LayerSetVisible(HumanoidVisualLayers.StencilMask, Sex == Sex.Female);
|
||||
|
||||
if (Owner.TryGetComponent<CuffableComponent>(out var cuffed))
|
||||
{
|
||||
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, !cuffed.CanStillInteract);
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false);
|
||||
}
|
||||
|
||||
var hairStyle = Appearance.HairStyleName;
|
||||
if (string.IsNullOrWhiteSpace(hairStyle) || !HairStyles.HairStylesMap.ContainsKey(hairStyle))
|
||||
hairStyle = HairStyles.DefaultHairStyle;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Content.Server.GameObjects.Components.ActionBlocking;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Body;
|
||||
using Content.Shared.Body.Part;
|
||||
using Content.Shared.GameObjects.Components.Body;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Server.Body;
|
||||
using Content.Client.GameObjects.Components.Items;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
[TestFixture]
|
||||
[TestOf(typeof(CuffableComponent))]
|
||||
[TestOf(typeof(HandcuffComponent))]
|
||||
public class CuffUnitTest : ContentIntegrationTest
|
||||
{
|
||||
[Test]
|
||||
public async Task Test()
|
||||
{
|
||||
var server = StartServerDummyTicker();
|
||||
|
||||
IEntity human;
|
||||
IEntity otherHuman;
|
||||
IEntity cuffs;
|
||||
IEntity cables;
|
||||
HandcuffComponent cableHandcuff;
|
||||
HandcuffComponent handcuff;
|
||||
CuffableComponent cuffed;
|
||||
IHandsComponent hands;
|
||||
BodyManagerComponent body;
|
||||
|
||||
server.Assert(() =>
|
||||
{
|
||||
var mapManager = IoCManager.Resolve<IMapManager>();
|
||||
mapManager.CreateNewMapEntity(MapId.Nullspace);
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
// Spawn the entities
|
||||
human = entityManager.SpawnEntity("BaseHumanMob_Content", MapCoordinates.Nullspace);
|
||||
otherHuman = entityManager.SpawnEntity("BaseHumanMob_Content", MapCoordinates.Nullspace);
|
||||
cuffs = entityManager.SpawnEntity("Handcuffs", MapCoordinates.Nullspace);
|
||||
cables = entityManager.SpawnEntity("Cablecuffs", MapCoordinates.Nullspace);
|
||||
|
||||
human.Transform.WorldPosition = otherHuman.Transform.WorldPosition;
|
||||
|
||||
// Test for components existing
|
||||
Assert.True(human.TryGetComponent(out cuffed!), $"Human has no {nameof(CuffableComponent)}");
|
||||
Assert.True(human.TryGetComponent(out hands!), $"Human has no {nameof(HandsComponent)}");
|
||||
Assert.True(human.TryGetComponent(out body!), $"Human has no {nameof(BodyManagerComponent)}");
|
||||
Assert.True(cuffs.TryGetComponent(out handcuff!), $"Handcuff has no {nameof(HandcuffComponent)}");
|
||||
Assert.True(cables.TryGetComponent(out cableHandcuff!), $"Cablecuff has no {nameof(HandcuffComponent)}");
|
||||
|
||||
// Test to ensure cuffed players register the handcuffs
|
||||
cuffed.AddNewCuffs(cuffs);
|
||||
Assert.True(cuffed.CuffedHandCount > 0, "Handcuffing a player did not result in their hands being cuffed");
|
||||
|
||||
// Test to ensure a player with 4 hands will still only have 2 hands cuffed
|
||||
AddHand(body);
|
||||
AddHand(body);
|
||||
Assert.True(cuffed.CuffedHandCount == 2 && hands.Hands.Count() == 4, "Player doesn't have correct amount of hands cuffed");
|
||||
|
||||
// Test to give a player with 4 hands 2 sets of cuffs
|
||||
cuffed.AddNewCuffs(cables);
|
||||
Assert.True(cuffed.CuffedHandCount == 4, "Player doesn't have correct amount of hands cuffed");
|
||||
|
||||
});
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
}
|
||||
|
||||
private void AddHand(BodyManagerComponent body)
|
||||
{
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
prototypeManager.TryIndex("bodyPart.LHand.BasicHuman", out BodyPartPrototype prototype);
|
||||
|
||||
var part = new BodyPart(prototype);
|
||||
var slot = part.GetHashCode().ToString();
|
||||
|
||||
body.Template.Slots.Add(slot, BodyPartType.Hand);
|
||||
body.InstallBodyPart(part, slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Body;
|
||||
using Content.Shared.Body.Part;
|
||||
@@ -42,7 +42,7 @@ namespace Content.Server.Body
|
||||
}
|
||||
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
prototypeManager.TryIndex("bodyPart.Hand.BasicHuman", out BodyPartPrototype prototype);
|
||||
prototypeManager.TryIndex("bodyPart.LHand.BasicHuman", out BodyPartPrototype prototype);
|
||||
|
||||
var part = new BodyPart(prototype);
|
||||
var slot = part.GetHashCode().ToString();
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
|
||||
using Robust.Server.GameObjects;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Robust.Shared.Log;
|
||||
using System.Linq;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Robust.Shared.Maths;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Serilog;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class CuffableComponent : SharedCuffableComponent
|
||||
{
|
||||
[Dependency]
|
||||
private readonly ISharedNotifyManager _notifyManager;
|
||||
|
||||
/// <summary>
|
||||
/// How many of this entity's hands are currently cuffed.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public int CuffedHandCount => _container.ContainedEntities.Count * 2;
|
||||
|
||||
protected IEntity LastAddedCuffs => _container.ContainedEntities[_container.ContainedEntities.Count - 1];
|
||||
|
||||
public IReadOnlyList<IEntity> StoredEntities => _container.ContainedEntities;
|
||||
|
||||
/// <summary>
|
||||
/// Container of various handcuffs currently applied to the entity.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
private Container _container = default!;
|
||||
|
||||
private bool _dirtyThisFrame = false;
|
||||
private float _interactRange;
|
||||
private IHandsComponent _hands;
|
||||
|
||||
public event Action OnCuffedStateChanged;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_container = ContainerManagerComponent.Ensure<Container>(Name, Owner);
|
||||
_interactRange = SharedInteractionSystem.InteractionRange / 2;
|
||||
|
||||
if (!Owner.TryGetComponent(out _hands))
|
||||
{
|
||||
Logger.Warning("Player does not have an IHandsComponent!");
|
||||
}
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
// there are 2 approaches i can think of to handle the handcuff overlay on players
|
||||
// 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same.
|
||||
// 2 - allow for several different player overlays for each different cuff type.
|
||||
// approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it.
|
||||
// right now we're doing approach #1.
|
||||
|
||||
if (CuffedHandCount > 0)
|
||||
{
|
||||
if (LastAddedCuffs.TryGetComponent<HandcuffComponent>(out var cuffs))
|
||||
{
|
||||
return new CuffableComponentState(CuffedHandCount,
|
||||
CanStillInteract,
|
||||
cuffs.CuffedRSI,
|
||||
$"{cuffs.OverlayIconState}-{CuffedHandCount}",
|
||||
cuffs.Color);
|
||||
// the iconstate is formatted as blah-2, blah-4, blah-6, etc.
|
||||
// the number corresponds to how many hands are cuffed.
|
||||
}
|
||||
}
|
||||
|
||||
return new CuffableComponentState(CuffedHandCount,
|
||||
CanStillInteract,
|
||||
"/Objects/Misc/handcuffs.rsi",
|
||||
"body-overlay-2",
|
||||
Color.White);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a set of cuffs to an existing CuffedComponent.
|
||||
/// </summary>
|
||||
/// <param name="prototype"></param>
|
||||
public void AddNewCuffs(IEntity handcuff)
|
||||
{
|
||||
if (!handcuff.HasComponent<HandcuffComponent>())
|
||||
{
|
||||
Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(
|
||||
handcuff.Transform.MapPosition,
|
||||
Owner.Transform.MapPosition,
|
||||
_interactRange,
|
||||
ignoredEnt: Owner))
|
||||
{
|
||||
Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!");
|
||||
return;
|
||||
}
|
||||
|
||||
_container.Insert(handcuff);
|
||||
CanStillInteract = _hands.Hands.Count() > CuffedHandCount;
|
||||
|
||||
OnCuffedStateChanged.Invoke();
|
||||
UpdateStatusEffect();
|
||||
UpdateHeldItems();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
public void Update(float frameTime)
|
||||
{
|
||||
UpdateHandCount();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs.
|
||||
/// </summary>
|
||||
private void UpdateHandCount()
|
||||
{
|
||||
_dirtyThisFrame = false;
|
||||
var handCount = _hands.Hands.Count();
|
||||
|
||||
while (CuffedHandCount > handCount && CuffedHandCount > 0)
|
||||
{
|
||||
_dirtyThisFrame = true;
|
||||
|
||||
var entity = _container.ContainedEntities[_container.ContainedEntities.Count - 1];
|
||||
_container.Remove(entity);
|
||||
entity.Transform.WorldPosition = Owner.Transform.GridPosition.Position;
|
||||
}
|
||||
|
||||
if (_dirtyThisFrame)
|
||||
{
|
||||
CanStillInteract = handCount > CuffedHandCount;
|
||||
OnCuffedStateChanged.Invoke();
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check how many items the user is holding and if it's more than the number of cuffed hands, drop some items.
|
||||
/// </summary>
|
||||
public void UpdateHeldItems()
|
||||
{
|
||||
var itemCount = _hands.GetAllHeldItems().Count();
|
||||
var freeHandCount = _hands.Hands.Count() - CuffedHandCount;
|
||||
|
||||
if (freeHandCount < itemCount)
|
||||
{
|
||||
foreach (ItemComponent item in _hands.GetAllHeldItems())
|
||||
{
|
||||
if (freeHandCount < itemCount)
|
||||
{
|
||||
freeHandCount++;
|
||||
_hands.Drop(item.Owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the status effect indicator on the HUD.
|
||||
/// </summary>
|
||||
private void UpdateStatusEffect()
|
||||
{
|
||||
if (Owner.TryGetComponent(out ServerStatusEffectsComponent status))
|
||||
{
|
||||
status.ChangeStatusEffectIcon(StatusEffect.Cuffed,
|
||||
CanStillInteract ? "/Textures/Interface/StatusEffects/Handcuffed/Uncuffed.png" : "/Textures/Interface/StatusEffects/Handcuffed/Handcuffed.png");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
|
||||
/// If the uncuffing succeeds, the cuffs will drop on the floor.
|
||||
/// </summary>
|
||||
/// <param name="user">The cuffed entity</param>
|
||||
/// <param name="cuffsToRemove">Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity.</param>
|
||||
public async void TryUncuff(IEntity user, IEntity cuffsToRemove = null)
|
||||
{
|
||||
var isOwner = user == Owner;
|
||||
|
||||
if (cuffsToRemove == null)
|
||||
{
|
||||
cuffsToRemove = LastAddedCuffs;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_container.ContainedEntities.Contains(cuffsToRemove))
|
||||
{
|
||||
Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
|
||||
}
|
||||
}
|
||||
|
||||
if (!cuffsToRemove.TryGetComponent<HandcuffComponent>(out var cuff))
|
||||
{
|
||||
Logger.Warning($"A user is trying to remove handcuffs without a {nameof(HandcuffComponent)}. This should never happen!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwner && !ActionBlockerSystem.CanInteract(user))
|
||||
{
|
||||
user.PopupMessage(user, "You can't do that!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwner &&
|
||||
!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(
|
||||
user.Transform.MapPosition,
|
||||
Owner.Transform.MapPosition,
|
||||
_interactRange,
|
||||
ignoredEnt: Owner))
|
||||
{
|
||||
user.PopupMessage(user, "You are too far away to remove the cuffs.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(
|
||||
cuffsToRemove.Transform.MapPosition,
|
||||
Owner.Transform.MapPosition,
|
||||
_interactRange,
|
||||
ignoredEnt: Owner))
|
||||
{
|
||||
Logger.Warning("Handcuffs being removed from player are obstructed or too far away! This should not happen!");
|
||||
return;
|
||||
}
|
||||
|
||||
user.PopupMessage(user, "You start removing the cuffs.");
|
||||
|
||||
var audio = EntitySystem.Get<AudioSystem>();
|
||||
audio.PlayFromEntity(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, Owner);
|
||||
|
||||
var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
|
||||
var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
NeedHand = true
|
||||
};
|
||||
|
||||
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
|
||||
var result = await doAfterSystem.DoAfter(doAfterEventArgs);
|
||||
|
||||
if (result != DoAfterStatus.Cancelled)
|
||||
{
|
||||
audio.PlayFromEntity(cuff.EndUncuffSound, Owner);
|
||||
|
||||
_container.ForceRemove(cuffsToRemove);
|
||||
cuffsToRemove.Transform.AttachToGridOrMap();
|
||||
cuffsToRemove.Transform.WorldPosition = Owner.Transform.WorldPosition;
|
||||
|
||||
if (cuff.BreakOnRemove)
|
||||
{
|
||||
cuff.Broken = true;
|
||||
|
||||
cuffsToRemove.Name = cuff.BrokenName;
|
||||
cuffsToRemove.Description = cuff.BrokenDesc;
|
||||
|
||||
if (cuffsToRemove.TryGetComponent<SpriteComponent>(out var sprite))
|
||||
{
|
||||
sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state?
|
||||
}
|
||||
}
|
||||
|
||||
CanStillInteract = _hands.Hands.Count() > CuffedHandCount;
|
||||
OnCuffedStateChanged.Invoke();
|
||||
UpdateStatusEffect();
|
||||
Dirty();
|
||||
|
||||
if (CuffedHandCount == 0)
|
||||
{
|
||||
_notifyManager.PopupMessage(user, user, Loc.GetString("You successfully remove the cuffs."));
|
||||
|
||||
if (!isOwner)
|
||||
{
|
||||
_notifyManager.PopupMessage(user, Owner, Loc.GetString("{0:theName} uncuffs your hands.", user));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isOwner)
|
||||
{
|
||||
_notifyManager.PopupMessage(user, user, Loc.GetString("You successfully remove the cuffs. {0} of {0:theName}'s hands remain cuffed.", CuffedHandCount, user));
|
||||
_notifyManager.PopupMessage(user, Owner, Loc.GetString("{0:theName} removes your cuffs. {1} of your hands remain cuffed.", user, CuffedHandCount));
|
||||
}
|
||||
else
|
||||
{
|
||||
_notifyManager.PopupMessage(user, user, Loc.GetString("You successfully remove the cuffs. {0} of your hands remain cuffed.", CuffedHandCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_notifyManager.PopupMessage(user, user, Loc.GetString("You fail to remove the cuffs."));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the uncuffing of a cuffed person. Used by other people and by the component owner to break out of cuffs.
|
||||
/// </summary>
|
||||
[Verb]
|
||||
private sealed class UncuffVerb : Verb<CuffableComponent>
|
||||
{
|
||||
protected override void GetData(IEntity user, CuffableComponent component, VerbData data)
|
||||
{
|
||||
if ((user != component.Owner && !ActionBlockerSystem.CanInteract(user)) || component.CuffedHandCount == 0)
|
||||
{
|
||||
data.Visibility = VerbVisibility.Invisible;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Text = Loc.GetString("Uncuff");
|
||||
}
|
||||
|
||||
protected override void Activate(IEntity user, CuffableComponent component)
|
||||
{
|
||||
if (component.CuffedHandCount > 0)
|
||||
{
|
||||
component.TryUncuff(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Robust.Shared.Maths;
|
||||
using System;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class HandcuffComponent : SharedHandcuffComponent, IAfterInteract
|
||||
{
|
||||
[Dependency]
|
||||
private readonly ISharedNotifyManager _notifyManager;
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes to apply a <see cref="CuffedComponent"/> to an entity.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public float CuffTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes to remove a <see cref="CuffedComponent"/> from an entity.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public float UncuffTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes for a cuffed entity to remove <see cref="CuffedComponent"/> from itself.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public float BreakoutTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public float StunBonus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Will the cuffs break when removed?
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool BreakOnRemove { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of the RSI file used for the player cuffed overlay.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string CuffedRSI { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used with the RSI file for the player cuffed overlay.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string OverlayIconState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used for broken handcuffs
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string BrokenState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used for broken handcuffs
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string BrokenName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used for broken handcuffs
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string BrokenDesc { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public bool Broken
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isBroken;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_isBroken != value)
|
||||
{
|
||||
_isBroken = value;
|
||||
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string StartCuffSound { get; set; }
|
||||
public string EndCuffSound { get; set; }
|
||||
public string StartBreakoutSound { get; set; }
|
||||
public string StartUncuffSound { get; set; }
|
||||
public string EndUncuffSound { get; set; }
|
||||
public Color Color { get; set; }
|
||||
|
||||
// Non-exposed data fields
|
||||
private bool _isBroken = false;
|
||||
private float _interactRange;
|
||||
private DoAfterSystem _doAfterSystem;
|
||||
private AudioSystem _audioSystem;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_audioSystem = EntitySystem.Get<AudioSystem>();
|
||||
_doAfterSystem = EntitySystem.Get<DoAfterSystem>();
|
||||
_interactRange = SharedInteractionSystem.InteractionRange / 2;
|
||||
}
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(this, x => x.CuffTime, "cuffTime", 5.0f);
|
||||
serializer.DataField(this, x => x.BreakoutTime, "breakoutTime", 30.0f);
|
||||
serializer.DataField(this, x => x.UncuffTime, "uncuffTime", 5.0f);
|
||||
serializer.DataField(this, x => x.StunBonus, "stunBonus", 2.0f);
|
||||
serializer.DataField(this, x => x.StartCuffSound, "startCuffSound", "/Audio/Items/Handcuffs/cuff_start.ogg");
|
||||
serializer.DataField(this, x => x.EndCuffSound, "endCuffSound", "/Audio/Items/Handcuffs/cuff_end.ogg");
|
||||
serializer.DataField(this, x => x.StartUncuffSound, "startUncuffSound", "/Audio/Items/Handcuffs/cuff_takeoff_start.ogg");
|
||||
serializer.DataField(this, x => x.EndUncuffSound, "endUncuffSound", "/Audio/Items/Handcuffs/cuff_takeoff_end.ogg");
|
||||
serializer.DataField(this, x => x.StartBreakoutSound, "startBreakoutSound", "/Audio/Items/Handcuffs/cuff_breakout_start.ogg");
|
||||
serializer.DataField(this, x => x.CuffedRSI, "cuffedRSI", "Objects/Misc/handcuffs.rsi");
|
||||
serializer.DataField(this, x => x.OverlayIconState, "bodyIconState", "body-overlay");
|
||||
serializer.DataField(this, x => x.Color, "color", Color.White);
|
||||
serializer.DataField(this, x => x.BreakOnRemove, "breakOnRemove", false);
|
||||
serializer.DataField(this, x => x.BrokenState, "brokenIconState", string.Empty);
|
||||
serializer.DataField(this, x => x.BrokenName, "brokenName", string.Empty);
|
||||
serializer.DataField(this, x => x.BrokenDesc, "brokenDesc", string.Empty);
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new HandcuffedComponentState(Broken ? BrokenState : string.Empty);
|
||||
}
|
||||
|
||||
void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.Target == null || !ActionBlockerSystem.CanUse(eventArgs.User) || !eventArgs.Target.TryGetComponent<CuffableComponent>(out var cuffed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventArgs.Target == eventArgs.User)
|
||||
{
|
||||
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("You can't cuff yourself!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Broken)
|
||||
{
|
||||
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("The cuffs are broken!"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventArgs.Target.TryGetComponent<HandsComponent>(out var hands))
|
||||
{
|
||||
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("{0:theName} has no hands!", eventArgs.Target));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cuffed.CuffedHandCount == hands.Count)
|
||||
{
|
||||
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("{0:theName} has no free hands to handcuff!", eventArgs.Target));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(
|
||||
eventArgs.User.Transform.MapPosition,
|
||||
eventArgs.Target.Transform.MapPosition,
|
||||
_interactRange,
|
||||
ignoredEnt: Owner))
|
||||
{
|
||||
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("You are too far away to use the cuffs!"));
|
||||
return;
|
||||
}
|
||||
|
||||
_notifyManager.PopupMessage(eventArgs.User, eventArgs.User, Loc.GetString("You start cuffing {0:theName}.", eventArgs.Target));
|
||||
_notifyManager.PopupMessage(eventArgs.User, eventArgs.Target, Loc.GetString("{0:theName} starts cuffing you!", eventArgs.User));
|
||||
_audioSystem.PlayFromEntity(StartCuffSound, Owner);
|
||||
|
||||
TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the cuffed state of an entity
|
||||
/// </summary>
|
||||
private async void TryUpdateCuff(IEntity user, IEntity target, CuffableComponent cuffs)
|
||||
{
|
||||
var cuffTime = CuffTime;
|
||||
|
||||
if (target.TryGetComponent<StunnableComponent>(out var stun) && stun.Stunned)
|
||||
{
|
||||
cuffTime = MathF.Max(0.1f, cuffTime - StunBonus);
|
||||
}
|
||||
|
||||
var doAfterEventArgs = new DoAfterEventArgs(user, cuffTime, default, target)
|
||||
{
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
NeedHand = true
|
||||
};
|
||||
|
||||
var result = await _doAfterSystem.DoAfter(doAfterEventArgs);
|
||||
|
||||
if (result != DoAfterStatus.Cancelled)
|
||||
{
|
||||
_audioSystem.PlayFromEntity(EndCuffSound, Owner);
|
||||
_notifyManager.PopupMessage(user, user, Loc.GetString("You successfully cuff {0}.", target.Name));
|
||||
_notifyManager.PopupMessage(target, target, Loc.GetString("You have been cuffed by {0}!", user.Name));
|
||||
|
||||
if (user.TryGetComponent<HandsComponent>(out var hands))
|
||||
{
|
||||
hands.Drop(Owner);
|
||||
cuffs.AddNewCuffs(Owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning("Unable to remove handcuffs from player's hands! This should not happen!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessage(user, Loc.GetString("You were interrupted while cuffing {0}!", target.Name));
|
||||
target.PopupMessage(target, Loc.GetString("You interrupt {0} while they are cuffing you!", user.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Content.Server.GameObjects.Components.ActionBlocking;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Server.Interfaces;
|
||||
@@ -28,7 +30,8 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
|
||||
public const float StripDelay = 2f;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(StrippingUiKey.Key);
|
||||
[ViewVariables]
|
||||
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(StrippingUiKey.Key);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -39,6 +42,14 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
UserInterface.OnReceiveMessage += HandleUserInterfaceMessage;
|
||||
}
|
||||
|
||||
Owner.EnsureComponent<InventoryComponent>();
|
||||
Owner.EnsureComponent<HandsComponent>();
|
||||
Owner.EnsureComponent<CuffableComponent>();
|
||||
|
||||
if (Owner.TryGetComponent(out CuffableComponent? cuffed))
|
||||
{
|
||||
cuffed.OnCuffedStateChanged += UpdateSubscribed;
|
||||
}
|
||||
if (Owner.TryGetComponent(out InventoryComponent? inventory))
|
||||
{
|
||||
inventory.OnItemChanged += UpdateSubscribed;
|
||||
@@ -62,8 +73,9 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
|
||||
var inventory = GetInventorySlots();
|
||||
var hands = GetHandSlots();
|
||||
var cuffs = GetHandcuffs();
|
||||
|
||||
UserInterface.SetState(new StrippingBoundUserInterfaceState(inventory, hands));
|
||||
UserInterface.SetState(new StrippingBoundUserInterfaceState(inventory, hands, cuffs));
|
||||
}
|
||||
|
||||
public bool CanDragDrop(DragDropEventArgs eventArgs)
|
||||
@@ -80,6 +92,23 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
return true;
|
||||
}
|
||||
|
||||
private Dictionary<EntityUid, string> GetHandcuffs()
|
||||
{
|
||||
var dictionary = new Dictionary<EntityUid, string>();
|
||||
|
||||
if (!Owner.TryGetComponent(out CuffableComponent? cuffed))
|
||||
{
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
foreach (IEntity entity in cuffed.StoredEntities)
|
||||
{
|
||||
dictionary.Add(entity.Uid, entity.Name);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
private Dictionary<Slots, string> GetInventorySlots()
|
||||
{
|
||||
var dictionary = new Dictionary<Slots, string>();
|
||||
@@ -360,26 +389,46 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
switch (obj.Message)
|
||||
{
|
||||
case StrippingInventoryButtonPressed inventoryMessage:
|
||||
var inventory = Owner.GetComponent<InventoryComponent>();
|
||||
|
||||
if (Owner.TryGetComponent<InventoryComponent>(out var inventory))
|
||||
{
|
||||
if (inventory.TryGetSlotItem(inventoryMessage.Slot, out ItemComponent _))
|
||||
placingItem = false;
|
||||
|
||||
if(placingItem)
|
||||
if (placingItem)
|
||||
PlaceActiveHandItemInInventory(user, inventoryMessage.Slot);
|
||||
else
|
||||
TakeItemFromInventory(user, inventoryMessage.Slot);
|
||||
}
|
||||
break;
|
||||
case StrippingHandButtonPressed handMessage:
|
||||
var hands = Owner.GetComponent<HandsComponent>();
|
||||
|
||||
case StrippingHandButtonPressed handMessage:
|
||||
|
||||
if (Owner.TryGetComponent<HandsComponent>(out var hands))
|
||||
{
|
||||
if (hands.TryGetItem(handMessage.Hand, out _))
|
||||
placingItem = false;
|
||||
|
||||
if(placingItem)
|
||||
if (placingItem)
|
||||
PlaceActiveHandItemInHands(user, handMessage.Hand);
|
||||
else
|
||||
TakeItemFromHands(user, handMessage.Hand);
|
||||
}
|
||||
break;
|
||||
|
||||
case StrippingHandcuffButtonPressed handcuffMessage:
|
||||
|
||||
if (Owner.TryGetComponent<CuffableComponent>(out var cuffed))
|
||||
{
|
||||
foreach (var entity in cuffed.StoredEntities)
|
||||
{
|
||||
if (entity.Uid == handcuffMessage.Handcuff)
|
||||
{
|
||||
cuffed.TryUncuff(user, entity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ namespace Content.Server.GameObjects.Components.Items.Clothing
|
||||
});
|
||||
|
||||
serializer.DataField(ref _quickEquipEnabled, "QuickEquip", true);
|
||||
|
||||
serializer.DataFieldCached(ref _heatResistance, "HeatResistance", 323);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.GameObjects.Components.Body;
|
||||
using Content.Server.GameObjects.Components.Body;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Shared.GameObjects.Components.Movement;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
|
||||
18
Content.Server/GameObjects/EntitySystems/CuffSystem.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Content.Server.GameObjects.Components.ActionBlocking;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
|
||||
namespace Content.Server.GameObjects.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class CuffSystem : EntitySystem
|
||||
{
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
foreach (var comp in ComponentManager.EntityQuery<CuffableComponent>())
|
||||
{
|
||||
comp.Update(frameTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.GameObjects;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Content.Shared.GameObjects.Components.Items;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Robust.Server.GameObjects.Components.Container;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
public class SharedCuffableComponent : Component, IActionBlocker
|
||||
{
|
||||
public override string Name => "Cuffable";
|
||||
public override uint? NetID => ContentNetIDs.CUFFED;
|
||||
|
||||
[ViewVariables]
|
||||
public bool CanStillInteract = true;
|
||||
|
||||
#region ActionBlockers
|
||||
|
||||
bool IActionBlocker.CanInteract() => CanStillInteract;
|
||||
bool IActionBlocker.CanUse() => CanStillInteract;
|
||||
bool IActionBlocker.CanPickup() => CanStillInteract;
|
||||
bool IActionBlocker.CanDrop() => CanStillInteract;
|
||||
bool IActionBlocker.CanAttack() => CanStillInteract;
|
||||
bool IActionBlocker.CanEquip() => CanStillInteract;
|
||||
bool IActionBlocker.CanUnequip() => CanStillInteract;
|
||||
|
||||
#endregion
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
protected sealed class CuffableComponentState : ComponentState
|
||||
{
|
||||
public bool CanStillInteract { get; }
|
||||
public int NumHandsCuffed { get; }
|
||||
public string RSI { get; }
|
||||
public string IconState { get; }
|
||||
public Color Color { get; }
|
||||
|
||||
public CuffableComponentState(int numHandsCuffed, bool canStillInteract, string rsiPath, string iconState, Color color) : base(ContentNetIDs.CUFFED)
|
||||
{
|
||||
NumHandsCuffed = numHandsCuffed;
|
||||
CanStillInteract = canStillInteract;
|
||||
RSI = rsiPath;
|
||||
IconState = iconState;
|
||||
Color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
using System;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
public class SharedHandcuffComponent : Component
|
||||
{
|
||||
public override string Name => "Handcuff";
|
||||
public override uint? NetID => ContentNetIDs.HANDCUFFS;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
protected sealed class HandcuffedComponentState : ComponentState
|
||||
{
|
||||
public string IconState { get; }
|
||||
|
||||
public HandcuffedComponentState(string iconState) : base(ContentNetIDs.HANDCUFFS)
|
||||
{
|
||||
IconState = iconState;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,16 +41,29 @@ namespace Content.Shared.GameObjects.Components.GUI
|
||||
}
|
||||
}
|
||||
|
||||
[NetSerializable, Serializable]
|
||||
public class StrippingHandcuffButtonPressed : BoundUserInterfaceMessage
|
||||
{
|
||||
public EntityUid Handcuff { get; }
|
||||
|
||||
public StrippingHandcuffButtonPressed(EntityUid handcuff)
|
||||
{
|
||||
Handcuff = handcuff;
|
||||
}
|
||||
}
|
||||
|
||||
[NetSerializable, Serializable]
|
||||
public class StrippingBoundUserInterfaceState : BoundUserInterfaceState
|
||||
{
|
||||
public Dictionary<Slots, string> Inventory { get; }
|
||||
public Dictionary<string, string> Hands { get; }
|
||||
public Dictionary<EntityUid, string> Handcuffs { get; }
|
||||
|
||||
public StrippingBoundUserInterfaceState(Dictionary<Slots, string> inventory, Dictionary<string, string> hands)
|
||||
public StrippingBoundUserInterfaceState(Dictionary<Slots, string> inventory, Dictionary<string, string> hands, Dictionary<EntityUid, string> handcuffs)
|
||||
{
|
||||
Inventory = inventory;
|
||||
Hands = hands;
|
||||
Handcuffs = handcuffs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
@@ -58,6 +58,7 @@ namespace Content.Shared.GameObjects.Components.Mobs
|
||||
Thirst,
|
||||
Pressure,
|
||||
Stun,
|
||||
Cuffed,
|
||||
Buckled,
|
||||
Piloting,
|
||||
Pulling,
|
||||
|
||||
@@ -68,6 +68,8 @@
|
||||
public const uint BOLTACTION_BARREL = 1062;
|
||||
public const uint PUMP_BARREL = 1063;
|
||||
public const uint REVOLVER_BARREL = 1064;
|
||||
public const uint CUFFED = 1065;
|
||||
public const uint HANDCUFFS = 1066;
|
||||
|
||||
// Net IDs for integration tests.
|
||||
public const uint PREDICTION_TEST = 10001;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Preferences.Appearance
|
||||
@@ -19,6 +19,7 @@ namespace Content.Shared.Preferences.Appearance
|
||||
LLeg,
|
||||
RFoot,
|
||||
LFoot,
|
||||
Handcuffs,
|
||||
StencilMask
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Resources/Audio/Items/Handcuffs/cuff_breakout_start.ogg
Normal file
BIN
Resources/Audio/Items/Handcuffs/cuff_end.ogg
Normal file
BIN
Resources/Audio/Items/Handcuffs/cuff_start.ogg
Normal file
BIN
Resources/Audio/Items/Handcuffs/cuff_takeoff_end.ogg
Normal file
BIN
Resources/Audio/Items/Handcuffs/cuff_takeoff_start.ogg
Normal file
BIN
Resources/Audio/Items/Handcuffs/rope_breakout.ogg
Normal file
BIN
Resources/Audio/Items/Handcuffs/rope_end.ogg
Normal file
BIN
Resources/Audio/Items/Handcuffs/rope_start.ogg
Normal file
BIN
Resources/Audio/Items/Handcuffs/rope_takeoff.ogg
Normal file
@@ -90,6 +90,10 @@
|
||||
color: "#e8b59b"
|
||||
sprite: Mobs/Species/Human/parts.rsi
|
||||
state: r_foot
|
||||
- map: ["enum.HumanoidVisualLayers.Handcuffs"]
|
||||
color: "#ffffff"
|
||||
sprite: Objects/Misc/handcuffs.rsi
|
||||
state: body-overlay-2
|
||||
- map: ["enum.Slots.IDCARD"]
|
||||
- map: ["enum.Slots.GLOVES"]
|
||||
- map: ["enum.Slots.SHOES"]
|
||||
@@ -144,6 +148,7 @@
|
||||
- type: BuckleVisualizer
|
||||
- type: CombatMode
|
||||
- type: Climbing
|
||||
- type: Cuffable
|
||||
- type: Teleportable
|
||||
- type: CharacterInfo
|
||||
- type: FootstepSound
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
- type: entity
|
||||
name: handcuffs
|
||||
description: Just a prop for screenshots for now, sorry!
|
||||
description: Used to detain criminals and other assholes.
|
||||
id: Handcuffs
|
||||
parent: BaseItem
|
||||
components:
|
||||
- type: Handcuff
|
||||
cuffTime: 3.0
|
||||
uncuffTime: 3.0
|
||||
stunBonus: 2.0
|
||||
breakoutTime: 20.0
|
||||
cuffedRSI: Objects/Misc/handcuffs.rsi
|
||||
iconState: body-overlay
|
||||
|
||||
- type: Sprite
|
||||
sprite: Objects/Misc/handcuffs.rsi
|
||||
state: handcuff
|
||||
@@ -18,10 +26,29 @@
|
||||
|
||||
|
||||
- type: entity
|
||||
name: cable restraints
|
||||
name: makeshift handcuffs
|
||||
description: Homemade handcuffs crafted from spare cables.
|
||||
id: Cablecuffs
|
||||
parent: Handcuffs
|
||||
components:
|
||||
- type: Handcuff
|
||||
cuffTime: 3.5
|
||||
uncuffTime: 3.5
|
||||
stunBonus: 2.0
|
||||
breakoutTime: 15.0
|
||||
cuffedRSI: Objects/Misc/cablecuffs.rsi
|
||||
bodyIconState: body-overlay
|
||||
color: red
|
||||
breakOnRemove: true
|
||||
brokenIconState: cuff-broken
|
||||
brokenName: broken cables
|
||||
brokenDesc: These cables are broken in several places and don't seem very useful.
|
||||
startCuffSound: /Audio/Items/Handcuffs/rope_start.ogg
|
||||
endCuffSound: /Audio/Items/Handcuffs/rope_end.ogg
|
||||
startUncuffSound: /Audio/Items/Handcuffs/rope_start.ogg
|
||||
endUncuffSound: /Audio/Items/Handcuffs/rope_breakout.ogg
|
||||
startBreakoutSound: /Audio/Items/Handcuffs/rope_takeoff.ogg
|
||||
|
||||
- type: Sprite
|
||||
sprite: Objects/Misc/cablecuffs.rsi
|
||||
state: cuff
|
||||
@@ -30,7 +57,9 @@
|
||||
- type: Icon
|
||||
sprite: Objects/Misc/cablecuffs.rsi
|
||||
state: cuff
|
||||
color: red
|
||||
|
||||
- type: Clothing
|
||||
sprite: Objects/Misc/cablecuffs.rsi
|
||||
color: red
|
||||
Slots: [belt]
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 97 B |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Resources/Textures/Objects/Misc/cablecuffs.rsi/cuff-broken.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 333 B After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 204 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 209 B After Width: | Height: | Size: 3.0 KiB |
@@ -13,6 +13,51 @@
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cuff-broken",
|
||||
"directions": 1,
|
||||
"delays": [
|
||||
[
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "body-overlay-2",
|
||||
"directions": 4,
|
||||
"delays": [
|
||||
[
|
||||
1
|
||||
],
|
||||
[
|
||||
1
|
||||
],
|
||||
[
|
||||
1
|
||||
],
|
||||
[
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "body-overlay-4",
|
||||
"directions": 4,
|
||||
"delays": [
|
||||
[
|
||||
1
|
||||
],
|
||||
[
|
||||
1
|
||||
],
|
||||
[
|
||||
1
|
||||
],
|
||||
[
|
||||
1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "inhand-left",
|
||||
|
||||
BIN
Resources/Textures/Objects/Misc/handcuffs.rsi/body-overlay-2.png
Normal file
|
After Width: | Height: | Size: 258 B |
BIN
Resources/Textures/Objects/Misc/handcuffs.rsi/body-overlay-4.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
@@ -1 +1,8 @@
|
||||
{"version": 1, "size": {"x": 32, "y": 32}, "states": [{"name": "handcuff", "directions": 1, "delays": [[1.0]]}, {"name": "inhand-left", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "inhand-right", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "equipped-BELT", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}]}
|
||||
{"version": 1, "size": {"x": 32, "y": 32}, "states": [
|
||||
{"name": "body-overlay-2", "directions": 4, "delays": [[1.0],[1.0],[1.0],[1.0]]},
|
||||
{"name": "body-overlay-4", "directions": 4, "delays": [[1.0],[1.0],[1.0],[1.0]]},
|
||||
{"name": "handcuff", "directions": 1, "delays": [[1.0]]},
|
||||
{"name": "inhand-left", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]},
|
||||
{"name": "inhand-right", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]},
|
||||
{"name": "equipped-BELT", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}
|
||||
]}
|
||||