diff --git a/Content.Server/NPC/HTN/Preconditions/InContainerPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/InContainerPrecondition.cs
new file mode 100644
index 0000000000..aa0ad98ede
--- /dev/null
+++ b/Content.Server/NPC/HTN/Preconditions/InContainerPrecondition.cs
@@ -0,0 +1,27 @@
+using Robust.Server.Containers;
+
+namespace Content.Server.NPC.HTN.Preconditions;
+
+///
+/// Checks if the owner in container or not
+///
+public sealed partial class InContainerPrecondition : HTNPrecondition
+{
+ private ContainerSystem _container = default!;
+
+ [ViewVariables(VVAccess.ReadWrite)] [DataField("isInContainer")] public bool IsInContainer = true;
+
+ public override void Initialize(IEntitySystemManager sysManager)
+ {
+ base.Initialize(sysManager);
+ _container = sysManager.GetEntitySystem();
+ }
+
+ public override bool IsMet(NPCBlackboard blackboard)
+ {
+ var owner = blackboard.GetValue(NPCBlackboard.Owner);
+
+ return IsInContainer && _container.IsEntityInContainer(owner) ||
+ !IsInContainer && !_container.IsEntityInContainer(owner);
+ }
+}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/ContainerOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/ContainerOperator.cs
new file mode 100644
index 0000000000..667d0b8ec4
--- /dev/null
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/ContainerOperator.cs
@@ -0,0 +1,40 @@
+using Robust.Server.Containers;
+
+namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
+
+public sealed partial class ContainerOperator : HTNOperator
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ private ContainerSystem _container = default!;
+ private EntityQuery _transformQuery;
+
+ [DataField("shutdownState")]
+ public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.TaskFinished;
+
+ [DataField("targetKey", required: true)]
+ public string TargetKey = default!;
+
+ public override void Initialize(IEntitySystemManager sysManager)
+ {
+ base.Initialize(sysManager);
+ _container = sysManager.GetEntitySystem();
+ _transformQuery = _entManager.GetEntityQuery();
+ }
+
+ public override void Startup(NPCBlackboard blackboard)
+ {
+ base.Startup(blackboard);
+ var owner = blackboard.GetValue(NPCBlackboard.Owner);
+
+ if (!_container.TryGetOuterContainer(owner, _transformQuery.GetComponent(owner), out var outerContainer) && outerContainer == null)
+ return;
+
+ var target = outerContainer.Owner;
+ blackboard.SetValue(TargetKey, target);
+ }
+
+ public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
+ {
+ return HTNOperatorStatus.Finished;
+ }
+}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/EscapeOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/EscapeOperator.cs
new file mode 100644
index 0000000000..a794e1e314
--- /dev/null
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/EscapeOperator.cs
@@ -0,0 +1,140 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Content.Server.NPC.Components;
+using Content.Server.Storage.EntitySystems;
+using Content.Shared.CombatMode;
+using Robust.Server.Containers;
+
+namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Melee;
+
+public sealed partial class EscapeOperator : HTNOperator, IHtnConditionalShutdown
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ private ContainerSystem _container = default!;
+ private EntityStorageSystem _entityStorage = default!;
+
+ [DataField("shutdownState")]
+ public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.TaskFinished;
+
+ [DataField("targetKey", required: true)]
+ public string TargetKey = default!;
+
+ public override void Initialize(IEntitySystemManager sysManager)
+ {
+ base.Initialize(sysManager);
+ _container = sysManager.GetEntitySystem();
+ _entityStorage = sysManager.GetEntitySystem();
+ }
+
+ public override void Startup(NPCBlackboard blackboard)
+ {
+ base.Startup(blackboard);
+ var owner = blackboard.GetValue(NPCBlackboard.Owner);
+ var target = blackboard.GetValue(TargetKey);
+
+ if (_entityStorage.TryOpenStorage(owner, target))
+ {
+ TaskShutdown(blackboard, HTNOperatorStatus.Finished);
+ return;
+ }
+
+ var melee = _entManager.EnsureComponent(owner);
+ melee.MissChance = blackboard.GetValueOrDefault(NPCBlackboard.MeleeMissChance, _entManager);
+ melee.Target = target;
+ }
+
+ public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
+ CancellationToken cancelToken)
+ {
+ var owner = blackboard.GetValue(NPCBlackboard.Owner);
+ if (!blackboard.TryGetValue(TargetKey, out var target, _entManager))
+ {
+ return (false, null);
+ }
+
+ if (!_container.IsEntityInContainer(owner))
+ {
+ return (false, null);
+ }
+
+ if (_entityStorage.TryOpenStorage(owner, target))
+ {
+ return (false, null);
+ }
+
+ return (true, null);
+ }
+
+ public void ConditionalShutdown(NPCBlackboard blackboard)
+ {
+ var owner = blackboard.GetValue(NPCBlackboard.Owner);
+ _entManager.System().SetInCombatMode(owner, false);
+ _entManager.RemoveComponent(owner);
+ blackboard.Remove(TargetKey);
+ }
+
+ public override void TaskShutdown(NPCBlackboard blackboard, HTNOperatorStatus status)
+ {
+ base.TaskShutdown(blackboard, status);
+
+ ConditionalShutdown(blackboard);
+ }
+
+ public override void PlanShutdown(NPCBlackboard blackboard)
+ {
+ base.PlanShutdown(blackboard);
+
+ ConditionalShutdown(blackboard);
+ }
+
+ public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
+ {
+ base.Update(blackboard, frameTime);
+ var owner = blackboard.GetValue(NPCBlackboard.Owner);
+ HTNOperatorStatus status;
+
+ if (_entManager.TryGetComponent(owner, out var combat) &&
+ blackboard.TryGetValue(TargetKey, out var target, _entManager))
+ {
+ combat.Target = target;
+
+ // Success
+ if (!_container.IsEntityInContainer(owner))
+ {
+ status = HTNOperatorStatus.Finished;
+ }
+ else
+ {
+ if (_entityStorage.TryOpenStorage(owner, target))
+ {
+ status = HTNOperatorStatus.Finished;
+ }
+ else
+ {
+ switch (combat.Status)
+ {
+ case CombatStatus.TargetOutOfRange:
+ case CombatStatus.Normal:
+ status = HTNOperatorStatus.Continuing;
+ break;
+ default:
+ status = HTNOperatorStatus.Failed;
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ status = HTNOperatorStatus.Failed;
+ }
+
+ // Mark it as finished to continue the plan.
+ if (status == HTNOperatorStatus.Continuing && ShutdownState == HTNPlanState.PlanFinished)
+ {
+ status = HTNOperatorStatus.Finished;
+ }
+
+ return status;
+ }
+}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnPullOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnPullOperator.cs
new file mode 100644
index 0000000000..54f422fe67
--- /dev/null
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnPullOperator.cs
@@ -0,0 +1,35 @@
+using Content.Shared.Movement.Pulling.Components;
+using Content.Shared.Movement.Pulling.Systems;
+
+namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
+
+public sealed partial class UnPullOperator : HTNOperator
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ private PullingSystem _pulling = default!;
+
+ private EntityQuery _pullableQuery;
+
+ [DataField("shutdownState")]
+ public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.TaskFinished;
+
+ public override void Initialize(IEntitySystemManager sysManager)
+ {
+ base.Initialize(sysManager);
+ _pulling = sysManager.GetEntitySystem();
+ _pullableQuery = _entManager.GetEntityQuery();
+ }
+
+ public override void Startup(NPCBlackboard blackboard)
+ {
+ base.Startup(blackboard);
+ var owner = blackboard.GetValue(NPCBlackboard.Owner);
+
+ _pulling.TryStopPull(owner, _pullableQuery.GetComponent(owner), owner);
+ }
+
+ public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
+ {
+ return HTNOperatorStatus.Finished;
+ }
+}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs
new file mode 100644
index 0000000000..207665d786
--- /dev/null
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs
@@ -0,0 +1,34 @@
+using Content.Server.Buckle.Systems;
+using Content.Shared.Buckle.Components;
+
+namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
+
+public sealed partial class UnbuckleOperator : HTNOperator
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ private BuckleSystem _buckle = default!;
+
+ [DataField("shutdownState")]
+ public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.TaskFinished;
+
+ public override void Initialize(IEntitySystemManager sysManager)
+ {
+ base.Initialize(sysManager);
+ _buckle = sysManager.GetEntitySystem();
+ }
+
+ public override void Startup(NPCBlackboard blackboard)
+ {
+ base.Startup(blackboard);
+ var owner = blackboard.GetValue(NPCBlackboard.Owner);
+ if (!_entManager.TryGetComponent(owner, out var buckle) || !buckle.Buckled)
+ return;
+
+ _buckle.TryUnbuckle(owner, owner, true, buckle);
+ }
+
+ public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
+ {
+ return HTNOperatorStatus.Finished;
+ }
+}
diff --git a/Resources/Prototypes/NPCs/Combat/melee.yml b/Resources/Prototypes/NPCs/Combat/melee.yml
index 122875ed97..e4b207d16e 100644
--- a/Resources/Prototypes/NPCs/Combat/melee.yml
+++ b/Resources/Prototypes/NPCs/Combat/melee.yml
@@ -17,6 +17,29 @@
- !type:HTNCompoundTask
task: PickupMeleeCompound
+ - preconditions:
+ - !type:BuckledPrecondition
+ isBuckled: true
+ tasks:
+ - !type:HTNPrimitiveTask
+ shutdownState: TaskFinished
+ operator: !type:UnbuckleOperator
+
+ - preconditions:
+ - !type:InContainerPrecondition
+ isInContainer: true
+ tasks:
+ - !type:HTNCompoundTask
+ task: EscapeCompound
+
+ - preconditions:
+ - !type:PulledPrecondition
+ isPulled: true
+ tasks:
+ - !type:HTNPrimitiveTask
+ shutdownState: TaskFinished
+ operator: !type:UnPullOperator
+
# Melee combat (unarmed or otherwise)
- tasks:
- !type:HTNPrimitiveTask
@@ -101,6 +124,21 @@
proto: NearbyMeleeTargets
key: Target
+- type: htnCompound
+ id: EscapeCompound
+ branches:
+ - tasks:
+ - !type:HTNPrimitiveTask
+ shutdownState: TaskFinished
+ operator: !type:ContainerOperator
+ targetKey: Target
+ - !type:HTNPrimitiveTask
+ operator: !type:EscapeOperator
+ targetKey: Target
+ preconditions:
+ - !type:InContainerPrecondition
+ isInContainer: true
+
- type: htnCompound
id: MeleeAttackOrderedTargetCompound
branches: