Cuff enhancements (#3087)
* Cuff enhancements * Cuffs now have an OnClick for the alert to remove them * nullables * Use default interaction range so highlights are accurate * Cuffing fails more gracely * Make shared abstract and add component references to client / server * Don't cache AudioSystem and HandsComponent given cuffs are rarely used * Fix test Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
#nullable enable
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.GameObjects;
|
||||
@@ -6,9 +7,10 @@ using Robust.Shared.GameObjects;
|
||||
namespace Content.Client.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedHandcuffComponent))]
|
||||
public class HandcuffComponent : SharedHandcuffComponent
|
||||
{
|
||||
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
if (curState is not HandcuffedComponentState state)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
|
||||
[TestFixture]
|
||||
[TestOf(typeof(CuffableComponent))]
|
||||
[TestOf(typeof(HandcuffComponent))]
|
||||
public class CuffUnitTest : ContentIntegrationTest
|
||||
public class HandCuffTest : ContentIntegrationTest
|
||||
{
|
||||
private const string PROTOTYPES = @"
|
||||
- type: entity
|
||||
@@ -77,7 +77,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
|
||||
Assert.True(secondCuffs.TryGetComponent(out secondHandcuff!), $"Second handcuffs has no {nameof(HandcuffComponent)}");
|
||||
|
||||
// Test to ensure cuffed players register the handcuffs
|
||||
cuffed.AddNewCuffs(cuffs);
|
||||
cuffed.TryAddNewCuffs(human, 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
|
||||
@@ -86,7 +86,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
|
||||
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(secondCuffs);
|
||||
cuffed.TryAddNewCuffs(human, secondCuffs);
|
||||
Assert.True(cuffed.CuffedHandCount == 4, "Player doesn't have correct amount of hands cuffed");
|
||||
|
||||
});
|
||||
25
Content.Server/Alert/Click/RemoveCuffs.cs
Normal file
25
Content.Server/Alert/Click/RemoveCuffs.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
#nullable enable
|
||||
using Content.Server.GameObjects.Components.ActionBlocking;
|
||||
using Content.Shared.Alert;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Alert.Click
|
||||
{
|
||||
/// <summary>
|
||||
/// Try to remove handcuffs from yourself
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class RemoveCuffs : IAlertClick
|
||||
{
|
||||
public void ExposeData(ObjectSerializer serializer) {}
|
||||
|
||||
public void AlertClicked(ClickAlertEventArgs args)
|
||||
{
|
||||
if (args.Player.TryGetComponent(out CuffableComponent? cuffableComponent))
|
||||
{
|
||||
cuffableComponent.TryUncuff(args.Player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Shared.Interfaces;
|
||||
@@ -46,24 +44,20 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
private Container _container = default!;
|
||||
|
||||
private float _interactRange;
|
||||
private IHandsComponent _hands;
|
||||
// TODO: Make a component message
|
||||
public event Action? OnCuffedStateChanged;
|
||||
|
||||
public event Action OnCuffedStateChanged;
|
||||
private bool _uncuffing;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_container = ContainerManagerComponent.Ensure<Container>(Name, Owner);
|
||||
_interactRange = SharedInteractionSystem.InteractionRange / 2;
|
||||
|
||||
Owner.EntityManager.EventBus.SubscribeEvent<HandCountChangedEvent>(EventSource.Local, this, HandleHandCountChange);
|
||||
|
||||
if (!Owner.TryGetComponent(out _hands))
|
||||
{
|
||||
Logger.Warning("Player does not have an IHandsComponent!");
|
||||
}
|
||||
Owner.EnsureComponentWarn<HandsComponent>();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
@@ -99,27 +93,35 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
/// Add a set of cuffs to an existing CuffedComponent.
|
||||
/// </summary>
|
||||
/// <param name="prototype"></param>
|
||||
public void AddNewCuffs(IEntity handcuff)
|
||||
public bool TryAddNewCuffs(IEntity user, IEntity handcuff)
|
||||
{
|
||||
if (!handcuff.HasComponent<HandcuffComponent>())
|
||||
{
|
||||
Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!handcuff.InRangeUnobstructed(Owner, _interactRange))
|
||||
if (!handcuff.InRangeUnobstructed(Owner))
|
||||
{
|
||||
Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!");
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Success!
|
||||
if (user.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.IsHolding(handcuff))
|
||||
{
|
||||
// Good lord handscomponent is scuffed, I hope some smug person will fix it someday
|
||||
handsComponent.Drop(handcuff);
|
||||
}
|
||||
|
||||
_container.Insert(handcuff);
|
||||
CanStillInteract = _hands.Hands.Count() > CuffedHandCount;
|
||||
CanStillInteract = Owner.TryGetComponent(out HandsComponent? ownerHands) && ownerHands.Hands.Count() > CuffedHandCount;
|
||||
|
||||
OnCuffedStateChanged?.Invoke();
|
||||
UpdateAlert();
|
||||
UpdateHeldItems();
|
||||
Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -128,7 +130,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
private void UpdateHandCount()
|
||||
{
|
||||
var dirty = false;
|
||||
var handCount = _hands.Hands.Count();
|
||||
var handCount = Owner.TryGetComponent(out HandsComponent? handsComponent) ? handsComponent.Hands.Count() : 0;
|
||||
|
||||
while (CuffedHandCount > handCount && CuffedHandCount > 0)
|
||||
{
|
||||
@@ -142,7 +144,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
if (dirty)
|
||||
{
|
||||
CanStillInteract = handCount > CuffedHandCount;
|
||||
OnCuffedStateChanged.Invoke();
|
||||
OnCuffedStateChanged?.Invoke();
|
||||
Dirty();
|
||||
}
|
||||
}
|
||||
@@ -160,17 +162,19 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
/// </summary>
|
||||
public void UpdateHeldItems()
|
||||
{
|
||||
var itemCount = _hands.GetAllHeldItems().Count();
|
||||
var freeHandCount = _hands.Hands.Count() - CuffedHandCount;
|
||||
if (!Owner.TryGetComponent(out HandsComponent? handsComponent)) return;
|
||||
|
||||
var itemCount = handsComponent.GetAllHeldItems().Count();
|
||||
var freeHandCount = handsComponent.Hands.Count() - CuffedHandCount;
|
||||
|
||||
if (freeHandCount < itemCount)
|
||||
{
|
||||
foreach (var item in _hands.GetAllHeldItems())
|
||||
foreach (var item in handsComponent.GetAllHeldItems())
|
||||
{
|
||||
if (freeHandCount < itemCount)
|
||||
{
|
||||
freeHandCount++;
|
||||
_hands.Drop(item.Owner, false);
|
||||
handsComponent.Drop(item.Owner, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -185,7 +189,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
/// </summary>
|
||||
private void UpdateAlert()
|
||||
{
|
||||
if (Owner.TryGetComponent(out ServerAlertsComponent status))
|
||||
if (Owner.TryGetComponent(out ServerAlertsComponent? status))
|
||||
{
|
||||
if (CanStillInteract)
|
||||
{
|
||||
@@ -204,8 +208,10 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
/// </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)
|
||||
public async void TryUncuff(IEntity user, IEntity? cuffsToRemove = null)
|
||||
{
|
||||
if (_uncuffing) return;
|
||||
|
||||
var isOwner = user == Owner;
|
||||
|
||||
if (cuffsToRemove == null)
|
||||
@@ -232,13 +238,13 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwner && !user.InRangeUnobstructed(Owner, _interactRange))
|
||||
if (!isOwner && !user.InRangeUnobstructed(Owner))
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("You are too far away to remove the cuffs."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cuffsToRemove.InRangeUnobstructed(Owner, _interactRange))
|
||||
if (!cuffsToRemove.InRangeUnobstructed(Owner))
|
||||
{
|
||||
Logger.Warning("Handcuffs being removed from player are obstructed or too far away! This should not happen!");
|
||||
return;
|
||||
@@ -247,7 +253,17 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
user.PopupMessage(Loc.GetString("You start removing the cuffs."));
|
||||
|
||||
var audio = EntitySystem.Get<AudioSystem>();
|
||||
audio.PlayFromEntity(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, Owner);
|
||||
if (isOwner)
|
||||
{
|
||||
if (cuff.StartBreakoutSound != null)
|
||||
audio.PlayFromEntity(cuff.StartBreakoutSound, Owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cuff.StartUncuffSound != null)
|
||||
audio.PlayFromEntity(cuff.StartUncuffSound, Owner);
|
||||
}
|
||||
|
||||
|
||||
var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
|
||||
var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime)
|
||||
@@ -259,10 +275,15 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
};
|
||||
|
||||
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
|
||||
_uncuffing = true;
|
||||
|
||||
var result = await doAfterSystem.DoAfter(doAfterEventArgs);
|
||||
|
||||
_uncuffing = false;
|
||||
|
||||
if (result != DoAfterStatus.Cancelled)
|
||||
{
|
||||
if (cuff.EndUncuffSound != null)
|
||||
audio.PlayFromEntity(cuff.EndUncuffSound, Owner);
|
||||
|
||||
_container.ForceRemove(cuffsToRemove);
|
||||
@@ -276,14 +297,14 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
cuffsToRemove.Name = cuff.BrokenName;
|
||||
cuffsToRemove.Description = cuff.BrokenDesc;
|
||||
|
||||
if (cuffsToRemove.TryGetComponent<SpriteComponent>(out var sprite))
|
||||
if (cuffsToRemove.TryGetComponent<SpriteComponent>(out var sprite) && cuff.BrokenState != null)
|
||||
{
|
||||
sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state?
|
||||
}
|
||||
}
|
||||
|
||||
CanStillInteract = _hands.Hands.Count() > CuffedHandCount;
|
||||
OnCuffedStateChanged.Invoke();
|
||||
CanStillInteract = Owner.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.Hands.Count() > CuffedHandCount;
|
||||
OnCuffedStateChanged?.Invoke();
|
||||
UpdateAlert();
|
||||
Dirty();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||
using Content.Shared.GameObjects.Components.ActionBlocking;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
@@ -14,7 +14,6 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
@@ -22,6 +21,7 @@ using Robust.Shared.ViewVariables;
|
||||
namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedHandcuffComponent))]
|
||||
public class HandcuffComponent : SharedHandcuffComponent, IAfterInteract
|
||||
{
|
||||
/// <summary>
|
||||
@@ -58,31 +58,31 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
/// The path of the RSI file used for the player cuffed overlay.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string CuffedRSI { get; set; }
|
||||
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; }
|
||||
public string? OverlayIconState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used for broken handcuffs
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string BrokenState { get; set; }
|
||||
public string? BrokenState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used for broken handcuffs
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string BrokenName { get; set; }
|
||||
public string BrokenName { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used for broken handcuffs
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string BrokenDesc { get; set; }
|
||||
public string BrokenDesc { get; set; } = default!;
|
||||
|
||||
[ViewVariables]
|
||||
public bool Broken
|
||||
@@ -102,25 +102,20 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
}
|
||||
}
|
||||
|
||||
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 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 AudioSystem _audioSystem;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_audioSystem = EntitySystem.Get<AudioSystem>();
|
||||
_interactRange = SharedInteractionSystem.InteractionRange / 2;
|
||||
}
|
||||
/// <summary>
|
||||
/// Used to prevent DoAfter getting spammed.
|
||||
/// </summary>
|
||||
private bool _cuffing;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
@@ -150,6 +145,8 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (_cuffing) return true;
|
||||
|
||||
if (eventArgs.Target == null || !ActionBlockerSystem.CanUse(eventArgs.User) || !eventArgs.Target.TryGetComponent<CuffableComponent>(out var cuffed))
|
||||
{
|
||||
return false;
|
||||
@@ -179,7 +176,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!eventArgs.InRangeUnobstructed(_interactRange, ignoreInsideBlocker: true))
|
||||
if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true))
|
||||
{
|
||||
eventArgs.User.PopupMessage(Loc.GetString("You are too far away to use the cuffs!"));
|
||||
return true;
|
||||
@@ -187,7 +184,9 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
|
||||
eventArgs.User.PopupMessage(Loc.GetString("You start cuffing {0:theName}.", eventArgs.Target));
|
||||
eventArgs.User.PopupMessage(eventArgs.Target, Loc.GetString("{0:theName} starts cuffing you!", eventArgs.User));
|
||||
_audioSystem.PlayFromEntity(StartCuffSound, Owner);
|
||||
|
||||
if (StartCuffSound != null)
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(StartCuffSound, Owner);
|
||||
|
||||
TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed);
|
||||
return true;
|
||||
@@ -214,22 +213,21 @@ namespace Content.Server.GameObjects.Components.ActionBlocking
|
||||
NeedHand = true
|
||||
};
|
||||
|
||||
_cuffing = true;
|
||||
|
||||
var result = await EntitySystem.Get<DoAfterSystem>().DoAfter(doAfterEventArgs);
|
||||
|
||||
_cuffing = false;
|
||||
|
||||
if (result != DoAfterStatus.Cancelled)
|
||||
{
|
||||
_audioSystem.PlayFromEntity(EndCuffSound, Owner);
|
||||
if (cuffs.TryAddNewCuffs(user, Owner))
|
||||
{
|
||||
if (EndCuffSound != null)
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(EndCuffSound, Owner);
|
||||
|
||||
user.PopupMessage(Loc.GetString("You successfully cuff {0:theName}.", target));
|
||||
target.PopupMessage(Loc.GetString("You have been cuffed by {0:theName}!", user));
|
||||
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.ActionBlocking
|
||||
{
|
||||
public class SharedHandcuffComponent : Component
|
||||
public abstract class SharedHandcuffComponent : Component
|
||||
{
|
||||
public override string Name => "Handcuff";
|
||||
public override uint? NetID => ContentNetIDs.HANDCUFFS;
|
||||
|
||||
@@ -89,9 +89,10 @@
|
||||
|
||||
- type: alert
|
||||
alertType: Handcuffed
|
||||
onClick: !type:RemoveCuffs { }
|
||||
icon: /Textures/Interface/Alerts/Handcuffed/Handcuffed.png
|
||||
name: "[color=yellow]Handcuffed[/color]"
|
||||
description: "You're [color=yellow]handcuffed[/color] and can't use your hands. If anyone drags you, you won't be able to resist.."
|
||||
description: "You're [color=yellow]handcuffed[/color] and can't use your hands. If anyone drags you, you won't be able to resist."
|
||||
|
||||
- type: alert
|
||||
alertType: Buckled
|
||||
|
||||
Reference in New Issue
Block a user