Cuffable/Handcuff ECS (#14382)
This commit is contained in:
@@ -1,295 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Server.Recycling.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Cuffs.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedCuffableComponent))]
|
||||
public sealed class CuffableComponent : SharedCuffableComponent
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
|
||||
private bool _uncuffing;
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Owner.EnsureComponentWarn<HandsComponent>();
|
||||
}
|
||||
|
||||
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 (_entMan.TryGetComponent<HandcuffComponent?>(LastAddedCuffs, 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>
|
||||
public bool TryAddNewCuffs(EntityUid user, EntityUid handcuff)
|
||||
{
|
||||
if (!_entMan.HasComponent<HandcuffComponent>(handcuff))
|
||||
{
|
||||
Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(handcuff, Owner))
|
||||
{
|
||||
Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!");
|
||||
return true;
|
||||
}
|
||||
|
||||
var sys = _entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>();
|
||||
|
||||
// Success!
|
||||
sys.TryDrop(user, handcuff);
|
||||
|
||||
Container.Insert(handcuff);
|
||||
UpdateHeldItems(handcuff);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds virtual cuff items to the user's hands.
|
||||
/// </summary>
|
||||
public void UpdateHeldItems(EntityUid handcuff)
|
||||
{
|
||||
// TODO when ecs-ing this, we probably don't just want to use the generic virtual-item entity, and instead
|
||||
// want to add our own item, so that use-in-hand triggers an uncuff attempt and the like.
|
||||
|
||||
if (!_entMan.TryGetComponent(Owner, out HandsComponent? handsComponent)) return;
|
||||
|
||||
var handSys = _entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>();
|
||||
|
||||
var freeHands = 0;
|
||||
foreach (var hand in handSys.EnumerateHands(Owner, handsComponent))
|
||||
{
|
||||
if (hand.HeldEntity == null)
|
||||
{
|
||||
freeHands++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is this entity removable? (it might be an existing handcuff blocker)
|
||||
if (_entMan.HasComponent<UnremoveableComponent>(hand.HeldEntity))
|
||||
continue;
|
||||
|
||||
handSys.DoDrop(Owner, hand, true, handsComponent);
|
||||
freeHands++;
|
||||
if (freeHands == 2)
|
||||
break;
|
||||
}
|
||||
|
||||
var virtSys = _entMan.EntitySysManager.GetEntitySystem<HandVirtualItemSystem>();
|
||||
|
||||
if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem1))
|
||||
_entMan.EnsureComponent<UnremoveableComponent>(virtItem1.Value);
|
||||
|
||||
if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem2))
|
||||
_entMan.EnsureComponent<UnremoveableComponent>(virtItem2.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the status effect indicator on the HUD.
|
||||
/// </summary>
|
||||
private void UpdateAlert()
|
||||
{
|
||||
if (CanStillInteract)
|
||||
{
|
||||
EntitySystem.Get<AlertsSystem>().ClearAlert(Owner, AlertType.Handcuffed);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntitySystem.Get<AlertsSystem>().ShowAlert(Owner, AlertType.Handcuffed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(EntityUid user, EntityUid? cuffsToRemove = null)
|
||||
{
|
||||
if (_uncuffing) return;
|
||||
|
||||
var isOwner = user == Owner;
|
||||
|
||||
if (cuffsToRemove == null)
|
||||
{
|
||||
if (Container.ContainedEntities.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
cuffsToRemove = LastAddedCuffs;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Container.ContainedEntities.Contains(cuffsToRemove.Value))
|
||||
{
|
||||
Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
|
||||
}
|
||||
}
|
||||
|
||||
if (!_entMan.TryGetComponent<HandcuffComponent?>(cuffsToRemove, out var cuff))
|
||||
{
|
||||
Logger.Warning($"A user is trying to remove handcuffs without a {nameof(HandcuffComponent)}. This should never happen!");
|
||||
return;
|
||||
}
|
||||
|
||||
var attempt = new UncuffAttemptEvent(user, Owner);
|
||||
_entMan.EventBus.RaiseLocalEvent(user, attempt, true);
|
||||
|
||||
if (attempt.Cancelled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isOwner && !EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(user, Owner))
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"));
|
||||
return;
|
||||
}
|
||||
|
||||
user.PopupMessage(Loc.GetString("cuffable-component-start-removing-cuffs-message"));
|
||||
|
||||
if (isOwner)
|
||||
{
|
||||
SoundSystem.Play(cuff.StartBreakoutSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
SoundSystem.Play(cuff.StartUncuffSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner);
|
||||
}
|
||||
|
||||
var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
|
||||
var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime, target: Owner)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
NeedHand = true
|
||||
};
|
||||
|
||||
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
|
||||
_uncuffing = true;
|
||||
|
||||
var result = await doAfterSystem.WaitDoAfter(doAfterEventArgs);
|
||||
|
||||
_uncuffing = false;
|
||||
|
||||
if (result != DoAfterStatus.Cancelled)
|
||||
{
|
||||
Uncuff(user, cuffsToRemove.Value, cuff, isOwner);
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-fail-message"));
|
||||
}
|
||||
}
|
||||
|
||||
//Lord forgive me for putting this here
|
||||
//Cuff ECS when
|
||||
public void Uncuff(EntityUid user, EntityUid cuffsToRemove, HandcuffComponent cuff, bool isOwner)
|
||||
{
|
||||
SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner);
|
||||
|
||||
_entMan.EntitySysManager.GetEntitySystem<HandVirtualItemSystem>().DeleteInHandsMatching(user, cuffsToRemove);
|
||||
Container.Remove(cuffsToRemove);
|
||||
|
||||
if (cuff.BreakOnRemove)
|
||||
{
|
||||
_entMan.QueueDeleteEntity(cuffsToRemove);
|
||||
var trash = _entMan.SpawnEntity(cuff.BrokenPrototype, MapCoordinates.Nullspace);
|
||||
_entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, trash);
|
||||
}
|
||||
else
|
||||
{
|
||||
_entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, cuffsToRemove);
|
||||
}
|
||||
|
||||
if (CuffedHandCount == 0)
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-success-message"));
|
||||
|
||||
if (!isOwner)
|
||||
{
|
||||
user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message", ("otherName", user)));
|
||||
}
|
||||
|
||||
if (user == Owner)
|
||||
{
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed themselves");
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed {_entMan.ToPrettyString(Owner):player}");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isOwner)
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount), ("otherName", user)));
|
||||
user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-partial-success-message", ("otherName", user), ("cuffedHandCount", CuffedHandCount)));
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
using Content.Server.Administration.Components;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Stunnable;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Cuffs.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedHandcuffComponent))]
|
||||
public sealed class HandcuffComponent : SharedHandcuffComponent
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes to apply a <see cref="CuffedComponent"/> to an entity.
|
||||
/// </summary>
|
||||
[DataField("cuffTime")]
|
||||
public float CuffTime { get; set; } = 3.5f;
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes to remove a <see cref="CuffedComponent"/> from an entity.
|
||||
/// </summary>
|
||||
[DataField("uncuffTime")]
|
||||
public float UncuffTime { get; set; } = 3.5f;
|
||||
|
||||
/// <summary>
|
||||
/// The time it takes for a cuffed entity to remove <see cref="CuffedComponent"/> from itself.
|
||||
/// </summary>
|
||||
[DataField("breakoutTime")]
|
||||
public float BreakoutTime { get; set; } = 30f;
|
||||
|
||||
/// <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>
|
||||
[DataField("stunBonus")]
|
||||
public float StunBonus { get; set; } = 2f;
|
||||
|
||||
/// <summary>
|
||||
/// Will the cuffs break when removed?
|
||||
/// </summary>
|
||||
[DataField("breakOnRemove")]
|
||||
public bool BreakOnRemove { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Will the cuffs break when removed?
|
||||
/// </summary>
|
||||
[DataField("brokenPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string? BrokenPrototype { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The path of the RSI file used for the player cuffed overlay.
|
||||
/// </summary>
|
||||
[DataField("cuffedRSI")]
|
||||
public string? CuffedRSI { get; set; } = "Objects/Misc/handcuffs.rsi";
|
||||
|
||||
/// <summary>
|
||||
/// The iconstate used with the RSI file for the player cuffed overlay.
|
||||
/// </summary>
|
||||
[DataField("bodyIconState")]
|
||||
public string? OverlayIconState { get; set; } = "body-overlay";
|
||||
|
||||
[DataField("startCuffSound")]
|
||||
public SoundSpecifier StartCuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_start.ogg");
|
||||
|
||||
[DataField("endCuffSound")]
|
||||
public SoundSpecifier EndCuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_end.ogg");
|
||||
|
||||
[DataField("startBreakoutSound")]
|
||||
public SoundSpecifier StartBreakoutSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_breakout_start.ogg");
|
||||
|
||||
[DataField("startUncuffSound")]
|
||||
public SoundSpecifier StartUncuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_start.ogg");
|
||||
|
||||
[DataField("endUncuffSound")]
|
||||
public SoundSpecifier EndUncuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_end.ogg");
|
||||
[DataField("color")]
|
||||
public Color Color { get; set; } = Color.White;
|
||||
|
||||
/// <summary>
|
||||
/// Used to prevent DoAfter getting spammed.
|
||||
/// </summary>
|
||||
public bool Cuffing;
|
||||
|
||||
/// <summary>
|
||||
/// Update the cuffed state of an entity
|
||||
/// </summary>
|
||||
public async void TryUpdateCuff(EntityUid user, EntityUid target, CuffableComponent cuffs)
|
||||
{
|
||||
var cuffTime = CuffTime;
|
||||
|
||||
if (_entities.HasComponent<StunnedComponent>(target))
|
||||
{
|
||||
cuffTime = MathF.Max(0.1f, cuffTime - StunBonus);
|
||||
}
|
||||
|
||||
if (_entities.HasComponent<DisarmProneComponent>(target))
|
||||
cuffTime = 0.0f; // cuff them instantly.
|
||||
|
||||
var doAfterEventArgs = new DoAfterEventArgs(user, cuffTime, default, target)
|
||||
{
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
NeedHand = true
|
||||
};
|
||||
|
||||
Cuffing = true;
|
||||
|
||||
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterEventArgs);
|
||||
|
||||
Cuffing = false;
|
||||
|
||||
// TODO these pop-ups need third-person variants (i.e. {$user} is cuffing {$target}!
|
||||
|
||||
if (result != DoAfterStatus.Cancelled)
|
||||
{
|
||||
if (cuffs.TryAddNewCuffs(user, Owner))
|
||||
{
|
||||
SoundSystem.Play(EndCuffSound.GetSound(), Filter.Pvs(Owner), Owner);
|
||||
if (target == user)
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("handcuff-component-cuff-self-success-message"));
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entities.ToPrettyString(user):player} has cuffed himself");
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("handcuff-component-cuff-other-success-message",("otherName", target)));
|
||||
target.PopupMessage(Loc.GetString("handcuff-component-cuff-by-other-success-message", ("otherName", user)));
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entities.ToPrettyString(user):player} has cuffed {_entities.ToPrettyString(target):player}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (target == user)
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("handcuff-component-cuff-interrupt-self-message"));
|
||||
}
|
||||
else
|
||||
{
|
||||
user.PopupMessage(Loc.GetString("handcuff-component-cuff-interrupt-message",("targetName", target)));
|
||||
target.PopupMessage(Loc.GetString("handcuff-component-cuff-interrupt-other-message",("otherName", user)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,216 +1,44 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Cuffs.Components;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Cuffs;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Cuffs.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Server.Cuffs
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class CuffableSystem : SharedCuffableSystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
|
||||
[Dependency] private readonly HandVirtualItemSystem _virtualSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HandCountChangedEvent>(OnHandCountChanged);
|
||||
SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
|
||||
SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
|
||||
SubscribeLocalEvent<HandcuffComponent, AfterInteractEvent>(OnCuffAfterInteract);
|
||||
SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
|
||||
SubscribeLocalEvent<CuffableComponent, EntRemovedFromContainerMessage>(OnCuffsRemoved);
|
||||
SubscribeLocalEvent<HandcuffComponent, ComponentGetState>(OnHandcuffGetState);
|
||||
SubscribeLocalEvent<CuffableComponent, ComponentGetState>(OnCuffableGetState);
|
||||
}
|
||||
|
||||
private void OnCuffsRemoved(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args)
|
||||
private void OnHandcuffGetState(EntityUid uid, HandcuffComponent component, ref ComponentGetState args)
|
||||
{
|
||||
if (args.Container.ID == component.Container.ID)
|
||||
_virtualSystem.DeleteInHandsMatching(uid, args.Entity);
|
||||
args.State = new HandcuffComponentState(component.OverlayIconState, component.Cuffing);
|
||||
}
|
||||
|
||||
private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent<Verb> args)
|
||||
private void OnCuffableGetState(EntityUid uid, CuffableComponent component, ref ComponentGetState args)
|
||||
{
|
||||
// Can the user access the cuffs, and is there even anything to uncuff?
|
||||
if (!args.CanAccess || component.CuffedHandCount == 0 || args.Hands == null)
|
||||
return;
|
||||
|
||||
// We only check can interact if the user is not uncuffing themselves. As a result, the verb will show up
|
||||
// when the user is incapacitated & trying to uncuff themselves, but TryUncuff() will still fail when
|
||||
// attempted.
|
||||
if (args.User != args.Target && !args.CanInteract)
|
||||
return;
|
||||
|
||||
Verb verb = new()
|
||||
{
|
||||
Act = () => component.TryUncuff(args.User),
|
||||
DoContactInteraction = true,
|
||||
Text = Loc.GetString("uncuff-verb-get-data-text")
|
||||
};
|
||||
//TODO VERB ICON add uncuffing symbol? may re-use the alert symbol showing that you are currently cuffed?
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
private void OnCuffAfterInteract(EntityUid uid, HandcuffComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Target is not {Valid: true} target)
|
||||
return;
|
||||
|
||||
if (!args.CanReach)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("handcuff-component-too-far-away-error"), args.User, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
TryCuffing(uid, args.User, args.Target.Value, component);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void TryCuffing(EntityUid handcuff, EntityUid user, EntityUid target, HandcuffComponent component)
|
||||
{
|
||||
if (component.Cuffing || !EntityManager.TryGetComponent<CuffableComponent>(target, out var cuffed))
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent<HandsComponent?>(target, out var hands))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-hands-error",("targetName", target)), user, user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cuffed.CuffedHandCount >= hands.Count)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-free-hands-error",("targetName", target)), user, user);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO these messages really need third-party variants. I.e., "{$user} starts cuffing {$target}!"
|
||||
if (target == user)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("handcuff-component-target-self"), user, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-target-message",("targetName", target)), user, user);
|
||||
_popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-by-other-message",("otherName", user)), target, target);
|
||||
}
|
||||
|
||||
_audio.PlayPvs(component.StartCuffSound, handcuff);
|
||||
|
||||
component.TryUpdateCuff(user, target, cuffed);
|
||||
}
|
||||
|
||||
private void OnCuffMeleeHit(EntityUid uid, HandcuffComponent component, MeleeHitEvent args)
|
||||
{
|
||||
if (!args.HitEntities.Any())
|
||||
return;
|
||||
|
||||
TryCuffing(uid, args.User, args.HitEntities.First(), component);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
|
||||
private void OnUncuffAttempt(UncuffAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!EntityManager.EntityExists(args.User))
|
||||
{
|
||||
// Should this even be possible?
|
||||
args.Cancel();
|
||||
return;
|
||||
}
|
||||
// If the user is the target, special logic applies.
|
||||
// This is because the CanInteract blocking of the cuffs prevents self-uncuff.
|
||||
if (args.User == args.Target)
|
||||
{
|
||||
// This UncuffAttemptEvent check should probably be In MobStateSystem, not here?
|
||||
if (_mobState.IsIncapacitated(args.User))
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO Find a way for cuffable to check ActionBlockerSystem.CanInteract() without blocking itself
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if the user can interact.
|
||||
if (!_actionBlockerSystem.CanInteract(args.User, args.Target))
|
||||
{
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
if (args.Cancelled)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("cuffable-component-cannot-interact-message"), args.Target, args.User);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 OnHandCountChanged(HandCountChangedEvent message)
|
||||
{
|
||||
var owner = message.Sender;
|
||||
|
||||
if (!EntityManager.TryGetComponent(owner, out CuffableComponent? cuffable) ||
|
||||
!cuffable.Initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dirty = false;
|
||||
var handCount = EntityManager.GetComponentOrNull<HandsComponent>(owner)?.Count ?? 0;
|
||||
|
||||
while (cuffable.CuffedHandCount > handCount && cuffable.CuffedHandCount > 0)
|
||||
{
|
||||
dirty = true;
|
||||
|
||||
var container = cuffable.Container;
|
||||
var entity = container.ContainedEntities[^1];
|
||||
|
||||
container.Remove(entity);
|
||||
EntityManager.GetComponent<TransformComponent>(entity).WorldPosition = EntityManager.GetComponent<TransformComponent>(owner).WorldPosition;
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
{
|
||||
UpdateCuffState(owner, cuffable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event fired on the User when the User attempts to cuff the Target.
|
||||
/// Should generate popups on the User.
|
||||
/// </summary>
|
||||
public sealed class UncuffAttemptEvent : CancellableEntityEventArgs
|
||||
{
|
||||
public readonly EntityUid User;
|
||||
public readonly EntityUid Target;
|
||||
|
||||
public UncuffAttemptEvent(EntityUid user, EntityUid target)
|
||||
{
|
||||
User = user;
|
||||
Target = target;
|
||||
// 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.
|
||||
HandcuffComponent? cuffs = null;
|
||||
if (component.CuffedHandCount > 0)
|
||||
TryComp(component.LastAddedCuffs, out cuffs);
|
||||
args.State = new CuffableComponentState(component.CuffedHandCount,
|
||||
component.CanStillInteract,
|
||||
component.Uncuffing,
|
||||
cuffs?.CuffedRSI,
|
||||
$"{cuffs?.OverlayIconState}-{component.CuffedHandCount}",
|
||||
cuffs?.Color);
|
||||
// the iconstate is formatted as blah-2, blah-4, blah-6, etc.
|
||||
// the number corresponds to how many hands are cuffed.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user