diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 2e2209f2ab..973e9bd0cf 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -348,6 +348,7 @@ namespace Content.Client.Entry
"InteractionPopup",
"HealthAnalyzer",
"Thirst",
+ "CanEscapeInventory",
"Wires"
};
}
diff --git a/Content.Server/Resist/CanEscapeInventoryComponent.cs b/Content.Server/Resist/CanEscapeInventoryComponent.cs
new file mode 100644
index 0000000000..9ca1099903
--- /dev/null
+++ b/Content.Server/Resist/CanEscapeInventoryComponent.cs
@@ -0,0 +1,29 @@
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization.Manager.Attributes;
+using Robust.Shared.ViewVariables;
+using Robust.Shared.Analyzers;
+using System.Threading;
+
+namespace Content.Server.Resist;
+
+[RegisterComponent]
+public sealed class CanEscapeInventoryComponent : Component
+{
+ ///
+ /// How long it takes to break out of storage. Default at 5 seconds.
+ ///
+ [ViewVariables]
+ [DataField("resistTime")]
+ public float ResistTime = 5f;
+
+ ///
+ /// For quick exit if the player attempts to move while already resisting
+ ///
+ [ViewVariables]
+ public bool IsResisting = false;
+
+ ///
+ /// Cancellation token used to cancel the DoAfter if the mob is removed before it's complete
+ ///
+ public CancellationTokenSource? CancelToken;
+}
diff --git a/Content.Server/Resist/EscapeInventorySystem.cs b/Content.Server/Resist/EscapeInventorySystem.cs
new file mode 100644
index 0000000000..923f92d3d2
--- /dev/null
+++ b/Content.Server/Resist/EscapeInventorySystem.cs
@@ -0,0 +1,83 @@
+using Content.Shared.Movement;
+using Content.Server.DoAfter;
+using Robust.Shared.Containers;
+using Content.Server.Popups;
+using Content.Shared.Movement.EntitySystems;
+using Robust.Shared.Player;
+using Content.Shared.Storage;
+using Content.Shared.Inventory;
+using Content.Shared.Hands.Components;
+
+namespace Content.Server.Resist;
+
+public sealed class EscapeInventorySystem : EntitySystem
+{
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnRelayMovement);
+ SubscribeLocalEvent(OnMoveAttempt);
+ SubscribeLocalEvent(OnEscapeComplete);
+ SubscribeLocalEvent(OnEscapeFail);
+ }
+
+ private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent component, RelayMoveInputEvent args)
+ {
+ //Prevents the user from creating multiple DoAfters if they're already resisting.
+ if (component.IsResisting == true)
+ return;
+
+ if (_containerSystem.TryGetContainingContainer(uid, out var container)
+ && (HasComp(container.Owner) || HasComp(container.Owner) || HasComp(container.Owner)))
+ {
+ AttemptEscape(uid, container.Owner, component);
+ }
+ }
+
+ private void OnMoveAttempt(EntityUid uid, CanEscapeInventoryComponent component, UpdateCanMoveEvent args)
+ {
+ if (_containerSystem.IsEntityOrParentInContainer(uid))
+ args.Cancel();
+ }
+
+ private void AttemptEscape(EntityUid user, EntityUid container, CanEscapeInventoryComponent component)
+ {
+ component.CancelToken = new();
+ var doAfterEventArgs = new DoAfterEventArgs(user, component.ResistTime, component.CancelToken.Token, container)
+ {
+ BreakOnTargetMove = false,
+ BreakOnUserMove = false,
+ BreakOnDamage = true,
+ BreakOnStun = true,
+ NeedHand = false,
+ UserFinishedEvent = new EscapeDoAfterComplete(),
+ UserCancelledEvent = new EscapeDoAfterCancel(),
+ };
+
+ component.IsResisting = true;
+ _popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting"), user, Filter.Entities(user));
+ _popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting-target"), container, Filter.Entities(container));
+ _doAfterSystem.DoAfter(doAfterEventArgs);
+ }
+
+ private void OnEscapeComplete(EntityUid uid, CanEscapeInventoryComponent component, EscapeDoAfterComplete ev)
+ {
+ //Drops the mob on the tile below the container
+ Transform(uid).AttachParentToContainerOrGrid(EntityManager);
+ component.IsResisting = false;
+ }
+
+ private void OnEscapeFail(EntityUid uid, CanEscapeInventoryComponent component, EscapeDoAfterCancel ev)
+ {
+ component.IsResisting = false;
+ }
+
+ private sealed class EscapeDoAfterComplete : EntityEventArgs { }
+
+ private sealed class EscapeDoAfterCancel : EntityEventArgs { }
+}
diff --git a/Resources/Locale/en-US/resist/components/escape-inventory-component.ftl b/Resources/Locale/en-US/resist/components/escape-inventory-component.ftl
new file mode 100644
index 0000000000..d5d681626f
--- /dev/null
+++ b/Resources/Locale/en-US/resist/components/escape-inventory-component.ftl
@@ -0,0 +1,2 @@
+escape-inventory-component-start-resisting = You start struggling to escape!
+escape-inventory-component-start-resisting-target = Something is struggling to get out of your inventory!
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index d71782d9df..28a831b69a 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -747,6 +747,7 @@
- type: Bloodstream
bloodMaxVolume: 50
- type: DiseaseCarrier #The other class lab animal and disease vector
+ - type: CanEscapeInventory
- type: entity