diff --git a/Content.Client/GameObjects/Components/ActionBlocking/CuffableComponent.cs b/Content.Client/GameObjects/Components/ActionBlocking/CuffableComponent.cs new file mode 100644 index 0000000000..344e2d0c2c --- /dev/null +++ b/Content.Client/GameObjects/Components/ActionBlocking/CuffableComponent.cs @@ -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(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(out var sprite)) + { + sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/ActionBlocking/HandcuffComponent.cs b/Content.Client/GameObjects/Components/ActionBlocking/HandcuffComponent.cs new file mode 100644 index 0000000000..62c2204cec --- /dev/null +++ b/Content.Client/GameObjects/Components/ActionBlocking/HandcuffComponent.cs @@ -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(out var sprite)) + { + sprite.LayerSetState(0, new RSI.StateId(cuffState.IconState)); // TODO: safety check to see if RSI contains the state? + } + } + } +} diff --git a/Content.Client/GameObjects/Components/HUD/Inventory/StrippableBoundUserInterface.cs b/Content.Client/GameObjects/Components/HUD/Inventory/StrippableBoundUserInterface.cs index 2b4f518501..2fd1c8ee66 100644 --- a/Content.Client/GameObjects/Components/HUD/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/GameObjects/Components/HUD/Inventory/StrippableBoundUserInterface.cs @@ -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 Inventory { get; private set; } public Dictionary Hands { get; private set; } + public Dictionary 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) => @@ -66,6 +72,18 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory SendMessage(new StrippingHandButtonPressed(hand)); }); } + } + + 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) @@ -76,6 +94,7 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory Inventory = stripState.Inventory; Hands = stripState.Hands; + Handcuffs = stripState.Handcuffs; UpdateMenu(); } diff --git a/Content.Client/GameObjects/Components/Items/HandsComponent.cs b/Content.Client/GameObjects/Components/Items/HandsComponent.cs index f023c467ee..4a11eeb584 100644 --- a/Content.Client/GameObjects/Components/Items/HandsComponent.cs +++ b/Content.Client/GameObjects/Components/Items/HandsComponent.cs @@ -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); } diff --git a/Content.Client/GameObjects/Components/Items/ItemComponent.cs b/Content.Client/GameObjects/Components/Items/ItemComponent.cs index 812c5223d0..b84e040fa4 100644 --- a/Content.Client/GameObjects/Components/Items/ItemComponent.cs +++ b/Content.Client/GameObjects/Components/Items/ItemComponent.cs @@ -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); } diff --git a/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs index 0ddf3f175a..dea7c697df 100644 --- a/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs +++ b/Content.Client/GameObjects/Components/Mobs/HumanoidAppearanceComponent.cs @@ -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(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; diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs new file mode 100644 index 0000000000..ea8d5ea324 --- /dev/null +++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs @@ -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(); + mapManager.CreateNewMapEntity(MapId.Nullspace); + + var entityManager = IoCManager.Resolve(); + + // 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(); + 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); + } + } +} diff --git a/Content.Server/Body/BodyCommands.cs b/Content.Server/Body/BodyCommands.cs index 9809a5845f..da8c9e4ca1 100644 --- a/Content.Server/Body/BodyCommands.cs +++ b/Content.Server/Body/BodyCommands.cs @@ -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(); - 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(); diff --git a/Content.Server/GameObjects/Components/ActionBlocking/CuffableComponent.cs b/Content.Server/GameObjects/Components/ActionBlocking/CuffableComponent.cs new file mode 100644 index 0000000000..4d07bc1d54 --- /dev/null +++ b/Content.Server/GameObjects/Components/ActionBlocking/CuffableComponent.cs @@ -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; + + /// + /// How many of this entity's hands are currently cuffed. + /// + [ViewVariables] + public int CuffedHandCount => _container.ContainedEntities.Count * 2; + + protected IEntity LastAddedCuffs => _container.ContainedEntities[_container.ContainedEntities.Count - 1]; + + public IReadOnlyList StoredEntities => _container.ContainedEntities; + + /// + /// Container of various handcuffs currently applied to the entity. + /// + [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(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(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); + } + + /// + /// Add a set of cuffs to an existing CuffedComponent. + /// + /// + public void AddNewCuffs(IEntity handcuff) + { + if (!handcuff.HasComponent()) + { + Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!"); + return; + } + + if (!EntitySystem.Get().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(); + } + + /// + /// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs. + /// + 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(); + } + } + + /// + /// Check how many items the user is holding and if it's more than the number of cuffed hands, drop some items. + /// + 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; + } + } + } + } + + /// + /// Updates the status effect indicator on the HUD. + /// + 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"); + } + } + + /// + /// 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. + /// + /// The cuffed entity + /// Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity. + 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(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().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().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(); + 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(); + 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(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; + } + + /// + /// Allows the uncuffing of a cuffed person. Used by other people and by the component owner to break out of cuffs. + /// + [Verb] + private sealed class UncuffVerb : Verb + { + 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); + } + } + } + } +} diff --git a/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs b/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs new file mode 100644 index 0000000000..ea855cf6bd --- /dev/null +++ b/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs @@ -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; + + /// + /// The time it takes to apply a to an entity. + /// + [ViewVariables] + public float CuffTime { get; set; } + + /// + /// The time it takes to remove a from an entity. + /// + [ViewVariables] + public float UncuffTime { get; set; } + + /// + /// The time it takes for a cuffed entity to remove from itself. + /// + [ViewVariables] + public float BreakoutTime { get; set; } + + /// + /// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs. + /// + [ViewVariables] + public float StunBonus { get; set; } + + /// + /// Will the cuffs break when removed? + /// + [ViewVariables] + public bool BreakOnRemove { get; set; } + + /// + /// The path of the RSI file used for the player cuffed overlay. + /// + [ViewVariables] + public string CuffedRSI { get; set; } + + /// + /// The iconstate used with the RSI file for the player cuffed overlay. + /// + [ViewVariables] + public string OverlayIconState { get; set; } + + /// + /// The iconstate used for broken handcuffs + /// + [ViewVariables] + public string BrokenState { get; set; } + + /// + /// The iconstate used for broken handcuffs + /// + [ViewVariables] + public string BrokenName { get; set; } + + /// + /// The iconstate used for broken handcuffs + /// + [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(); + _doAfterSystem = EntitySystem.Get(); + _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(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(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().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); + } + + /// + /// Update the cuffed state of an entity + /// + private async void TryUpdateCuff(IEntity user, IEntity target, CuffableComponent cuffs) + { + var cuffTime = CuffTime; + + if (target.TryGetComponent(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(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)); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/GUI/StrippableComponent.cs b/Content.Server/GameObjects/Components/GUI/StrippableComponent.cs index ec8ba05e42..4e43e0be99 100644 --- a/Content.Server/GameObjects/Components/GUI/StrippableComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/StrippableComponent.cs @@ -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(); + Owner.EnsureComponent(); + Owner.EnsureComponent(); + + 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 GetHandcuffs() + { + var dictionary = new Dictionary(); + + if (!Owner.TryGetComponent(out CuffableComponent? cuffed)) + { + return dictionary; + } + + foreach (IEntity entity in cuffed.StoredEntities) + { + dictionary.Add(entity.Uid, entity.Name); + } + + return dictionary; + } + private Dictionary GetInventorySlots() { var dictionary = new Dictionary(); @@ -360,26 +389,46 @@ namespace Content.Server.GameObjects.Components.GUI switch (obj.Message) { case StrippingInventoryButtonPressed inventoryMessage: - var inventory = Owner.GetComponent(); - if (inventory.TryGetSlotItem(inventoryMessage.Slot, out ItemComponent _)) - placingItem = false; + if (Owner.TryGetComponent(out var inventory)) + { + if (inventory.TryGetSlotItem(inventoryMessage.Slot, out ItemComponent _)) + placingItem = false; - if(placingItem) - PlaceActiveHandItemInInventory(user, inventoryMessage.Slot); - else - TakeItemFromInventory(user, inventoryMessage.Slot); + if (placingItem) + PlaceActiveHandItemInInventory(user, inventoryMessage.Slot); + else + TakeItemFromInventory(user, inventoryMessage.Slot); + } break; + case StrippingHandButtonPressed handMessage: - var hands = Owner.GetComponent(); - if (hands.TryGetItem(handMessage.Hand, out _)) - placingItem = false; + if (Owner.TryGetComponent(out var hands)) + { + if (hands.TryGetItem(handMessage.Hand, out _)) + placingItem = false; - if(placingItem) - PlaceActiveHandItemInHands(user, handMessage.Hand); - else - TakeItemFromHands(user, handMessage.Hand); + if (placingItem) + PlaceActiveHandItemInHands(user, handMessage.Hand); + else + TakeItemFromHands(user, handMessage.Hand); + } + break; + + case StrippingHandcuffButtonPressed handcuffMessage: + + if (Owner.TryGetComponent(out var cuffed)) + { + foreach (var entity in cuffed.StoredEntities) + { + if (entity.Uid == handcuffMessage.Handcuff) + { + cuffed.TryUncuff(user, entity); + return; + } + } + } break; } } diff --git a/Content.Server/GameObjects/Components/Items/Clothing/ClothingComponent.cs b/Content.Server/GameObjects/Components/Items/Clothing/ClothingComponent.cs index 0690744f42..c8fe5b8b78 100644 --- a/Content.Server/GameObjects/Components/Items/Clothing/ClothingComponent.cs +++ b/Content.Server/GameObjects/Components/Items/Clothing/ClothingComponent.cs @@ -61,7 +61,6 @@ namespace Content.Server.GameObjects.Components.Items.Clothing }); serializer.DataField(ref _quickEquipEnabled, "QuickEquip", true); - serializer.DataFieldCached(ref _heatResistance, "HeatResistance", 323); } diff --git a/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs b/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs index 2fce1dc185..f04b50cff2 100644 --- a/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs +++ b/Content.Server/GameObjects/Components/Movement/ClimbableComponent.cs @@ -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; diff --git a/Content.Server/GameObjects/EntitySystems/CuffSystem.cs b/Content.Server/GameObjects/EntitySystems/CuffSystem.cs new file mode 100644 index 0000000000..f08ddae3ba --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/CuffSystem.cs @@ -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()) + { + comp.Update(frameTime); + } + } + } +} diff --git a/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs b/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs index 5765098531..832fb7858f 100644 --- a/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs +++ b/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs @@ -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; diff --git a/Content.Shared/GameObjects/Components/ActionBlocking/SharedCuffableComponent.cs b/Content.Shared/GameObjects/Components/ActionBlocking/SharedCuffableComponent.cs new file mode 100644 index 0000000000..9d82443e31 --- /dev/null +++ b/Content.Shared/GameObjects/Components/ActionBlocking/SharedCuffableComponent.cs @@ -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; + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/ActionBlocking/SharedHandcuffComponent.cs b/Content.Shared/GameObjects/Components/ActionBlocking/SharedHandcuffComponent.cs new file mode 100644 index 0000000000..c707802918 --- /dev/null +++ b/Content.Shared/GameObjects/Components/ActionBlocking/SharedHandcuffComponent.cs @@ -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; + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/GUI/SharedStrippableComponent.cs b/Content.Shared/GameObjects/Components/GUI/SharedStrippableComponent.cs index bcc9b2fc7c..c498f21e8f 100644 --- a/Content.Shared/GameObjects/Components/GUI/SharedStrippableComponent.cs +++ b/Content.Shared/GameObjects/Components/GUI/SharedStrippableComponent.cs @@ -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 Inventory { get; } public Dictionary Hands { get; } + public Dictionary Handcuffs { get; } - public StrippingBoundUserInterfaceState(Dictionary inventory, Dictionary hands) + public StrippingBoundUserInterfaceState(Dictionary inventory, Dictionary hands, Dictionary handcuffs) { Inventory = inventory; Hands = hands; + Handcuffs = handcuffs; } } } diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs index 189476b442..c76a275ddd 100644 --- a/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs @@ -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, diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index 2eb1e59b58..365ca7aad4 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -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; diff --git a/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs b/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs index 72b53675c1..d1d57c64df 100644 --- a/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Preferences/Appearance/HumanoidCharacterAppearance.cs @@ -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 } } diff --git a/Resources/Audio/Items/Handcuffs/cuff_breakout_start.ogg b/Resources/Audio/Items/Handcuffs/cuff_breakout_start.ogg new file mode 100644 index 0000000000..f58274219d Binary files /dev/null and b/Resources/Audio/Items/Handcuffs/cuff_breakout_start.ogg differ diff --git a/Resources/Audio/Items/Handcuffs/cuff_end.ogg b/Resources/Audio/Items/Handcuffs/cuff_end.ogg new file mode 100644 index 0000000000..3249a5a78f Binary files /dev/null and b/Resources/Audio/Items/Handcuffs/cuff_end.ogg differ diff --git a/Resources/Audio/Items/Handcuffs/cuff_start.ogg b/Resources/Audio/Items/Handcuffs/cuff_start.ogg new file mode 100644 index 0000000000..5dfae57c20 Binary files /dev/null and b/Resources/Audio/Items/Handcuffs/cuff_start.ogg differ diff --git a/Resources/Audio/Items/Handcuffs/cuff_takeoff_end.ogg b/Resources/Audio/Items/Handcuffs/cuff_takeoff_end.ogg new file mode 100644 index 0000000000..e608a2c128 Binary files /dev/null and b/Resources/Audio/Items/Handcuffs/cuff_takeoff_end.ogg differ diff --git a/Resources/Audio/Items/Handcuffs/cuff_takeoff_start.ogg b/Resources/Audio/Items/Handcuffs/cuff_takeoff_start.ogg new file mode 100644 index 0000000000..70b6d5f0fc Binary files /dev/null and b/Resources/Audio/Items/Handcuffs/cuff_takeoff_start.ogg differ diff --git a/Resources/Audio/Items/Handcuffs/rope_breakout.ogg b/Resources/Audio/Items/Handcuffs/rope_breakout.ogg new file mode 100644 index 0000000000..f255d7d78b Binary files /dev/null and b/Resources/Audio/Items/Handcuffs/rope_breakout.ogg differ diff --git a/Resources/Audio/Items/Handcuffs/rope_end.ogg b/Resources/Audio/Items/Handcuffs/rope_end.ogg new file mode 100644 index 0000000000..427f94c6aa Binary files /dev/null and b/Resources/Audio/Items/Handcuffs/rope_end.ogg differ diff --git a/Resources/Audio/Items/Handcuffs/rope_start.ogg b/Resources/Audio/Items/Handcuffs/rope_start.ogg new file mode 100644 index 0000000000..7a7720530f Binary files /dev/null and b/Resources/Audio/Items/Handcuffs/rope_start.ogg differ diff --git a/Resources/Audio/Items/Handcuffs/rope_takeoff.ogg b/Resources/Audio/Items/Handcuffs/rope_takeoff.ogg new file mode 100644 index 0000000000..47adf2fca8 Binary files /dev/null and b/Resources/Audio/Items/Handcuffs/rope_takeoff.ogg differ diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index aabc6022cc..241fac47ca 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml index 74b2955efe..51544fabf1 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml @@ -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 - Slots: [belt] + color: red + Slots: [belt] \ No newline at end of file diff --git a/Resources/Textures/Interface/StatusEffects/Handcuffed/Handcuffed.png b/Resources/Textures/Interface/StatusEffects/Handcuffed/Handcuffed.png new file mode 100644 index 0000000000..3536af53b2 Binary files /dev/null and b/Resources/Textures/Interface/StatusEffects/Handcuffed/Handcuffed.png differ diff --git a/Resources/Textures/Interface/StatusEffects/Handcuffed/Uncuffed.png b/Resources/Textures/Interface/StatusEffects/Handcuffed/Uncuffed.png new file mode 100644 index 0000000000..6879bd394f Binary files /dev/null and b/Resources/Textures/Interface/StatusEffects/Handcuffed/Uncuffed.png differ diff --git a/Resources/Textures/Objects/Misc/cablecuffs.rsi/body-overlay-2.png b/Resources/Textures/Objects/Misc/cablecuffs.rsi/body-overlay-2.png new file mode 100644 index 0000000000..8c3c8a53fd Binary files /dev/null and b/Resources/Textures/Objects/Misc/cablecuffs.rsi/body-overlay-2.png differ diff --git a/Resources/Textures/Objects/Misc/cablecuffs.rsi/body-overlay-4.png b/Resources/Textures/Objects/Misc/cablecuffs.rsi/body-overlay-4.png new file mode 100644 index 0000000000..ff0d583777 Binary files /dev/null and b/Resources/Textures/Objects/Misc/cablecuffs.rsi/body-overlay-4.png differ diff --git a/Resources/Textures/Objects/Misc/cablecuffs.rsi/cuff-broken.png b/Resources/Textures/Objects/Misc/cablecuffs.rsi/cuff-broken.png new file mode 100644 index 0000000000..3da396ef0f Binary files /dev/null and b/Resources/Textures/Objects/Misc/cablecuffs.rsi/cuff-broken.png differ diff --git a/Resources/Textures/Objects/Misc/cablecuffs.rsi/cuff.png b/Resources/Textures/Objects/Misc/cablecuffs.rsi/cuff.png index 4679847291..6e4b284336 100644 Binary files a/Resources/Textures/Objects/Misc/cablecuffs.rsi/cuff.png and b/Resources/Textures/Objects/Misc/cablecuffs.rsi/cuff.png differ diff --git a/Resources/Textures/Objects/Misc/cablecuffs.rsi/inhand-left.png b/Resources/Textures/Objects/Misc/cablecuffs.rsi/inhand-left.png index 3ed7f9d40f..d311c0d128 100644 Binary files a/Resources/Textures/Objects/Misc/cablecuffs.rsi/inhand-left.png and b/Resources/Textures/Objects/Misc/cablecuffs.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Misc/cablecuffs.rsi/inhand-right.png b/Resources/Textures/Objects/Misc/cablecuffs.rsi/inhand-right.png index 9fdcd9f62c..f02cd96249 100644 Binary files a/Resources/Textures/Objects/Misc/cablecuffs.rsi/inhand-right.png and b/Resources/Textures/Objects/Misc/cablecuffs.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Misc/cablecuffs.rsi/meta.json b/Resources/Textures/Objects/Misc/cablecuffs.rsi/meta.json index 2129ef9e34..071dcf8cba 100644 --- a/Resources/Textures/Objects/Misc/cablecuffs.rsi/meta.json +++ b/Resources/Textures/Objects/Misc/cablecuffs.rsi/meta.json @@ -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", diff --git a/Resources/Textures/Objects/Misc/handcuffs.rsi/body-overlay-2.png b/Resources/Textures/Objects/Misc/handcuffs.rsi/body-overlay-2.png new file mode 100644 index 0000000000..7a270dc250 Binary files /dev/null and b/Resources/Textures/Objects/Misc/handcuffs.rsi/body-overlay-2.png differ diff --git a/Resources/Textures/Objects/Misc/handcuffs.rsi/body-overlay-4.png b/Resources/Textures/Objects/Misc/handcuffs.rsi/body-overlay-4.png new file mode 100644 index 0000000000..ae5e8e2b42 Binary files /dev/null and b/Resources/Textures/Objects/Misc/handcuffs.rsi/body-overlay-4.png differ diff --git a/Resources/Textures/Objects/Misc/handcuffs.rsi/meta.json b/Resources/Textures/Objects/Misc/handcuffs.rsi/meta.json index 4fa026ebe6..c6533225fe 100644 --- a/Resources/Textures/Objects/Misc/handcuffs.rsi/meta.json +++ b/Resources/Textures/Objects/Misc/handcuffs.rsi/meta.json @@ -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]]}]} \ No newline at end of file +{"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]]} +]} \ No newline at end of file