diff --git a/Content.Server/Alert/Click/StopPulling.cs b/Content.Server/Alert/Click/StopPulling.cs index 76f9569429f987..3941ff6873f078 100644 --- a/Content.Server/Alert/Click/StopPulling.cs +++ b/Content.Server/Alert/Click/StopPulling.cs @@ -20,7 +20,7 @@ public void AlertClicked(EntityUid player) if (entManager.TryGetComponent(player, out PullerComponent? puller) && entManager.TryGetComponent(puller.Pulling, out PullableComponent? pullableComp)) { - ps.TryStopPull(puller.Pulling.Value, pullableComp, user: player); + ps.TryLowerGrabStage(puller.Pulling.Value, player, true); } } } diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs index 5fbd0c48147eda..d63764724fcfff 100644 --- a/Content.Server/Body/Systems/RespiratorSystem.cs +++ b/Content.Server/Body/Systems/RespiratorSystem.cs @@ -18,6 +18,8 @@ using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Content.Shared.Movement.Pulling.Systems; +using Content.Shared.Movement.Pulling.Components; namespace Content.Server.Body.Systems; @@ -59,6 +61,13 @@ private void OnUnpaused(Entity ent, ref EntityUnpausedEvent ent.Comp.NextUpdate += args.PausedTime; } + public bool CanBreathe(EntityUid uid) + { + if (TryComp(uid, out var pullable) && pullable.GrabStage == GrabStage.Suffocate) + return false; + return true; + } + public override void Update(float frameTime) { base.Update(frameTime); @@ -91,7 +100,7 @@ public override void Update(float frameTime) } } - if (respirator.Saturation < respirator.SuffocationThreshold) + if (respirator.Saturation < respirator.SuffocationThreshold || !CanBreathe(uid)) { if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown) { diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs index feae130ce8e363..49e1bbc797fd1c 100644 --- a/Content.Server/Hands/Systems/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -23,6 +23,8 @@ using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; +using Content.Shared.Hands; +using Content.Shared.Inventory.VirtualItem; namespace Content.Server.Hands.Systems { @@ -90,7 +92,7 @@ private void OnDisarmed(EntityUid uid, HandsComponent component, DisarmedEvent a // Break any pulls if (TryComp(uid, out PullerComponent? puller) && TryComp(puller.Pulling, out PullableComponent? pullable)) - _pullingSystem.TryStopPull(puller.Pulling.Value, pullable); + _pullingSystem.TryStopPull(puller.Pulling.Value, pullable, ignoreGrab: true); var offsetRandomCoordinates = _transformSystem.GetMoverCoordinates(args.Target).Offset(_random.NextVector2(1f, 1.5f)); if (!ThrowHeldItem(args.Target, offsetRandomCoordinates)) @@ -173,6 +175,18 @@ private bool HandleThrowItem(ICommonSession? playerSession, EntityCoordinates co if (playerSession?.AttachedEntity is not {Valid: true} player || !Exists(player)) return false; + if (TryGetActiveItem(player, out var item) && TryComp(item, out var virtComp)) + { + var userEv = new VirtualItemDropAttemptEvent(virtComp.BlockingEntity, player, item.Value, true); + RaiseLocalEvent(player, userEv); + + var targEv = new VirtualItemDropAttemptEvent(virtComp.BlockingEntity, player, item.Value, true); + RaiseLocalEvent(virtComp.BlockingEntity, targEv); + + if (userEv.Cancelled || targEv.Cancelled) + return false; + } + return ThrowHeldItem(player, coordinates); } @@ -216,6 +230,15 @@ hands.ActiveHandEntity is not { } throwEnt || var ev = new BeforeThrowEvent(throwEnt, direction, throwStrength, player); RaiseLocalEvent(player, ref ev); + if (TryComp(throwEnt, out var virt)) + { + var userEv = new VirtualItemThrownEvent(virt.BlockingEntity, player, throwEnt, direction); + RaiseLocalEvent(player, userEv); + + var targEv = new VirtualItemThrownEvent(virt.BlockingEntity, player, throwEnt, direction); + RaiseLocalEvent(virt.BlockingEntity, targEv); + } + if (ev.Cancelled) return true; diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs index 88c5fb9459253d..3de60b96d7e8ed 100644 --- a/Content.Server/Implants/SubdermalImplantSystem.cs +++ b/Content.Server/Implants/SubdermalImplantSystem.cs @@ -1,4 +1,4 @@ -using Content.Server.Cuffs; +using Content.Server.Cuffs; using Content.Server.Forensics; using Content.Server.Humanoid; using Content.Server.Implants.Components; @@ -108,7 +108,7 @@ private void OnScramImplant(EntityUid uid, SubdermalImplantComponent component, // We need stop the user from being pulled so they don't just get "attached" with whoever is pulling them. // This can for example happen when the user is cuffed and being pulled. if (TryComp(ent, out var pull) && _pullingSystem.IsPulled(ent, pull)) - _pullingSystem.TryStopPull(ent, pull); + _pullingSystem.TryStopPull(ent, pull, ignoreGrab: true); var xform = Transform(ent); var targetCoords = SelectRandomTileInRange(xform, implant.TeleportRadius); diff --git a/Content.Shared/Administration/SharedAdminFrozenSystem.cs b/Content.Shared/Administration/SharedAdminFrozenSystem.cs index 259df2bdf2ac14..a2e92a2b88a19f 100644 --- a/Content.Shared/Administration/SharedAdminFrozenSystem.cs +++ b/Content.Shared/Administration/SharedAdminFrozenSystem.cs @@ -62,7 +62,7 @@ private void OnStartup(EntityUid uid, AdminFrozenComponent component, ComponentS { if (TryComp(uid, out var pullable)) { - _pulling.TryStopPull(uid, pullable); + _pulling.TryStopPull(uid, pullable, ignoreGrab: true); } UpdateCanMove(uid, component, args); diff --git a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs index efb5dfd0248203..dd19c27e2a9695 100644 --- a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs +++ b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs @@ -132,7 +132,7 @@ private void OnAnchorComplete(EntityUid uid, AnchorableComponent component, TryA if (TryComp(uid, out var pullable) && pullable.Puller != null) { - _pulling.TryStopPull(uid, pullable); + _pulling.TryStopPull(uid, pullable, ignoreGrab: true); } // TODO: Anchoring snaps rn anyway! diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs index ae22efcd6a53bb..ed7ede0b2eabee 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs @@ -10,6 +10,7 @@ using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Utility; +using Content.Shared.Hands; namespace Content.Shared.Hands.EntitySystems; @@ -98,7 +99,25 @@ private void SwapHandsPressed(ICommonSession? session) private bool DropPressed(ICommonSession? session, EntityCoordinates coords, EntityUid netEntity) { if (TryComp(session?.AttachedEntity, out HandsComponent? hands) && hands.ActiveHand != null) - TryDrop(session.AttachedEntity.Value, hands.ActiveHand, coords, handsComp: hands); + { + if (session != null) + { + var ent = session.AttachedEntity.Value; + + if (TryGetActiveItem(ent, out var item) && TryComp(item, out var virtComp)) + { + var userEv = new VirtualItemDropAttemptEvent(virtComp.BlockingEntity, ent, item.Value, false); + RaiseLocalEvent(ent, userEv); + + var targEv = new VirtualItemDropAttemptEvent(virtComp.BlockingEntity, ent, item.Value, false); + RaiseLocalEvent(virtComp.BlockingEntity, targEv); + + if (userEv.Cancelled || targEv.Cancelled) + return false; + } + TryDrop(ent, hands.ActiveHand, coords, handsComp: hands); + } + } // always send to server. return false; diff --git a/Content.Shared/Hands/HandEvents.cs b/Content.Shared/Hands/HandEvents.cs index 0499c05f4261af..f900d92acd9a46 100644 --- a/Content.Shared/Hands/HandEvents.cs +++ b/Content.Shared/Hands/HandEvents.cs @@ -148,11 +148,52 @@ public sealed class VirtualItemDeletedEvent : EntityEventArgs { public EntityUid BlockingEntity; public EntityUid User; + public EntityUid VirtualItem; - public VirtualItemDeletedEvent(EntityUid blockingEntity, EntityUid user) + public VirtualItemDeletedEvent(EntityUid blockingEntity, EntityUid user, EntityUid virtualItem) { BlockingEntity = blockingEntity; User = user; + VirtualItem = virtualItem; + } + } + + /// + /// Raised directed on both the blocking entity and user when + /// a virtual hand item is thrown (at least attempted to). + /// + public sealed class VirtualItemThrownEvent : EntityEventArgs + { + public EntityUid BlockingEntity; + public EntityUid User; + public EntityUid VirtualItem; + public Vector2 Direction; + public VirtualItemThrownEvent(EntityUid blockingEntity, EntityUid user, EntityUid virtualItem, Vector2 direction) + { + BlockingEntity = blockingEntity; + User = user; + VirtualItem = virtualItem; + Direction = direction; + } + } + + /// + /// Raised directed on both the blocking entity and user when + /// user tries to drop it by keybind. + /// Cancellable. + /// + public sealed class VirtualItemDropAttemptEvent : CancellableEntityEventArgs + { + public EntityUid BlockingEntity; + public EntityUid User; + public EntityUid VirtualItem; + public bool Throw; + public VirtualItemDropAttemptEvent(EntityUid blockingEntity, EntityUid user, EntityUid virtualItem, bool thrown) + { + BlockingEntity = blockingEntity; + User = user; + VirtualItem = virtualItem; + Throw = thrown; } } diff --git a/Content.Shared/Inventory/VirtualItem/SharedVirtualItemSystem.cs b/Content.Shared/Inventory/VirtualItem/SharedVirtualItemSystem.cs index b31cc75576387d..1bd68dac10b7b6 100644 --- a/Content.Shared/Inventory/VirtualItem/SharedVirtualItemSystem.cs +++ b/Content.Shared/Inventory/VirtualItem/SharedVirtualItemSystem.cs @@ -235,10 +235,10 @@ public bool TrySpawnVirtualItem(EntityUid blockingEnt, EntityUid user, [NotNullW /// public void DeleteVirtualItem(Entity item, EntityUid user) { - var userEv = new VirtualItemDeletedEvent(item.Comp.BlockingEntity, user); + var userEv = new VirtualItemDeletedEvent(item.Comp.BlockingEntity, user, item.Owner); RaiseLocalEvent(user, userEv); - var targEv = new VirtualItemDeletedEvent(item.Comp.BlockingEntity, user); + var targEv = new VirtualItemDeletedEvent(item.Comp.BlockingEntity, user, item.Owner); RaiseLocalEvent(item.Comp.BlockingEntity, targEv); if (TerminatingOrDeleted(item)) diff --git a/Content.Shared/MartialArts/Components/GrabThrownComponent.cs b/Content.Shared/MartialArts/Components/GrabThrownComponent.cs new file mode 100644 index 00000000000000..a333d286236fb7 --- /dev/null +++ b/Content.Shared/MartialArts/Components/GrabThrownComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameStates; +using Content.Shared.Damage; + +namespace Content.Shared.MartialArts.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class GrabThrownComponent : Component +{ + public DamageSpecifier? DamageOnCollide; + + public DamageSpecifier? WallDamageOnCollide; + + public float? StaminaDamageOnCollide; +} diff --git a/Content.Shared/MartialArts/Systems/GrabThrownSystem.cs b/Content.Shared/MartialArts/Systems/GrabThrownSystem.cs new file mode 100644 index 00000000000000..656a5eaeb5d6b3 --- /dev/null +++ b/Content.Shared/MartialArts/Systems/GrabThrownSystem.cs @@ -0,0 +1,93 @@ +using Content.Shared.Damage.Systems; +using Content.Shared.Damage; +using Content.Shared.Effects; +using Content.Shared.MartialArts.Components; +using Content.Shared.Throwing; +using Robust.Shared.Network; +using Robust.Shared.Physics.Events; +using Robust.Shared.Player; +using System.Numerics; + +namespace Content.Shared.MartialArts.Systems; + +public sealed class GrabThrownSystem : EntitySystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; + [Dependency] private readonly StaminaSystem _stamina = default!; + [Dependency] private readonly ThrowingSystem _throwing = default!; + [Dependency] private readonly INetManager _netMan = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandleCollide); + SubscribeLocalEvent(OnStopThrow); + } + + private void HandleCollide(EntityUid uid, GrabThrownComponent component, ref StartCollideEvent args) + { + if (_netMan.IsClient) // To avoid effect spam + return; + + if (!HasComp(uid)) + { + RemComp(uid); + return; + } + + if (!args.OurFixture.Hard || !args.OtherFixture.Hard) + return; + + if (!HasComp(uid)) + RemComp(uid); + + var speed = args.OurBody.LinearVelocity.Length(); + + + if (component.StaminaDamageOnCollide != null) + _stamina.TakeStaminaDamage(uid, component.StaminaDamageOnCollide.Value); + + var damageScale = speed; + + if (component.DamageOnCollide != null) + _damageable.TryChangeDamage(uid, component.DamageOnCollide * damageScale); + + if (component.WallDamageOnCollide != null) + _damageable.TryChangeDamage(args.OtherEntity, component.WallDamageOnCollide * damageScale); + + _color.RaiseEffect(Color.Red, new List() { uid }, Filter.Pvs(uid, entityManager: EntityManager)); + + RemComp(uid); + } + + private void OnStopThrow(EntityUid uid, GrabThrownComponent comp, StopThrowEvent args) // We dont need this comp to exsist after fall + { + if (HasComp(uid)) + RemComp(uid); + } + + /// + /// Throwing entity to the direction and ensures GrabThrownComponent with params + /// + /// Entity to throw + /// Direction + /// Stamina damage on collide + /// Damage to entity on collide + /// Damage to wall or anything that was hit by entity + public void Throw( + EntityUid uid, + Vector2 vector, + float? staminaDamage = null, + DamageSpecifier? damageToUid = null, + DamageSpecifier? damageToWall = null) + { + _throwing.TryThrow(uid, vector, 5f, animated: false); + + var comp = EnsureComp(uid); + comp.StaminaDamageOnCollide = staminaDamage; + comp.DamageOnCollide = damageToUid; + comp.WallDamageOnCollide = damageToWall; + } +} diff --git a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs index 100cd9d6d3e6db..4c4b3e79ed28f5 100644 --- a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs +++ b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs @@ -1,6 +1,8 @@ using Content.Shared.Alert; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; +using Content.Shared.Movement.Pulling.Systems; +using Robust.Shared.Timing; namespace Content.Shared.Movement.Pulling.Components; @@ -41,4 +43,22 @@ public sealed partial class PullableComponent : Component [DataField] public ProtoId PulledAlert = "Pulled"; + + [DataField] + public Dictionary PulledAlertAlertSeverity = new() + { + { GrabStage.No, 0 }, + { GrabStage.Soft, 1 }, + { GrabStage.Hard, 2 }, + { GrabStage.Suffocate, 3 }, + }; + + [AutoNetworkedField, DataField] + public GrabStage GrabStage = GrabStage.No; + + [AutoNetworkedField, DataField] + public float GrabEscapeChance = 1f; + + [AutoNetworkedField] + public TimeSpan NextEscapeAttempt = TimeSpan.Zero; } diff --git a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs index 32e4d9b1f31677..15299072d0860a 100644 --- a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs +++ b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs @@ -1,4 +1,4 @@ -using Content.Shared.Alert; +using Content.Shared.Alert; using Content.Shared.Movement.Pulling.Systems; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -43,4 +43,49 @@ public sealed partial class PullerComponent : Component [DataField] public ProtoId PullingAlert = "Pulling"; + + [DataField] + public Dictionary PullingAlertSeverity = new() + { + { GrabStage.No, 0 }, + { GrabStage.Soft, 1 }, + { GrabStage.Hard, 2 }, + { GrabStage.Suffocate, 3 }, + }; + + [DataField, AutoNetworkedField] + public GrabStage GrabStage = GrabStage.No; + + [DataField, AutoNetworkedField] + public GrubStageDirection GrabStageDirection = GrubStageDirection.Increase; + + [AutoNetworkedField] + public TimeSpan NextStageChange; + + [DataField] + public TimeSpan StageChangeCooldown = TimeSpan.FromSeconds(1.5f); + + [DataField] + public Dictionary EscapeChances = new() + { + { GrabStage.No, 1f }, + { GrabStage.Soft, 0.7f }, + { GrabStage.Hard, 0.4f }, + { GrabStage.Suffocate, 0.1f }, + }; + + [DataField] + public float SuffocateGrabStaminaDamage = 10f; + + [DataField] + public float GrabThrowDamageModifier = 1f; + + [ViewVariables] + public List GrabVirtualItems = new(); + + [ViewVariables] + public Dictionary GrabVirtualItemStageCount = new() + { + { GrabStage.Suffocate, 1 }, + }; } diff --git a/Content.Shared/Movement/Pulling/Events/CheckGrabOverridesEvent.cs b/Content.Shared/Movement/Pulling/Events/CheckGrabOverridesEvent.cs new file mode 100644 index 00000000000000..faf3121eb6f0a2 --- /dev/null +++ b/Content.Shared/Movement/Pulling/Events/CheckGrabOverridesEvent.cs @@ -0,0 +1,14 @@ +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Pulling.Systems; + +namespace Content.Shared.Movement.Pulling.Events; + +public sealed class CheckGrabOverridesEvent : EntityEventArgs +{ + public CheckGrabOverridesEvent(GrabStage stage) + { + Stage = stage; + } + + public GrabStage Stage { get; set; } +} diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index f563440af04882..6b807e1b160b91 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -1,30 +1,47 @@ using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Alert; +using Content.Shared.Body.Events; using Content.Shared.Buckle.Components; +using Content.Shared.CombatMode; using Content.Shared.Cuffs.Components; +using Content.Shared.Damage.Systems; +using Content.Shared.Damage; using Content.Shared.Database; -using Content.Shared.Hands; +using Content.Shared.Effects; using Content.Shared.Hands.EntitySystems; +using Content.Shared.Hands; +using Content.Shared.IdentityManagement; using Content.Shared.Input; using Content.Shared.Interaction; +using Content.Shared.Inventory.VirtualItem; using Content.Shared.Item; +using Content.Shared.MartialArts.Components; +using Content.Shared.MartialArts.Systems; +using Content.Shared.MartialArts; +using Content.Shared.Mobs.Components; using Content.Shared.Movement.Events; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Events; using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.Pulling.Events; +using Content.Shared.Speech; using Content.Shared.Standing; using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.Input.Binding; -using Robust.Shared.Physics; +using Robust.Shared.Network; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; +using Robust.Shared.Physics; using Robust.Shared.Player; +using Robust.Shared.Random; using Robust.Shared.Timing; +using System.Linq; namespace Content.Shared.Movement.Pulling.Systems; @@ -43,8 +60,16 @@ public sealed class PullingSystem : EntitySystem [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly HeldSpeedModifierSystem _clothingMoveSpeed = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly StaminaSystem _stamina = default!; + [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private readonly GrabThrownSystem _throwing = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedVirtualItemSystem _virtualSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; public override void Initialize() { @@ -59,6 +84,8 @@ public override void Initialize() SubscribeLocalEvent>(AddPullVerbs); SubscribeLocalEvent(OnPullableContainerInsert); SubscribeLocalEvent(OnModifyUncuffDuration); + SubscribeLocalEvent(OnGrabbedMoveAttempt); + SubscribeLocalEvent(OnGrabbedSpeakAttempt); SubscribeLocalEvent(OnAfterState); SubscribeLocalEvent(OnPullerContainerInsert); @@ -66,6 +93,8 @@ public override void Initialize() SubscribeLocalEvent(OnVirtualItemDeleted); SubscribeLocalEvent(OnRefreshMovespeed); SubscribeLocalEvent(OnDropHandItems); + SubscribeLocalEvent(OnVirtualItemThrown); + SubscribeLocalEvent(OnVirtualItemDropAttempt); SubscribeLocalEvent(OnBuckled); SubscribeLocalEvent(OnGotBuckled); @@ -103,7 +132,12 @@ private void OnDropHandItems(EntityUid uid, PullerComponent pullerComp, DropHand if (!TryComp(pullerComp.Pulling, out PullableComponent? pullableComp)) return; - TryStopPull(pullerComp.Pulling.Value, pullableComp, uid); + foreach (var item in pullerComp.GrabVirtualItems) + { + QueueDel(item); + } + + TryStopPull(pullerComp.Pulling.Value, pullableComp, uid, true); } private void OnPullerContainerInsert(Entity ent, ref EntGotInsertedIntoContainerMessage args) @@ -114,12 +148,17 @@ private void OnPullerContainerInsert(Entity ent, ref EntGotInse if (!TryComp(ent.Comp.Pulling.Value, out PullableComponent? pulling)) return; - TryStopPull(ent.Comp.Pulling.Value, pulling, ent.Owner); + foreach (var item in ent.Comp.GrabVirtualItems) + { + QueueDel(item); + } + + TryStopPull(ent.Comp.Pulling.Value, pulling, ent.Owner, true); } private void OnPullableContainerInsert(Entity ent, ref EntGotInsertedIntoContainerMessage args) { - TryStopPull(ent.Owner, ent.Comp); + TryStopPull(ent.Owner, ent.Comp, ignoreGrab: true); } private void OnModifyUncuffDuration(Entity ent, ref ModifyUncuffDurationEvent args) @@ -145,6 +184,40 @@ private void OnPullerUnpaused(EntityUid uid, PullerComponent component, ref Enti component.NextThrow += args.PausedTime; } + private void OnVirtualItemDropAttempt(EntityUid uid, PullerComponent component, VirtualItemDropAttemptEvent args) + { + if (component.Pulling == null) + return; + + if (component.Pulling != args.BlockingEntity) + return; + + if (_timing.CurTime < component.NextStageChange) + { + args.Cancel(); // VirtualItem is NOT being deleted + return; + } + + if (!args.Throw) + { + if (component.GrabStage > GrabStage.No) + { + if (EntityManager.TryGetComponent(args.BlockingEntity, out PullableComponent? comp)) + { + TryLowerGrabStage(component.Pulling.Value, uid); + args.Cancel(); // VirtualItem is NOT being deleted + } + } + } + else + { + if (component.GrabStage <= GrabStage.Soft) + { + TryLowerGrabStage(component.Pulling.Value, uid); + args.Cancel(); // VirtualItem is NOT being deleted + } + } + } private void OnVirtualItemDeleted(EntityUid uid, PullerComponent component, VirtualItemDeletedEvent args) { // If client deletes the virtual hand then stop the pull. @@ -156,7 +229,67 @@ private void OnVirtualItemDeleted(EntityUid uid, PullerComponent component, Virt if (EntityManager.TryGetComponent(args.BlockingEntity, out PullableComponent? comp)) { - TryStopPull(args.BlockingEntity, comp, uid); + TryLowerGrabStage(component.Pulling.Value, uid); + } + } + + private void OnVirtualItemThrown(EntityUid uid, PullerComponent component, VirtualItemThrownEvent args) + { + if (component.Pulling == null) + return; + + if (component.Pulling != args.BlockingEntity) + return; + + if (EntityManager.TryGetComponent(args.BlockingEntity, out PullableComponent? comp)) + { + if (TryComp(uid, out var combatMode) && + combatMode.IsInCombatMode && + !HasComp(args.BlockingEntity) && + component.GrabStage > GrabStage.Soft) + { + var direction = args.Direction; + var vecBetween = (Transform(args.BlockingEntity).Coordinates.ToMapPos(EntityManager, _transform) - Transform(uid).WorldPosition); + + // Getting angle between us + var dirAngle = direction.ToWorldAngle().Degrees; + var betweenAngle = vecBetween.ToWorldAngle().Degrees; + + var angle = dirAngle - betweenAngle; + + if (angle < 0) + angle = -angle; + + var maxDistance = 3f; + var damageModifier = 1f; + + if (angle < 30) + { + damageModifier = 0.3f; + maxDistance = 1f; + } + else if (angle < 90) + { + damageModifier = 0.7f; + maxDistance = 1.5f; + } + else + maxDistance = 2.25f; + + var distance = Math.Clamp(args.Direction.Length(), 0.5f, maxDistance); + direction *= distance / args.Direction.Length(); + + + var damage = new DamageSpecifier(); + damage.DamageDict.Add("Blunt", 5); + damage *= damageModifier; + + TryStopPull(args.BlockingEntity, comp, uid, true); + _throwing.Throw(args.BlockingEntity, direction, 65f, damage * component.GrabThrowDamageModifier, damage * component.GrabThrowDamageModifier); + _throwing.Throw(uid, -direction * 0.5f); + _audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"), uid); + component.NextStageChange.Add(TimeSpan.FromSeconds(2f)); // To avoid grab and throw spamming + } } } @@ -175,10 +308,19 @@ private void AddPullVerbs(EntityUid uid, PullableComponent component, GetVerbsEv Verb verb = new() { Text = Loc.GetString("pulling-verb-get-data-text-stop-pulling"), - Act = () => TryStopPull(uid, component, user: args.User), + Act = () => TryStopPull(uid, component, user: args.User, true), DoContactInteraction = false // pulling handle its own contact interaction. }; args.Verbs.Add(verb); + + Verb grabVerb = new() // I'm not sure it is a good idea to add a button like this + { + Text = Loc.GetString("pulling-verb-get-data-text-grab"), + Act = () => TryGrab(uid, component.Puller.Value, true), + DoContactInteraction = false // pulling handle its own contact interaction. + }; + args.Verbs.Add(grabVerb); + } else if (CanPull(args.User, args.Target)) { @@ -198,11 +340,46 @@ private void OnRefreshMovespeed(EntityUid uid, PullerComponent component, Refres { var (walkMod, sprintMod) = _clothingMoveSpeed.GetHeldMovementSpeedModifiers(component.Pulling.Value, heldMoveSpeed); - args.ModifySpeed(walkMod, sprintMod); + + switch (component.GrabStage) + { + case GrabStage.No: + args.ModifySpeed(walkMod, sprintMod); + break; + case GrabStage.Soft: + args.ModifySpeed(walkMod * 0.9f, sprintMod * 0.9f); + break; + case GrabStage.Hard: + args.ModifySpeed(walkMod * 0.7f, sprintMod * 0.7f); + break; + case GrabStage.Suffocate: + args.ModifySpeed(walkMod * 0.4f, sprintMod * 0.4f); + break; + default: + args.ModifySpeed(walkMod, sprintMod); + break; + } return; } - args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier); + switch (component.GrabStage) + { + case GrabStage.No: + args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier); + break; + case GrabStage.Soft: + args.ModifySpeed(component.WalkSpeedModifier * 0.9f, component.SprintSpeedModifier * 0.9f); + break; + case GrabStage.Hard: + args.ModifySpeed(component.WalkSpeedModifier * 0.7f, component.SprintSpeedModifier * 0.7f); + break; + case GrabStage.Suffocate: + args.ModifySpeed(component.WalkSpeedModifier * 0.4f, component.SprintSpeedModifier * 0.4f); + break; + default: + args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier); + break; + } } private void OnPullableMoveInput(EntityUid uid, PullableComponent component, ref MoveInputEvent args) @@ -275,6 +452,10 @@ private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp) pullableComp.PullJointId = null; pullableComp.Puller = null; + pullableComp.GrabStage = GrabStage.No; + pullableComp.GrabEscapeChance = 1f; + _blocker.UpdateCanMove(pullableUid); + Dirty(pullableUid, pullableComp); // No more joints with puller -> force stop pull. @@ -283,6 +464,15 @@ private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp) var pullerUid = oldPuller.Value; _alertsSystem.ClearAlert(pullerUid, pullerComp.PullingAlert); pullerComp.Pulling = null; + + pullerComp.GrabStage = GrabStage.No; + List virtItems = pullerComp.GrabVirtualItems; + foreach (var item in virtItems) + { + QueueDel(item); + } + pullerComp.GrabVirtualItems.Clear(); + Dirty(oldPuller.Value, pullerComp); // Messaging @@ -321,7 +511,7 @@ private void OnReleasePulledObject(ICommonSession? session) return; } - TryStopPull(pullerComp.Pulling.Value, pullableComp, user: player); + TryStopPull(pullerComp.Pulling.Value, pullableComp, user: player, true); } public bool CanPull(EntityUid puller, EntityUid pullableUid, PullerComponent? pullerComp = null) @@ -375,12 +565,17 @@ public bool TogglePull(Entity pullable, EntityUid pullerUid) if (!Resolve(pullable, ref pullable.Comp, false)) return false; - if (pullable.Comp.Puller == pullerUid) - { - return TryStopPull(pullable, pullable.Comp); - } + if (pullable.Comp.Puller != pullerUid) + return TryStartPull(pullerUid, pullable, pullableComp: pullable); + + if (TryGrab(pullable, pullerUid)) + return true; + + if (TryComp(pullerUid, out var pullerComp) && _timing.CurTime < pullerComp.NextStageChange) + return true; + + return TryStopPull(pullable, pullable.Comp, ignoreGrab: true); - return TryStartPull(pullerUid, pullable, pullableComp: pullable); } public bool TogglePull(EntityUid pullerUid, PullerComponent puller) @@ -411,7 +606,7 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, // Ensure that the puller is not currently pulling anything. if (TryComp(pullerComp.Pulling, out var oldPullable) - && !TryStopPull(pullerComp.Pulling.Value, oldPullable, pullerUid)) + && !TryStopPull(pullerComp.Pulling.Value, oldPullable, pullerUid, true)) return false; // Stop anyone else pulling the entity we want to pull @@ -422,7 +617,39 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, return false; if (!TryStopPull(pullableUid, pullableComp, pullableComp.Puller)) + { + // Not succeed to retake grabbed entity + if (_netManager.IsServer) + { + _popup.PopupEntity(Loc.GetString("popup-grab-retake-fail", + ("puller", Identity.Entity(pullableComp.Puller.Value, EntityManager)), + ("pulled", Identity.Entity(pullableUid, EntityManager))), + pullerUid, pullerUid, PopupType.MediumCaution); + _popup.PopupEntity(Loc.GetString("popup-grab-retake-fail-puller", + ("puller", Identity.Entity(pullerUid, EntityManager)), + ("pulled", Identity.Entity(pullableUid, EntityManager))), + pullableComp.Puller.Value, pullableComp.Puller.Value, PopupType.MediumCaution); + } + return false; + } + + else if (pullableComp.GrabStage != GrabStage.No) + { + // Successful retake + if (_netManager.IsServer) + { + _popup.PopupEntity(Loc.GetString("popup-grab-retake-success", + ("puller", Identity.Entity(pullableComp.Puller.Value, EntityManager)), + ("pulled", Identity.Entity(pullableUid, EntityManager))), + pullerUid, pullerUid, PopupType.MediumCaution); + _popup.PopupEntity(Loc.GetString("popup-grab-retake-success-puller", + ("puller", Identity.Entity(pullerUid, EntityManager)), + ("pulled", Identity.Entity(pullableUid, EntityManager))), + pullableComp.Puller.Value, pullableComp.Puller.Value, PopupType.MediumCaution); + } + } + } var pullAttempt = new PullAttemptEvent(pullerUid, pullableUid); @@ -471,8 +698,8 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, // Messaging var message = new PullStartedMessage(pullerUid, pullableUid); _modifierSystem.RefreshMovementSpeedModifiers(pullerUid); - _alertsSystem.ShowAlert(pullerUid, pullerComp.PullingAlert); - _alertsSystem.ShowAlert(pullableUid, pullableComp.PulledAlert); + _alertsSystem.ShowAlert(pullerUid, pullerComp.PullingAlert, 0); + _alertsSystem.ShowAlert(pullableUid, pullableComp.PulledAlert, 0); RaiseLocalEvent(pullerUid, message); RaiseLocalEvent(pullableUid, message); @@ -485,7 +712,7 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, return true; } - public bool TryStopPull(EntityUid pullableUid, PullableComponent pullable, EntityUid? user = null) + public bool TryStopPull(EntityUid pullableUid, PullableComponent pullable, EntityUid? user = null, bool ignoreGrab = false) { var pullerUidNull = pullable.Puller; @@ -498,7 +725,284 @@ public bool TryStopPull(EntityUid pullableUid, PullableComponent pullable, Entit if (msg.Cancelled) return false; + // There are some events that should ignore grab stages + if (!ignoreGrab) + { + if (!AttemptGrabRelease(pullableUid)) + { + if (_netManager.IsServer && user != null && user.Value == pullableUid) + _popup.PopupEntity(Loc.GetString("popup-grab-release-fail-self"), pullableUid, pullableUid, PopupType.SmallCaution); + return false; + } + + if (_netManager.IsServer && user != null && user.Value == pullableUid) + { + _popup.PopupEntity(Loc.GetString("popup-grab-release-success-self"), pullableUid, pullableUid, PopupType.SmallCaution); + _popup.PopupEntity(Loc.GetString("popup-grab-release-success-puller", ("target", Identity.Entity(pullableUid, EntityManager))), pullerUidNull.Value, pullerUidNull.Value, PopupType.MediumCaution); + } + } + StopPulling(pullableUid, pullable); return true; } + + /// + /// Trying to grab the target + /// + /// Target that would be grabbed + /// Performer of the grab + /// If true, will ignore disabled combat mode + /// + /// + public bool TryGrab(Entity pullable, Entity puller, bool ignoreCombatMode = false) + { + if (!Resolve(pullable.Owner, ref pullable.Comp)) + return false; + + if (!Resolve(puller.Owner, ref puller.Comp)) + return false; + + if (pullable.Comp.Puller != puller.Owner || + puller.Comp.Pulling != pullable.Owner) + return false; + + if (puller.Comp.NextStageChange > _timing.CurTime) + return false; + + // You can't choke crates + if (!HasComp(pullable)) + return false; + + // Delay to avoid spamming + puller.Comp.NextStageChange = _timing.CurTime + puller.Comp.StageChangeCooldown; + Dirty(puller); + + // Don't grab without combat mode + if (!ignoreCombatMode) + { + if (!TryComp(puller.Owner, out var combatMode) || !combatMode.IsInCombatMode) + return false; + } + + // It's blocking stage update, maybe better UX? + if (puller.Comp.GrabStage == GrabStage.Suffocate) + { + _stamina.TakeStaminaDamage(pullable, puller.Comp.SuffocateGrabStaminaDamage); + + Dirty(pullable); + Dirty(puller); + return true; + } + + // Update stage + // TODO: Change grab stage direction + var nextStageAddition = puller.Comp.GrabStageDirection switch + { + GrubStageDirection.Increase => 1, + GrubStageDirection.Decrease => -1, + _ => throw new ArgumentOutOfRangeException(), + }; + + var newStage = puller.Comp.GrabStage + nextStageAddition; + + if (!TrySetGrabStages((puller.Owner, puller.Comp), (pullable.Owner, pullable.Comp), newStage)) + return true; + + _color.RaiseEffect(Color.Yellow, new List { pullable }, Filter.Pvs(pullable, entityManager: EntityManager)); + return true; + } + + private bool TrySetGrabStages(Entity puller, Entity pullable, GrabStage stage) + { + puller.Comp.GrabStage = stage; + pullable.Comp.GrabStage = stage; + + if (!TryUpdateGrabVirtualItems(puller, pullable)) + return false; + + var filter = Filter.Empty() + .AddPlayersByPvs(Transform(puller)) + .RemovePlayerByAttachedEntity(puller.Owner) + .RemovePlayerByAttachedEntity(pullable.Owner); + + var popupType = stage switch + { + GrabStage.No => PopupType.Small, + GrabStage.Soft => PopupType.Small, + GrabStage.Hard => PopupType.MediumCaution, + GrabStage.Suffocate => PopupType.LargeCaution, + _ => throw new ArgumentOutOfRangeException() + }; + + pullable.Comp.GrabEscapeChance = puller.Comp.EscapeChances[stage]; + + _alertsSystem.ShowAlert(puller, puller.Comp.PullingAlert, puller.Comp.PullingAlertSeverity[stage]); + _alertsSystem.ShowAlert(pullable, pullable.Comp.PulledAlert, pullable.Comp.PulledAlertAlertSeverity[stage]); + + _blocker.UpdateCanMove(pullable); + _modifierSystem.RefreshMovementSpeedModifiers(puller); + + // I'm lazy to write client code + if (!_netManager.IsServer) + return true; + + _popup.PopupEntity(Loc.GetString($"popup-grab-{puller.Comp.GrabStage.ToString().ToLower()}-target", ("puller", Identity.Entity(puller, EntityManager))), pullable, pullable, popupType); + _popup.PopupEntity(Loc.GetString($"popup-grab-{puller.Comp.GrabStage.ToString().ToLower()}-self", ("target", Identity.Entity(pullable, EntityManager))), pullable, puller, PopupType.Medium); + _popup.PopupEntity(Loc.GetString($"popup-grab-{puller.Comp.GrabStage.ToString().ToLower()}-others", ("target", Identity.Entity(pullable, EntityManager)), ("puller", Identity.Entity(puller, EntityManager))), pullable, filter, true, popupType); + + _audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"), pullable); + + Dirty(pullable); + Dirty(puller); + + return true; + } + + private bool TryUpdateGrabVirtualItems(Entity puller, Entity pullable) + { + // Updating virtual items + var virtualItemsCount = puller.Comp.GrabVirtualItems.Count; + + var newVirtualItemsCount = puller.Comp.NeedsHands ? 0 : 1; + if (puller.Comp.GrabVirtualItemStageCount.TryGetValue(puller.Comp.GrabStage, out var count)) + newVirtualItemsCount += count; + + if (virtualItemsCount != newVirtualItemsCount) + { + var delta = newVirtualItemsCount - virtualItemsCount; + + // Adding new virtual items + if (delta > 0) + { + for (var i = 0; i < delta; i++) + { + if (!_virtualSystem.TrySpawnVirtualItemInHand(pullable, puller.Owner, out var item, true)) + { + // I'm lazy write client code + if (_netManager.IsServer) + _popup.PopupEntity(Loc.GetString("popup-grab-need-hand"), puller, puller, PopupType.Medium); + + return false; + } + + puller.Comp.GrabVirtualItems.Add(item.Value); + } + } + + if (delta < 0) + { + for (var i = 0; i < Math.Abs(delta); i++) + { + if (i >= puller.Comp.GrabVirtualItems.Count) + break; + + var item = puller.Comp.GrabVirtualItems[i]; + puller.Comp.GrabVirtualItems.Remove(item); + QueueDel(item); + } + } + } + + return true; + } + + /// + /// Attempts to release entity from grab + /// + /// Grabbed entity + /// + public bool AttemptGrabRelease(Entity pullable) + { + if (!Resolve(pullable.Owner, ref pullable.Comp)) + return false; + if (_timing.CurTime < pullable.Comp.NextEscapeAttempt) // No autoclickers! Mwa-ha-ha + { + return false; + } + + if (_random.Prob(pullable.Comp.GrabEscapeChance)) + return true; + + pullable.Comp.NextEscapeAttempt = _timing.CurTime.Add(TimeSpan.FromSeconds(1)); + Dirty(pullable.Owner, pullable.Comp); + return false; + } + + private void OnGrabbedMoveAttempt(EntityUid uid, PullableComponent component, UpdateCanMoveEvent args) + { + if (component.GrabStage == GrabStage.No) + return; + + args.Cancel(); + + } + + private void OnGrabbedSpeakAttempt(EntityUid uid, PullableComponent component, SpeakAttemptEvent args) + { + if (component.GrabStage != GrabStage.Suffocate) + return; + + _popup.PopupEntity(Loc.GetString("popup-grabbed-cant-speak"), uid, uid, PopupType.MediumCaution); // You cant speak while someone is choking you + + args.Cancel(); + } + + /// + /// Tries to lower grab stage for target or release it + /// + /// Grabbed entity + /// Performer + /// If true, will NOT release target if combat mode is off + /// + public bool TryLowerGrabStage(Entity pullable, Entity puller, bool ignoreCombatMode = false) + { + if (!Resolve(pullable.Owner, ref pullable.Comp)) + return false; + + if (!Resolve(puller.Owner, ref puller.Comp)) + return false; + + if (pullable.Comp.Puller != puller.Owner || + puller.Comp.Pulling != pullable.Owner) + return false; + + if (_timing.CurTime < puller.Comp.NextStageChange) + return true; + + pullable.Comp.NextEscapeAttempt = _timing.CurTime.Add(TimeSpan.FromSeconds(1f)); + Dirty(pullable); + + if (!ignoreCombatMode) + { + if (!TryComp(puller.Owner, out var combatMode) || !combatMode.IsInCombatMode) + { + TryStopPull(pullable, pullable.Comp, ignoreGrab: true); + return true; + } + } + + if (puller.Comp.GrabStage == GrabStage.No) + { + TryStopPull(pullable, pullable.Comp, ignoreGrab: true); + return true; + } + + var newStage = puller.Comp.GrabStage - 1; + TrySetGrabStages((puller.Owner, puller.Comp), (pullable.Owner, pullable.Comp), newStage); + return true; + } +} + +public enum GrabStage +{ + No = 0, + Soft = 1, + Hard = 2, + Suffocate = 3, +} + +public enum GrubStageDirection +{ + Increase, + Decrease, } diff --git a/Content.Shared/Security/Systems/DeployableBarrierSystem.cs b/Content.Shared/Security/Systems/DeployableBarrierSystem.cs index 622edc4b62ecd4..e6e279efdb00e3 100644 --- a/Content.Shared/Security/Systems/DeployableBarrierSystem.cs +++ b/Content.Shared/Security/Systems/DeployableBarrierSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Lock; +using Content.Shared.Lock; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Security.Components; @@ -55,7 +55,7 @@ private void ToggleBarrierDeploy(EntityUid uid, bool isDeployed, DeployableBarri } if (TryComp(uid, out PullableComponent? pullable)) - _pulling.TryStopPull(uid, pullable); + _pulling.TryStopPull(uid, pullable, ignoreGrab: true); SharedPointLightComponent? pointLight = null; if (_pointLight.ResolveLight(uid, ref pointLight)) diff --git a/Content.Shared/Teleportation/Systems/SharedPortalSystem.cs b/Content.Shared/Teleportation/Systems/SharedPortalSystem.cs index 8d67aec518a683..e94dfdece55050 100644 --- a/Content.Shared/Teleportation/Systems/SharedPortalSystem.cs +++ b/Content.Shared/Teleportation/Systems/SharedPortalSystem.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using Content.Shared.Ghost; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Systems; @@ -95,13 +95,13 @@ private void OnCollide(EntityUid uid, PortalComponent component, ref StartCollid // break pulls before portal enter so we dont break shit if (TryComp(subject, out var pullable) && pullable.BeingPulled) { - _pulling.TryStopPull(subject, pullable); + _pulling.TryStopPull(subject, pullable, ignoreGrab: true); } if (TryComp(subject, out var pullerComp) && TryComp(pullerComp.Pulling, out var subjectPulling)) { - _pulling.TryStopPull(subject, subjectPulling); + _pulling.TryStopPull(subject, subjectPulling, ignoreGrab: true); } // if they came from another portal, just return and wait for them to exit the portal diff --git a/Resources/Locale/en-US/pulling/popups.ftl b/Resources/Locale/en-US/pulling/popups.ftl new file mode 100644 index 00000000000000..aa2025f9abf6aa --- /dev/null +++ b/Resources/Locale/en-US/pulling/popups.ftl @@ -0,0 +1,26 @@ +popup-grab-soft-target = {CAPITALIZE($puller)} grabbed you softly. +popup-grab-hard-target = {CAPITALIZE($puller)} grabbed you hardly. +popup-grab-suffocate-target = {CAPITALIZE($puller)} started to choke you! +popup-grab-no-target = {CAPITALIZE($puller)} stopped grabbing you. + +popup-grab-soft-self = You grabbed {CAPITALIZE($target)} softly. +popup-grab-hard-self = You grabbed {CAPITALIZE($target)} hardly. +popup-grab-suffocate-self = You started to choke {CAPITALIZE($target)}. +popup-grab-no-self = You stopped grabbing {CAPITALIZE($target)}. + +popup-grab-soft-others = {CAPITALIZE($puller)} grabbed {CAPITALIZE($target)} softly. +popup-grab-hard-others = {CAPITALIZE($puller)} grabbed {CAPITALIZE($target)} hardly. +popup-grab-suffocate-others = {CAPITALIZE($puller)} started to choke {CAPITALIZE($target)}! +popup-grab-no-others = {CAPITALIZE($puller)} stopped grabbing {CAPITALIZE($target)}. + +popup-grab-release-fail-self = You are trying to escape. +popup-grab-release-success-self = You escaped from grab! +popup-grab-release-success-puller = {CAPITALIZE($target)} escaped! +popup-grab-retake-fail = {CAPITALIZE($puller)} is not letting you to pull {CAPITALIZE($pulled)}! +popup-grab-retake-fail-puller = {CAPITALIZE($puller)} is trying to release {CAPITALIZE($pulled)}! +popup-grab-retake-success = You released {CAPITALIZE($pulled)} from {CAPITALIZE($puller)}'s grab! +popup-grab-retake-success-puller = {CAPITALIZE($puller)} released {CAPITALIZE($pulled)} from your grab! + +popup-grabbed-cant-speak = You can't breathe! + +popup-grab-need-hand = You need a free hand! diff --git a/Resources/Locale/en-US/pulling/pullable-component.ftl b/Resources/Locale/en-US/pulling/pullable-component.ftl index b9d076404d55ad..278042fce18851 100644 --- a/Resources/Locale/en-US/pulling/pullable-component.ftl +++ b/Resources/Locale/en-US/pulling/pullable-component.ftl @@ -1,4 +1,7 @@ ## PullingVerb pulling-verb-get-data-text = Pull -pulling-verb-get-data-text-stop-pulling = Stop pulling \ No newline at end of file +pulling-verb-get-data-text-stop-pulling = Stop pulling + +pulling-verb-get-data-text-grab = Grab + diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index c08453460f4fdd..e01b14f3ab9ac1 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -377,17 +377,37 @@ - type: alert id: Pulled - icons: [ /Textures/Interface/Alerts/Pull/pulled.png ] + icons: + - sprite: /Textures/Interface/Alerts/pull.rsi + state: pulled + - sprite: /Textures/Interface/Alerts/pull.rsi + state: grabbed-soft + - sprite: /Textures/Interface/Alerts/pull.rsi + state: grabbed-hard + - sprite: /Textures/Interface/Alerts/pull.rsi + state: grabbed-choke onClick: !type:StopBeingPulled { } name: alerts-pulled-name description: alerts-pulled-desc + minSeverity: 0 + maxSeverity: 3 - type: alert id: Pulling - icons: [ /Textures/Interface/Alerts/Pull/pulling.png ] + icons: + - sprite: /Textures/Interface/Alerts/pull.rsi + state: pulling + - sprite: /Textures/Interface/Alerts/pull.rsi + state: grab-soft + - sprite: /Textures/Interface/Alerts/pull.rsi + state: grab-hard + - sprite: /Textures/Interface/Alerts/pull.rsi + state: grab-choke onClick: !type:StopPulling { } name: alerts-pulling-name description: alerts-pulling-desc + minSeverity: 0 + maxSeverity: 3 - type: alert id: Bleed diff --git a/Resources/Textures/Interface/Alerts/pull.rsi/grab-choke.png b/Resources/Textures/Interface/Alerts/pull.rsi/grab-choke.png new file mode 100644 index 00000000000000..085c7c389c8879 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/pull.rsi/grab-choke.png differ diff --git a/Resources/Textures/Interface/Alerts/pull.rsi/grab-hard.png b/Resources/Textures/Interface/Alerts/pull.rsi/grab-hard.png new file mode 100644 index 00000000000000..1e703705845540 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/pull.rsi/grab-hard.png differ diff --git a/Resources/Textures/Interface/Alerts/pull.rsi/grab-soft.png b/Resources/Textures/Interface/Alerts/pull.rsi/grab-soft.png new file mode 100644 index 00000000000000..03877d2c1f6875 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/pull.rsi/grab-soft.png differ diff --git a/Resources/Textures/Interface/Alerts/pull.rsi/grabbed-choke.png b/Resources/Textures/Interface/Alerts/pull.rsi/grabbed-choke.png new file mode 100644 index 00000000000000..2b0932f5af096f Binary files /dev/null and b/Resources/Textures/Interface/Alerts/pull.rsi/grabbed-choke.png differ diff --git a/Resources/Textures/Interface/Alerts/pull.rsi/grabbed-hard.png b/Resources/Textures/Interface/Alerts/pull.rsi/grabbed-hard.png new file mode 100644 index 00000000000000..4d7d6a21443ddd Binary files /dev/null and b/Resources/Textures/Interface/Alerts/pull.rsi/grabbed-hard.png differ diff --git a/Resources/Textures/Interface/Alerts/pull.rsi/grabbed-soft.png b/Resources/Textures/Interface/Alerts/pull.rsi/grabbed-soft.png new file mode 100644 index 00000000000000..c0b7e8f7ae657d Binary files /dev/null and b/Resources/Textures/Interface/Alerts/pull.rsi/grabbed-soft.png differ diff --git a/Resources/Textures/Interface/Alerts/pull.rsi/meta.json b/Resources/Textures/Interface/Alerts/pull.rsi/meta.json new file mode 100644 index 00000000000000..0543a9338b3db8 --- /dev/null +++ b/Resources/Textures/Interface/Alerts/pull.rsi/meta.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Original taken from https://github.com/space-wizards/space-station-14, grab icons edited by _kote", + "states": [ + { + "name": "pulled", + "directions": 1 + }, + { + "name": "pulling", + "directions": 1 + }, + { + "name": "grab-soft", + "directions": 1 + }, + { + "name": "grab-hard", + "directions": 1 + }, + { + "name": "grabbed-soft", + "directions": 1 + }, + { + "name": "grabbed-hard", + "directions": 1 + }, + { + "name": "grab-choke", + "directions": 1, + "delays": [ [ 0.5, 0.5 ] ] + }, + { + "name": "grabbed-choke", + "directions": 1, + "delays": [ [ 0.5, 0.5 ] ] + } + ] +} diff --git a/Resources/Textures/Interface/Alerts/Pull/pulled.png b/Resources/Textures/Interface/Alerts/pull.rsi/pulled.png similarity index 100% rename from Resources/Textures/Interface/Alerts/Pull/pulled.png rename to Resources/Textures/Interface/Alerts/pull.rsi/pulled.png diff --git a/Resources/Textures/Interface/Alerts/Pull/pulling.png b/Resources/Textures/Interface/Alerts/pull.rsi/pulling.png similarity index 100% rename from Resources/Textures/Interface/Alerts/Pull/pulling.png rename to Resources/Textures/Interface/Alerts/pull.rsi/pulling.png