From cd78c5451db705e9f54228086e4f57e001e118d7 Mon Sep 17 00:00:00 2001 From: keronshb <54602815+keronshb@users.noreply.github.com> Date: Wed, 24 Aug 2022 10:50:31 -0400 Subject: [PATCH] Ensnaring Component and Bola Update (#9968) --- .../Components/EnsnareableComponent.cs | 13 ++ .../Components/EnsnaringComponent.cs | 9 ++ .../EnsnareableVisualizerComponent.cs | 9 ++ .../EnsnareableVisualizerSystem.cs | 42 +++++ .../Inventory/StrippableBoundUserInterface.cs | 13 ++ Content.Server/Alert/Click/RemoveEnsnare.cs | 25 +++ .../Components/EnsnareableComponent.cs | 15 ++ .../Components/EnsnaringComponent.cs | 43 +++++ .../Ensnaring/EnsnareableSystem.Ensnaring.cs | 149 ++++++++++++++++++ Content.Server/Ensnaring/EnsnareableSystem.cs | 62 ++++++++ Content.Server/Entry/IgnoredComponents.cs | 1 + Content.Server/Strip/StrippableSystem.cs | 47 +++++- Content.Shared/Alert/AlertType.cs | 1 + .../HumanoidVisualLayers.cs | 1 + .../Components/SharedEnsnareableComponent.cs | 37 +++++ .../Components/SharedEnsnaringComponent.cs | 71 +++++++++ .../Ensnaring/EnsnareableVisuals.cs | 9 ++ .../Ensnaring/SharedEnsnareableSystem.cs | 59 +++++++ .../Components/SharedStrippableComponent.cs | 16 +- .../Shared/Alert/AlertOrderPrototypeTests.cs | 6 + .../en-US/ensnare/ensnare-component.ftl | 5 + .../en-US/strip/strippable-component.ftl | 1 + Resources/Prototypes/Alerts/alerts.yml | 8 + .../Prototypes/Entities/Mobs/Species/base.yml | 4 + .../Objects/Weapons/Throwable/bola.yml | 8 + .../Alerts/ensnared.rsi/ensnared.png | Bin 0 -> 7117 bytes .../Interface/Alerts/ensnared.rsi/meta.json | 14 ++ .../Objects/Misc/ensnare.rsi/icon.png | Bin 0 -> 437 bytes .../Objects/Misc/ensnare.rsi/meta.json | 15 ++ 29 files changed, 681 insertions(+), 2 deletions(-) create mode 100644 Content.Client/Ensnaring/Components/EnsnareableComponent.cs create mode 100644 Content.Client/Ensnaring/Components/EnsnaringComponent.cs create mode 100644 Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerComponent.cs create mode 100644 Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerSystem.cs create mode 100644 Content.Server/Alert/Click/RemoveEnsnare.cs create mode 100644 Content.Server/Ensnaring/Components/EnsnareableComponent.cs create mode 100644 Content.Server/Ensnaring/Components/EnsnaringComponent.cs create mode 100644 Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs create mode 100644 Content.Server/Ensnaring/EnsnareableSystem.cs create mode 100644 Content.Shared/Ensnaring/Components/SharedEnsnareableComponent.cs create mode 100644 Content.Shared/Ensnaring/Components/SharedEnsnaringComponent.cs create mode 100644 Content.Shared/Ensnaring/EnsnareableVisuals.cs create mode 100644 Content.Shared/Ensnaring/SharedEnsnareableSystem.cs create mode 100644 Resources/Locale/en-US/ensnare/ensnare-component.ftl create mode 100644 Resources/Textures/Interface/Alerts/ensnared.rsi/ensnared.png create mode 100644 Resources/Textures/Interface/Alerts/ensnared.rsi/meta.json create mode 100644 Resources/Textures/Objects/Misc/ensnare.rsi/icon.png create mode 100644 Resources/Textures/Objects/Misc/ensnare.rsi/meta.json diff --git a/Content.Client/Ensnaring/Components/EnsnareableComponent.cs b/Content.Client/Ensnaring/Components/EnsnareableComponent.cs new file mode 100644 index 0000000000..7035dcfad8 --- /dev/null +++ b/Content.Client/Ensnaring/Components/EnsnareableComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Ensnaring.Components; + +namespace Content.Client.Ensnaring.Components; +[RegisterComponent] +[ComponentReference(typeof(SharedEnsnareableComponent))] +public sealed class EnsnareableComponent : SharedEnsnareableComponent +{ + [DataField("sprite")] + public string? Sprite; + + [DataField("state")] + public string? State; +} diff --git a/Content.Client/Ensnaring/Components/EnsnaringComponent.cs b/Content.Client/Ensnaring/Components/EnsnaringComponent.cs new file mode 100644 index 0000000000..83c7e1199d --- /dev/null +++ b/Content.Client/Ensnaring/Components/EnsnaringComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Ensnaring.Components; + +namespace Content.Client.Ensnaring.Components; +[RegisterComponent] +[ComponentReference(typeof(SharedEnsnaringComponent))] +public sealed class EnsnaringComponent : SharedEnsnaringComponent +{ + +} diff --git a/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerComponent.cs b/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerComponent.cs new file mode 100644 index 0000000000..3c651f6235 --- /dev/null +++ b/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Utility; + +namespace Content.Client.Ensnaring.Visualizers; +[RegisterComponent] +[Access(typeof(EnsnareableVisualizerSystem))] +public sealed class EnsnareableVisualizerComponent : Component +{ + +} diff --git a/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerSystem.cs b/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerSystem.cs new file mode 100644 index 0000000000..c20897fc9e --- /dev/null +++ b/Content.Client/Ensnaring/Visualizers/EnsnareableVisualizerSystem.cs @@ -0,0 +1,42 @@ +using Content.Client.Ensnaring.Components; +using Content.Shared.Ensnaring; +using Robust.Client.GameObjects; +using Robust.Shared.Utility; + +namespace Content.Client.Ensnaring.Visualizers; + +public sealed class EnsnareableVisualizerSystem : VisualizerSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + } + + private void OnComponentInit(EntityUid uid, EnsnareableVisualizerComponent component, ComponentInit args) + { + if(!TryComp(uid, out var sprite)) + return; + + sprite.LayerMapReserveBlank(EnsnaredVisualLayers.Ensnared); + } + + protected override void OnAppearanceChange(EntityUid uid, EnsnareableComponent component, ref AppearanceChangeEvent args) + { + if (args.Component.TryGetData(EnsnareableVisuals.IsEnsnared, out bool isEnsnared)) + { + if (args.Sprite != null && component.Sprite != null) + { + args.Sprite.LayerSetRSI(EnsnaredVisualLayers.Ensnared, component.Sprite); + args.Sprite.LayerSetState(EnsnaredVisualLayers.Ensnared, component.State); + args.Sprite.LayerSetVisible(EnsnaredVisualLayers.Ensnared, isEnsnared); + } + } + } +} + +public enum EnsnaredVisualLayers : byte +{ + Ensnared, +} diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index cb4554d689..c516b2b6ed 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -12,6 +12,7 @@ namespace Content.Client.Inventory public Dictionary<(string ID, string Name), string>? Inventory { get; private set; } public Dictionary? Hands { get; private set; } public Dictionary? Handcuffs { get; private set; } + public Dictionary? Ensnare { get; private set; } [ViewVariables] private StrippingMenu? _strippingMenu; @@ -79,6 +80,17 @@ namespace Content.Client.Inventory }); } } + + if (Ensnare != null) + { + foreach (var (id, name) in Ensnare) + { + _strippingMenu.AddButton(Loc.GetString("strippable-bound-user-interface-stripping-menu-ensnare-button"), name, (ev) => + { + SendMessage(new StrippingEnsnareButtonPressed(id)); + }); + } + } } protected override void UpdateState(BoundUserInterfaceState state) @@ -90,6 +102,7 @@ namespace Content.Client.Inventory Inventory = stripState.Inventory; Hands = stripState.Hands; Handcuffs = stripState.Handcuffs; + Ensnare = stripState.Ensnare; UpdateMenu(); } diff --git a/Content.Server/Alert/Click/RemoveEnsnare.cs b/Content.Server/Alert/Click/RemoveEnsnare.cs new file mode 100644 index 0000000000..bfac76c594 --- /dev/null +++ b/Content.Server/Alert/Click/RemoveEnsnare.cs @@ -0,0 +1,25 @@ +using Content.Server.Ensnaring; +using Content.Server.Ensnaring.Components; +using Content.Shared.Alert; +using JetBrains.Annotations; + +namespace Content.Server.Alert.Click; +[UsedImplicitly] +[DataDefinition] +public sealed class RemoveEnsnare : IAlertClick +{ + public void AlertClicked(EntityUid player) + { + var entManager = IoCManager.Resolve(); + if (entManager.TryGetComponent(player, out EnsnareableComponent? ensnareableComponent)) + { + foreach (var ensnare in ensnareableComponent.Container.ContainedEntities) + { + if (!entManager.TryGetComponent(ensnare, out EnsnaringComponent? ensnaringComponent)) + return; + + entManager.EntitySysManager.GetEntitySystem().TryFree(player, ensnaringComponent); + } + } + } +} diff --git a/Content.Server/Ensnaring/Components/EnsnareableComponent.cs b/Content.Server/Ensnaring/Components/EnsnareableComponent.cs new file mode 100644 index 0000000000..373015276a --- /dev/null +++ b/Content.Server/Ensnaring/Components/EnsnareableComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared.Ensnaring.Components; +using Robust.Shared.Containers; + +namespace Content.Server.Ensnaring.Components; +[RegisterComponent] +[ComponentReference(typeof(SharedEnsnareableComponent))] +public sealed class EnsnareableComponent : SharedEnsnareableComponent +{ + /// + /// The container where the entity will be stored + /// + [ViewVariables] + [DataField("container")] + public Container Container = default!; +} diff --git a/Content.Server/Ensnaring/Components/EnsnaringComponent.cs b/Content.Server/Ensnaring/Components/EnsnaringComponent.cs new file mode 100644 index 0000000000..69fa9a241f --- /dev/null +++ b/Content.Server/Ensnaring/Components/EnsnaringComponent.cs @@ -0,0 +1,43 @@ +using System.Threading; +using Content.Shared.Ensnaring.Components; + +namespace Content.Server.Ensnaring.Components; +[RegisterComponent] +[ComponentReference(typeof(SharedEnsnaringComponent))] +public sealed class EnsnaringComponent : SharedEnsnaringComponent +{ + /// + /// Should movement cancel breaking out? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("canMoveBreakout")] + public bool CanMoveBreakout; + + public CancellationTokenSource? CancelToken; +} + +/// +/// Used for the do after event to free the entity that owns the +/// +public sealed class FreeEnsnareDoAfterComplete : EntityEventArgs +{ + public readonly EntityUid EnsnaringEntity; + + public FreeEnsnareDoAfterComplete(EntityUid ensnaringEntity) + { + EnsnaringEntity = ensnaringEntity; + } +} + +/// +/// Used for the do after event when it fails to free the entity that owns the +/// +public sealed class FreeEnsnareDoAfterCancel : EntityEventArgs +{ + public readonly EntityUid EnsnaringEntity; + + public FreeEnsnareDoAfterCancel(EntityUid ensnaringEntity) + { + EnsnaringEntity = ensnaringEntity; + } +} diff --git a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs new file mode 100644 index 0000000000..999e08180a --- /dev/null +++ b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs @@ -0,0 +1,149 @@ +using System.Threading; +using Content.Server.DoAfter; +using Content.Server.Ensnaring.Components; +using Content.Shared.Alert; +using Content.Shared.Ensnaring.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.StepTrigger.Systems; +using Content.Shared.Throwing; +using Robust.Shared.Player; + +namespace Content.Server.Ensnaring; + +public sealed partial class EnsnareableSystem +{ + [Dependency] private readonly DoAfterSystem _doAfter = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + + public void InitializeEnsnaring() + { + SubscribeLocalEvent(OnComponentRemove); + SubscribeLocalEvent(AttemptStepTrigger); + SubscribeLocalEvent(OnStepTrigger); + SubscribeLocalEvent(OnThrowHit); + } + + private void OnComponentRemove(EntityUid uid, EnsnaringComponent component, ComponentRemove args) + { + if (!TryComp(component.Ensnared, out var ensnared)) + return; + + if (ensnared.IsEnsnared) + ForceFree(component); + } + + private void AttemptStepTrigger(EntityUid uid, EnsnaringComponent component, ref StepTriggerAttemptEvent args) + { + args.Continue = true; + } + + private void OnStepTrigger(EntityUid uid, EnsnaringComponent component, ref StepTriggeredEvent args) + { + TryEnsnare(args.Tripper, component); + } + + private void OnThrowHit(EntityUid uid, EnsnaringComponent component, ThrowDoHitEvent args) + { + if (!component.CanThrowTrigger) + return; + + TryEnsnare(args.Target, component); + } + + /// + /// Used where you want to try to ensnare an entity with the + /// + /// The entity that will be used to ensnare + /// The entity that will be ensnared + /// The ensnaring component + public void TryEnsnare(EntityUid target, EnsnaringComponent component) + { + //Don't do anything if they don't have the ensnareable component. + if (!TryComp(target, out var ensnareable)) + return; + + component.Ensnared = target; + ensnareable.Container.Insert(component.Owner); + ensnareable.IsEnsnared = true; + + UpdateAlert(ensnareable); + var ev = new EnsnareEvent(component.WalkSpeed, component.SprintSpeed); + RaiseLocalEvent(target, ev, false); + } + + /// + /// Used where you want to try to free an entity with the + /// + /// The entity that will be free + /// The ensnaring component + public void TryFree(EntityUid target, EnsnaringComponent component, EntityUid? user = null) + { + //Don't do anything if they don't have the ensnareable component. + if (!TryComp(target, out var ensnareable)) + return; + + if (component.CancelToken != null) + return; + + component.CancelToken = new CancellationTokenSource(); + + var isOwner = !(user != null && target != user); + var freeTime = isOwner ? component.BreakoutTime : component.FreeTime; + bool breakOnMove; + + if (isOwner) + breakOnMove = !component.CanMoveBreakout; + else + breakOnMove = true; + + var doAfterEventArgs = new DoAfterEventArgs(target, freeTime, component.CancelToken.Token, target) + { + BreakOnUserMove = breakOnMove, + BreakOnTargetMove = breakOnMove, + BreakOnDamage = false, + BreakOnStun = true, + NeedHand = true, + TargetFinishedEvent = new FreeEnsnareDoAfterComplete(component.Owner), + TargetCancelledEvent = new FreeEnsnareDoAfterCancel(component.Owner), + }; + + _doAfter.DoAfter(doAfterEventArgs); + + if (isOwner) + _popup.PopupEntity(Loc.GetString("ensnare-component-try-free", ("ensnare", component.Owner)), target, Filter.Entities(target)); + + if (!isOwner && user != null) + { + _popup.PopupEntity(Loc.GetString("ensnare-component-try-free-other", ("ensnare", component.Owner), ("user", Identity.Entity(target, EntityManager))), user.Value, Filter.Entities(user.Value)); + } + } + + /// + /// Used to force free someone for things like if the is removed + /// + public void ForceFree(EnsnaringComponent component) + { + if (!TryComp(component.Ensnared, out var ensnareable)) + return; + + ensnareable.Container.ForceRemove(component.Owner); + ensnareable.IsEnsnared = false; + component.Ensnared = null; + + UpdateAlert(ensnareable); + var ev = new EnsnareRemoveEvent(); + RaiseLocalEvent(component.Owner, ev, false); + } + + public void UpdateAlert(EnsnareableComponent component) + { + if (!component.IsEnsnared) + { + _alerts.ClearAlert(component.Owner, AlertType.Ensnared); + } + else + { + _alerts.ShowAlert(component.Owner, AlertType.Ensnared); + } + } +} diff --git a/Content.Server/Ensnaring/EnsnareableSystem.cs b/Content.Server/Ensnaring/EnsnareableSystem.cs new file mode 100644 index 0000000000..fb5ab949b7 --- /dev/null +++ b/Content.Server/Ensnaring/EnsnareableSystem.cs @@ -0,0 +1,62 @@ +using Content.Server.Ensnaring.Components; +using Content.Server.Popups; +using Content.Shared.Ensnaring; +using Content.Shared.Ensnaring.Components; +using Content.Shared.Popups; +using Robust.Server.Containers; +using Robust.Shared.Containers; +using Robust.Shared.Player; + +namespace Content.Server.Ensnaring; + +public sealed partial class EnsnareableSystem : SharedEnsnareableSystem +{ + [Dependency] private readonly ContainerSystem _container = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + InitializeEnsnaring(); + + SubscribeLocalEvent(OnEnsnareableInit); + SubscribeLocalEvent(OnFreeComplete); + SubscribeLocalEvent(OnFreeFail); + } + + private void OnEnsnareableInit(EntityUid uid, EnsnareableComponent component, ComponentInit args) + { + component.Container = _container.EnsureContainer(component.Owner, "ensnare"); + } + + private void OnFreeComplete(EntityUid uid, EnsnareableComponent component, FreeEnsnareDoAfterComplete args) + { + if (!TryComp(args.EnsnaringEntity, out var ensnaring)) + return; + + component.Container.Remove(args.EnsnaringEntity); + component.IsEnsnared = false; + ensnaring.Ensnared = null; + + _popup.PopupEntity(Loc.GetString("ensnare-component-try-free-complete", ("ensnare", args.EnsnaringEntity)), + uid, Filter.Entities(uid), PopupType.Large); + + UpdateAlert(component); + var ev = new EnsnareRemoveEvent(); + RaiseLocalEvent(uid, ev, false); + + ensnaring.CancelToken = null; + } + + private void OnFreeFail(EntityUid uid, EnsnareableComponent component, FreeEnsnareDoAfterCancel args) + { + if (!TryComp(args.EnsnaringEntity, out var ensnaring)) + return; + + ensnaring.CancelToken = null; + + _popup.PopupEntity(Loc.GetString("ensnare-component-try-free-fail", ("ensnare", args.EnsnaringEntity)), + uid, Filter.Entities(uid), PopupType.Large); + } +} diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index 4dd499b52a..6c690cee77 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -17,6 +17,7 @@ namespace Content.Server.Entry "CharacterInfo", "HandheldGPS", "CableVisualizer", + "EnsnareableVisualizer", }; } } diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 3b6f6b6f9c..2e096212ee 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -1,9 +1,13 @@ +using System.ComponentModel; using System.Threading; using Content.Server.Cuffs.Components; using Content.Server.DoAfter; +using Content.Server.Ensnaring; +using Content.Server.Ensnaring.Components; using Content.Server.Hands.Components; using Content.Server.Inventory; using Content.Server.UserInterface; +using Content.Shared.Ensnaring.Components; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; @@ -14,6 +18,7 @@ using Content.Shared.Popups; using Content.Shared.Strip.Components; using Content.Shared.Verbs; using Robust.Server.GameObjects; +using Robust.Shared.Map; using Robust.Shared.Player; namespace Content.Server.Strip @@ -24,6 +29,7 @@ namespace Content.Server.Strip [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly EnsnareableSystem _ensnaring = default!; // TODO: ECS popups. Not all of these have ECS equivalents yet. @@ -36,11 +42,13 @@ namespace Content.Server.Strip SubscribeLocalEvent(OnDidUnequip); SubscribeLocalEvent(OnCompInit); SubscribeLocalEvent(OnCuffStateChange); + SubscribeLocalEvent(OnEnsnareChange); // BUI SubscribeLocalEvent(OnStripInvButtonMessage); SubscribeLocalEvent(OnStripHandMessage); SubscribeLocalEvent(OnStripHandcuffMessage); + SubscribeLocalEvent(OnStripEnsnareMessage); SubscribeLocalEvent(OnOpenStripComplete); SubscribeLocalEvent(OnOpenStripCancelled); @@ -76,6 +84,26 @@ namespace Content.Server.Strip } } + private void OnStripEnsnareMessage(EntityUid uid, StrippableComponent component, StrippingEnsnareButtonPressed args) + { + if (args.Session.AttachedEntity is not {Valid: true} user) + return; + + var ensnareQuery = GetEntityQuery(); + + foreach (var entity in ensnareQuery.GetComponent(uid).Container.ContainedEntities) + { + if (!TryComp(entity, out var ensnaring)) + continue; + + if (entity != args.Ensnare) + continue; + + _ensnaring.TryFree(component.Owner, ensnaring, user); + return; + } + } + private void OnStripHandMessage(EntityUid uid, StrippableComponent component, StrippingHandButtonPressed args) { if (args.Session.AttachedEntity is not {Valid: true} user || @@ -154,6 +182,11 @@ namespace Content.Server.Strip UpdateState(uid, component); } + private void OnEnsnareChange(EntityUid uid, StrippableComponent component, EnsnaredChangedEvent args) + { + SendUpdate(uid, component); + } + private void OnDidUnequip(EntityUid uid, StrippableComponent component, DidUnequipEvent args) { SendUpdate(uid, component); @@ -174,6 +207,7 @@ namespace Content.Server.Strip } var cuffs = new Dictionary(); + var ensnare = new Dictionary(); var inventory = new Dictionary<(string ID, string Name), string>(); var hands = new Dictionary(); @@ -186,6 +220,17 @@ namespace Content.Server.Strip } } + var ensnareQuery = GetEntityQuery(); + + if (ensnareQuery.TryGetComponent(uid, out var _)) + { + foreach (var entity in ensnareQuery.GetComponent(uid).Container.ContainedEntities) + { + var name = Name(entity); + ensnare.Add(entity, name); + } + } + if (_inventorySystem.TryGetSlots(uid, out var slots)) { foreach (var slot in slots) @@ -223,7 +268,7 @@ namespace Content.Server.Strip } } - bui.SetState(new StrippingBoundUserInterfaceState(inventory, hands, cuffs)); + bui.SetState(new StrippingBoundUserInterfaceState(inventory, hands, cuffs, ensnare)); } private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args) diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs index 0c08b6708d..4d80a8acfd 100644 --- a/Content.Shared/Alert/AlertType.cs +++ b/Content.Shared/Alert/AlertType.cs @@ -17,6 +17,7 @@ namespace Content.Shared.Alert Weightless, Stun, Handcuffed, + Ensnared, Buckled, HumanCrit, HumanDead, diff --git a/Content.Shared/CharacterAppearance/HumanoidVisualLayers.cs b/Content.Shared/CharacterAppearance/HumanoidVisualLayers.cs index 28de91d641..c4583a84ad 100644 --- a/Content.Shared/CharacterAppearance/HumanoidVisualLayers.cs +++ b/Content.Shared/CharacterAppearance/HumanoidVisualLayers.cs @@ -24,6 +24,7 @@ namespace Content.Shared.CharacterAppearance LFoot, Handcuffs, StencilMask, + Ensnare, Fire, } } diff --git a/Content.Shared/Ensnaring/Components/SharedEnsnareableComponent.cs b/Content.Shared/Ensnaring/Components/SharedEnsnareableComponent.cs new file mode 100644 index 0000000000..cff199f570 --- /dev/null +++ b/Content.Shared/Ensnaring/Components/SharedEnsnareableComponent.cs @@ -0,0 +1,37 @@ +namespace Content.Shared.Ensnaring.Components; +/// +/// Use this on an entity that you would like to be ensnared by anything that has the +/// +public abstract class SharedEnsnareableComponent : Component +{ + /// + /// How much should this slow down the entities walk? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("walkSpeed")] + public float WalkSpeed = 1.0f; + + /// + /// How much should this slow down the entities sprint? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("sprintSpeed")] + public float SprintSpeed = 1.0f; + + /// + /// Is this entity currently ensnared? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("isEnsnared")] + public bool IsEnsnared; +} + +public sealed class EnsnaredChangedEvent : EntityEventArgs +{ + public readonly bool IsEnsnared; + + public EnsnaredChangedEvent(bool isEnsnared) + { + IsEnsnared = isEnsnared; + } +} diff --git a/Content.Shared/Ensnaring/Components/SharedEnsnaringComponent.cs b/Content.Shared/Ensnaring/Components/SharedEnsnaringComponent.cs new file mode 100644 index 0000000000..8c68b4e6a0 --- /dev/null +++ b/Content.Shared/Ensnaring/Components/SharedEnsnaringComponent.cs @@ -0,0 +1,71 @@ +namespace Content.Shared.Ensnaring.Components; +/// +/// Use this on something you want to use to ensnare an entity with +/// +public abstract class SharedEnsnaringComponent : Component +{ + /// + /// How long it should take to free someone else. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("freeTime")] + public float FreeTime = 3.5f; + + /// + /// How long it should take for an entity to free themselves. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("breakoutTime")] + public float BreakoutTime = 30.0f; + + /// + /// How much should this slow down the entities walk? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("walkSpeed")] + public float WalkSpeed = 0.9f; + + /// + /// How much should this slow down the entities sprint? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("sprintSpeed")] + public float SprintSpeed = 0.9f; + + /// + /// Should this ensnare someone when thrown? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("canThrowTrigger")] + public bool CanThrowTrigger; + + /// + /// What is ensnared? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("ensnared")] + public EntityUid? Ensnared; +} + +/// +/// Used whenever you want to do something when someone becomes ensnared by the +/// +public sealed class EnsnareEvent : EntityEventArgs +{ + public readonly float WalkSpeed; + public readonly float SprintSpeed; + + public EnsnareEvent(float walkSpeed, float sprintSpeed) + { + WalkSpeed = walkSpeed; + SprintSpeed = sprintSpeed; + } +} + +/// +/// Used whenever you want to do something when someone is freed by the +/// +public sealed class EnsnareRemoveEvent : CancellableEntityEventArgs +{ + +} diff --git a/Content.Shared/Ensnaring/EnsnareableVisuals.cs b/Content.Shared/Ensnaring/EnsnareableVisuals.cs new file mode 100644 index 0000000000..ff9def0428 --- /dev/null +++ b/Content.Shared/Ensnaring/EnsnareableVisuals.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Ensnaring; + +[Serializable, NetSerializable] +public enum EnsnareableVisuals : byte +{ + IsEnsnared +} diff --git a/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs b/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs new file mode 100644 index 0000000000..ddeb00d1f0 --- /dev/null +++ b/Content.Shared/Ensnaring/SharedEnsnareableSystem.cs @@ -0,0 +1,59 @@ +using Content.Shared.Ensnaring.Components; +using Content.Shared.Movement.Systems; + +namespace Content.Shared.Ensnaring; + +public abstract class SharedEnsnareableSystem : EntitySystem +{ + [Dependency] private readonly MovementSpeedModifierSystem _speedModifier = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(MovementSpeedModify); + SubscribeLocalEvent(OnEnsnare); + SubscribeLocalEvent(OnEnsnareRemove); + SubscribeLocalEvent(OnEnsnareChange); + } + + private void OnEnsnare(EntityUid uid, SharedEnsnareableComponent component, EnsnareEvent args) + { + component.WalkSpeed = args.WalkSpeed; + component.SprintSpeed = args.SprintSpeed; + + _speedModifier.RefreshMovementSpeedModifiers(uid); + + var ev = new EnsnaredChangedEvent(component.IsEnsnared); + RaiseLocalEvent(uid, ev, true); + } + + private void OnEnsnareRemove(EntityUid uid, SharedEnsnareableComponent component, EnsnareRemoveEvent args) + { + _speedModifier.RefreshMovementSpeedModifiers(uid); + + var ev = new EnsnaredChangedEvent(component.IsEnsnared); + RaiseLocalEvent(uid, ev, true); + } + + private void OnEnsnareChange(EntityUid uid, SharedEnsnareableComponent component, EnsnaredChangedEvent args) + { + UpdateAppearance(uid, component); + } + + private void UpdateAppearance(EntityUid uid, SharedEnsnareableComponent? component, AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref component, ref appearance, false)) + return; + + appearance.SetData(EnsnareableVisuals.IsEnsnared, component.IsEnsnared); + } + + private void MovementSpeedModify(EntityUid uid, SharedEnsnareableComponent component, RefreshMovementSpeedModifiersEvent args) + { + if (!component.IsEnsnared) + return; + + args.ModifySpeed(component.WalkSpeed, component.SprintSpeed); + } +} diff --git a/Content.Shared/Strip/Components/SharedStrippableComponent.cs b/Content.Shared/Strip/Components/SharedStrippableComponent.cs index 3dcfa99127..554043eff7 100644 --- a/Content.Shared/Strip/Components/SharedStrippableComponent.cs +++ b/Content.Shared/Strip/Components/SharedStrippableComponent.cs @@ -64,18 +64,32 @@ namespace Content.Shared.Strip.Components } } + [NetSerializable, Serializable] + public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage + { + public EntityUid Ensnare { get; } + + public StrippingEnsnareButtonPressed(EntityUid ensnare) + { + Ensnare = ensnare; + } + } + [NetSerializable, Serializable] public sealed class StrippingBoundUserInterfaceState : BoundUserInterfaceState { public Dictionary<(string ID, string Name), string> Inventory { get; } public Dictionary Hands { get; } public Dictionary Handcuffs { get; } + public Dictionary Ensnare { get; } - public StrippingBoundUserInterfaceState(Dictionary<(string ID, string Name), string> inventory, Dictionary hands, Dictionary handcuffs) + public StrippingBoundUserInterfaceState(Dictionary<(string ID, string Name), string> inventory, Dictionary hands, Dictionary handcuffs, + Dictionary ensnare) { Inventory = inventory; Hands = hands; Handcuffs = handcuffs; + Ensnare = ensnare; } } diff --git a/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs b/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs index fe293b050d..56f76d46a9 100644 --- a/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs +++ b/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs @@ -17,6 +17,7 @@ namespace Content.Tests.Shared.Alert id: testAlertOrder order: - alertType: Handcuffed + - alertType: Ensnared - category: Pressure - category: Hunger - alertType: Hot @@ -47,6 +48,10 @@ namespace Content.Tests.Shared.Alert id: Handcuffed icons: [] +- type: alert + id: Ensnared + icons: [] + - type: alert id: Hot icons: [] @@ -82,6 +87,7 @@ namespace Content.Tests.Shared.Alert // ensure they sort according to our expected criteria var expectedOrder = new List(); expectedOrder.Add(AlertType.Handcuffed); + expectedOrder.Add(AlertType.Ensnared); expectedOrder.Add(AlertType.HighPressure); // stuff with only category + same category ordered by enum value expectedOrder.Add(AlertType.Peckish); diff --git a/Resources/Locale/en-US/ensnare/ensnare-component.ftl b/Resources/Locale/en-US/ensnare/ensnare-component.ftl new file mode 100644 index 0000000000..957113ce35 --- /dev/null +++ b/Resources/Locale/en-US/ensnare/ensnare-component.ftl @@ -0,0 +1,5 @@ +ensnare-component-try-free = You struggle to remove {$ensnare} that's ensnaring you! +ensnare-component-try-free-complete = You successfully free yourself from the {$ensnare}! +ensnare-component-try-free-fail = You fail to free yourself from the {$ensnare}! + +ensnare-component-try-free-other = You start removing the {$ensnare} caught on {$user}! diff --git a/Resources/Locale/en-US/strip/strippable-component.ftl b/Resources/Locale/en-US/strip/strippable-component.ftl index 15d9a85578..71e9cf3f08 100644 --- a/Resources/Locale/en-US/strip/strippable-component.ftl +++ b/Resources/Locale/en-US/strip/strippable-component.ftl @@ -17,4 +17,5 @@ strip-verb-get-data-text = Strip strippable-bound-user-interface-stripping-menu-title = {$ownerName}'s inventory strippable-bound-user-interface-stripping-menu-handcuffs-button = Restraints +strippable-bound-user-interface-stripping-menu-ensnare-button = Leg Restraints strippable-bound-user-interface-stripping-menu-obfuscate = Occupied diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index d11aeef908..e868a1335b 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -9,6 +9,7 @@ - category: Internals - alertType: Fire - alertType: Handcuffed + - alertType: Ensnared - category: Buckled - alertType: Pulling - category: Piloting @@ -120,6 +121,13 @@ 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." +- type: alert + id: Ensnared + onClick: !type:RemoveEnsnare { } + icons: [ /Textures/Interface/Alerts/ensnared.rsi/ensnared.png ] + name: "[color=yellow]Ensnared[/color]" + description: "You're [color=yellow]ensnared[/color] and is impairing your ability to move." + - type: alert id: Buckled category: Buckled diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index b590a8c520..551ebd6151 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -280,9 +280,13 @@ normalState: Generic_mob_burning alternateState: Standing fireStackAlternateState: 3 + - type: EnsnareableVisualizer - type: CombatMode - type: Climbing - type: Cuffable + - type: Ensnareable + sprite: Objects/Misc/ensnare.rsi + state: icon - type: CharacterInfo - type: AnimationPlayer - type: Buckle diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/bola.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/bola.yml index 1e4bdc1eb9..4ab8412f2d 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/bola.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/bola.yml @@ -34,3 +34,11 @@ damage: types: Blunt: 5 + - type: Ensnaring + freeTime: 2.0 + breakoutTime: 3.5 #all bola should generally be fast to remove + walkSpeed: 0.7 #makeshift bola shouldn't slow too much + sprintSpeed: 0.7 + canThrowTrigger: true + canMoveBreakout: true + diff --git a/Resources/Textures/Interface/Alerts/ensnared.rsi/ensnared.png b/Resources/Textures/Interface/Alerts/ensnared.rsi/ensnared.png new file mode 100644 index 0000000000000000000000000000000000000000..166d05221eb5ed41fde3b5051a461ac507469c7c GIT binary patch literal 7117 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 nEvB%B*tXg?dssJw?qhsaG(mg4{U