diff --git a/Content.Client/_Goobstation/MartialArts/MartialArtsSystem.cs b/Content.Client/_Goobstation/MartialArts/MartialArtsSystem.cs
new file mode 100644
index 00000000000..049bc418cfa
--- /dev/null
+++ b/Content.Client/_Goobstation/MartialArts/MartialArtsSystem.cs
@@ -0,0 +1,10 @@
+using Content.Shared._Goobstation.MartialArts;
+
+namespace Content.Client._Goobstation.MartialArts;
+
+///
+/// This handles...
+///
+public sealed class MartialArtsSystem : SharedMartialArtsSystem
+{
+}
diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs
index 3ddef0cba08..3d346192c15 100644
--- a/Content.Server/Body/Systems/RespiratorSystem.cs
+++ b/Content.Server/Body/Systems/RespiratorSystem.cs
@@ -6,6 +6,7 @@
using Content.Server.Chat.Systems;
using Content.Server.EntityEffects.EffectConditions;
using Content.Server.EntityEffects.Effects;
+using Content.Shared._Goobstation.MartialArts.Components; // Goobstation - Martial Arts
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Alert;
using Content.Shared.Atmos;
@@ -21,6 +22,8 @@
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
+using Content.Shared.Movement.Pulling.Components; // Goobstation
+using Content.Shared.Movement.Pulling.Systems; // Goobstation
namespace Content.Server.Body.Systems;
@@ -52,6 +55,20 @@ public override void Initialize()
SubscribeLocalEvent(OnApplyMetabolicMultiplier);
}
+ // Goobstation start
+ // Can breathe check for grab
+ public bool CanBreathe(EntityUid uid, RespiratorComponent respirator)
+ {
+ if(respirator.Saturation < respirator.SuffocationThreshold)
+ return false;
+ if (TryComp(uid, out var pullable)
+ && pullable.GrabStage == GrabStage.Suffocate)
+ return false;
+
+ return !HasComp(uid);
+ }
+ // Goobstation end
+
private void OnMapInit(Entity ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
@@ -111,7 +128,7 @@ public override void Update(float frameTime)
}
}
- if (respirator.Saturation < respirator.SuffocationThreshold)
+ if (!CanBreathe(uid, respirator)) // Goobstation
{
if (_gameTiming.CurTime >= respirator.LastGaspEmoteTime + respirator.GaspEmoteCooldown)
{
diff --git a/Content.Server/Hands/Systems/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs
index 0062fc8198f..cfea6fe9900 100644
--- a/Content.Server/Hands/Systems/HandsSystem.cs
+++ b/Content.Server/Hands/Systems/HandsSystem.cs
@@ -9,6 +9,7 @@
using Content.Shared.CombatMode;
using Content.Shared.Damage.Systems;
using Content.Shared.Explosion;
+using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Input;
@@ -94,7 +95,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); // Goobstation: Added check for grab
var offsetRandomCoordinates = _transformSystem.GetMoverCoordinates(args.Target).Offset(_random.NextVector2(1f, 1.5f));
if (!ThrowHeldItem(args.Target, offsetRandomCoordinates))
@@ -201,6 +202,20 @@ private bool HandleThrowItem(ICommonSession? playerSession, EntityCoordinates co
if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
return false;
+ // Goobstation start
+ 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;
+ }
+ // Goobstation end
+
return ThrowHeldItem(player, coordinates);
}
@@ -215,6 +230,19 @@ hands.ActiveHandEntity is not { } throwEnt ||
!_actionBlockerSystem.CanThrow(player, throwEnt))
return false;
+ // Goobstation start: Added throwing for grabbed mobs, mnoved direction.
+ var direction = _transformSystem.ToMapCoordinates(coordinates).Position - _transformSystem.GetWorldPosition(player);
+
+ 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);
+ }
+ // Goobstation end
+
if (_timing.CurTime < hands.NextThrowTime)
return false;
hands.NextThrowTime = _timing.CurTime + hands.ThrowCooldown;
@@ -229,7 +257,6 @@ hands.ActiveHandEntity is not { } throwEnt ||
throwEnt = splitStack.Value;
}
- var direction = coordinates.ToMapPos(EntityManager, _transformSystem) - Transform(player).WorldPosition;
if (direction == Vector2.Zero)
return true;
diff --git a/Content.Server/Stunnable/Systems/StunbatonSystem.cs b/Content.Server/Stunnable/Systems/StunbatonSystem.cs
index efe8cc442eb..333b97996bd 100644
--- a/Content.Server/Stunnable/Systems/StunbatonSystem.cs
+++ b/Content.Server/Stunnable/Systems/StunbatonSystem.cs
@@ -1,6 +1,8 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.Events;
+using Content.Server.Stunnable.Components;
+using Content.Shared._Goobstation.MartialArts;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.Events;
using Content.Shared.Examine;
@@ -31,11 +33,16 @@ public override void Initialize()
private void OnStaminaHitAttempt(Entity entity, ref StaminaDamageOnHitAttemptEvent args)
{
- if (!_itemToggle.IsActivated(entity.Owner) ||
- !TryComp(entity.Owner, out var battery) || !_battery.TryUseCharge(entity.Owner, entity.Comp.EnergyPerUse, battery))
+ // Goobstation start
+ var energy = entity.Comp.EnergyPerUse;
+
+ if (!_itemToggle.IsActivated(entity.Owner)
+ || !TryComp(entity.Owner, out var battery)
+ || !_battery.TryUseCharge(entity.Owner, energy, battery))
{
args.Cancelled = true;
}
+ // Goobstation end
}
private void OnExamined(Entity entity, ref ExaminedEvent args)
diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
index f1c3144c925..b414b24a853 100644
--- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
+++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
@@ -11,6 +11,7 @@
using Robust.Shared.Player;
using System.Linq;
using System.Numerics;
+using Content.Shared._Goobstation.MartialArts.Events;
namespace Content.Server.Weapons.Melee;
@@ -88,10 +89,10 @@ protected override void DoDamageEffect(List targets, EntityUid? user,
{
// Filter out any deleted entities that may have been destroyed by the damage
var validTargets = targets.Where(t => !Deleted(t)).ToList();
-
+
if (validTargets.Count == 0)
return;
-
+
// Use coordinates from the targetXform if valid, otherwise fall back to user coordinates
var coordinates = targetXform.Coordinates;
if (!coordinates.IsValid(EntityManager))
@@ -101,7 +102,7 @@ protected override void DoDamageEffect(List targets, EntityUid? user,
else
return; // No valid coordinates available
}
-
+
var filter = Filter.Pvs(coordinates, entityMan: EntityManager).RemoveWhereAttachedEntity(o => o == user);
_color.RaiseEffect(Color.Red, validTargets, filter);
}
diff --git a/Content.Server/_Goobstation/MartialArts/MartialArtsSystem.cs b/Content.Server/_Goobstation/MartialArts/MartialArtsSystem.cs
new file mode 100644
index 00000000000..2ac61098e67
--- /dev/null
+++ b/Content.Server/_Goobstation/MartialArts/MartialArtsSystem.cs
@@ -0,0 +1,26 @@
+using Content.Server.Chat.Systems;
+using Content.Shared.Chat; // HardLight
+using Content.Shared._Goobstation.MartialArts;
+using Content.Shared._Goobstation.MartialArts.Components;
+using Content.Shared._Goobstation.MartialArts.Events;
+
+namespace Content.Server._Goobstation.MartialArts;
+
+///
+/// Just handles carp sayings for now.
+///
+public sealed class MartialArtsSystem : SharedMartialArtsSystem
+{
+ [Dependency] private readonly ChatSystem _chat = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnSleepingCarpSaying);
+ }
+
+ private void OnSleepingCarpSaying(Entity ent, ref SleepingCarpSaying args)
+ {
+ _chat.TrySendInGameICMessage(ent, Loc.GetString(args.Saying), InGameICChatType.Speak, false);
+ }
+}
diff --git a/Content.Server/_Goobstation/Teleportation/Systems/TeleportSystem.cs b/Content.Server/_Goobstation/Teleportation/Systems/TeleportSystem.cs
index b36243a6c8c..d634fbccad0 100644
--- a/Content.Server/_Goobstation/Teleportation/Systems/TeleportSystem.cs
+++ b/Content.Server/_Goobstation/Teleportation/Systems/TeleportSystem.cs
@@ -71,7 +71,7 @@ public void RandomTeleport(EntityUid uid, float radius, SoundSpecifier sound, in
// 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(uid, out var pull) && _pullingSystem.IsPulled(uid, pull))
- _pullingSystem.TryStopPull(uid, pull);
+ _pullingSystem.TryStopPull(uid, pull, ignoreGrab: true); // HardLight: pullable(uid, out var pullable))
{
- _pulling.TryStopPull(uid, pullable);
+ _pulling.TryStopPull(uid, pullable, ignoreGrab: true); // Goobstation
}
UpdateCanMove(uid, component, args);
diff --git a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
index 70214b18d65..f490c870e54 100644
--- a/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
+++ b/Content.Shared/Construction/EntitySystems/AnchorableSystem.cs
@@ -152,7 +152,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); // Goobstation
}
// TODO: Anchoring snaps rn anyway!
diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs
index 9c386673998..27782cf496b 100644
--- a/Content.Shared/Cuffs/SharedCuffableSystem.cs
+++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs
@@ -19,10 +19,12 @@
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Item;
using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Popups;
using Content.Shared.Pulling.Events;
using Content.Shared.Rejuvenate;
+using Content.Shared.Movement.Pulling.Systems;
using Content.Shared.Stunnable;
using Content.Shared.Timing;
using Content.Shared.Verbs;
@@ -813,10 +815,10 @@ public IReadOnlyList GetAllCuffs(CuffableComponent component)
private sealed partial class UnCuffDoAfterEvent : SimpleDoAfterEvent
{
}
-
- [Serializable, NetSerializable]
- private sealed partial class AddCuffDoAfterEvent : SimpleDoAfterEvent
- {
- }
}
}
+
+[Serializable, NetSerializable]
+public sealed partial class AddCuffDoAfterEvent : SimpleDoAfterEvent // Goobstation: Moved out of class made public
+{
+}
diff --git a/Content.Shared/Damage/Systems/StaminaSystem.cs b/Content.Shared/Damage/Systems/StaminaSystem.cs
index 96454d20dd8..dee5d1b65a2 100644
--- a/Content.Shared/Damage/Systems/StaminaSystem.cs
+++ b/Content.Shared/Damage/Systems/StaminaSystem.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using Content.Shared._Goobstation.MartialArts.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.CCVar;
@@ -145,6 +146,13 @@ private void OnMeleeHit(EntityUid uid, StaminaDamageOnHitComponent component, Me
return;
}
+ // Goobstation - Martial Arts
+ if (TryComp(args.User, out var knowledgeComp)
+ && TryComp(args.Weapon, out var blockedComp)
+ && knowledgeComp.MartialArtsForm == blockedComp.Form)
+ return;
+ // Goobstation
+
var ev = new StaminaDamageOnHitAttemptEvent();
RaiseLocalEvent(uid, ref ev);
if (ev.Cancelled)
diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs
index 578ac6c4d18..c6661b14e13 100644
--- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs
+++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs
@@ -118,7 +118,27 @@ private void SwapHandsPreviousPressed(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);
+ {
+ // Goobstation start
+ 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);
+ }
+ // Goobstation end
+ }
// always send to server.
return false;
diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs
index fa6ce655656..8b1220a179b 100644
--- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs
+++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs
@@ -9,6 +9,7 @@
using Content.Shared.Storage.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.Input.Binding;
+using Robust.Shared.Network;
namespace Content.Shared.Hands.EntitySystems;
@@ -19,6 +20,7 @@ public abstract partial class SharedHandsSystem
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
[Dependency] private readonly SharedVirtualItemSystem _virtualSystem = default!;
diff --git a/Content.Shared/Hands/HandEvents.cs b/Content.Shared/Hands/HandEvents.cs
index 0499c05f426..9e4f724835f 100644
--- a/Content.Shared/Hands/HandEvents.cs
+++ b/Content.Shared/Hands/HandEvents.cs
@@ -140,6 +140,8 @@ public PickupAnimationEvent(NetEntity itemUid,
}
}
+ // Goobstation start
+ // Added virtual items for grab intent, this is heavily edited please do not bulldoze.
///
/// Raised directed on both the blocking entity and user when
/// a virtual hand item is deleted.
@@ -148,14 +150,56 @@ 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;
+ }
+ }
+ // Goobstation end
+
///
/// Raised when putting an entity into a hand slot
///
diff --git a/Content.Shared/Inventory/VirtualItem/SharedVirtualItemSystem.cs b/Content.Shared/Inventory/VirtualItem/SharedVirtualItemSystem.cs
index 83e6d8ad283..8434c5e4230 100644
--- a/Content.Shared/Inventory/VirtualItem/SharedVirtualItemSystem.cs
+++ b/Content.Shared/Inventory/VirtualItem/SharedVirtualItemSystem.cs
@@ -233,7 +233,7 @@ public bool TrySpawnVirtualItem(EntityUid blockingEnt, EntityUid user, [NotNullW
{
var pos = Transform(user).Coordinates;
virtualItem = PredictedSpawnAttachedTo(VirtualItem, pos);
- var virtualItemComp = Comp(virtualItem.Value);
+ var virtualItemComp = EnsureComp(virtualItem.Value); // Goobstation
virtualItemComp.BlockingEntity = blockingEnt;
Dirty(virtualItem.Value, virtualItemComp);
return true;
@@ -244,10 +244,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); // Goobstation
RaiseLocalEvent(user, userEv);
- var targEv = new VirtualItemDeletedEvent(item.Comp.BlockingEntity, user);
+ var targEv = new VirtualItemDeletedEvent(item.Comp.BlockingEntity, user, item.Owner); // Goobstation
RaiseLocalEvent(item.Comp.BlockingEntity, targEv);
if (TerminatingOrDeleted(item))
diff --git a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs
index aa44669fd14..8de18fd5730 100644
--- a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs
+++ b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs
@@ -1,4 +1,6 @@
+using Content.Shared._Goobstation.TableSlam; // Goobstation - Table SLam
using Content.Shared.Alert;
+using Content.Shared.Movement.Pulling.Systems; // Goobstation
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -8,7 +10,7 @@ namespace Content.Shared.Movement.Pulling.Components;
/// Specifies an entity as being pullable by an entity with
///
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
-[Access(typeof(Systems.PullingSystem))]
+[Access(typeof(Systems.PullingSystem), typeof(TableSlamSystem))]
public sealed partial class PullableComponent : Component
{
///
@@ -39,8 +41,55 @@ public sealed partial class PullableComponent : Component
[AutoNetworkedField, DataField]
public bool PrevFixedRotation;
+ // Goobstation start
+ // Added Grab variables
+ [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;
+
[DataField]
public ProtoId PulledAlert = "Pulled";
+
+ [AutoNetworkedField]
+ public TimeSpan NextEscapeAttempt = TimeSpan.Zero;
+
+ ///
+ /// If this pullable being tabled.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool BeingTabled = false;
+
+ ///
+ /// Constant for tabling throw math
+ ///
+ [DataField]
+ public float BasedTabledForceSpeed = 5f;
+
+ ///
+ /// Stamina damage. taken on tabled
+ ///
+ [DataField]
+ public float TabledStaminaDamage = 40f;
+
+ ///
+ /// Damage taken on being tabled.
+ ///
+ [DataField]
+ public float TabledDamage = 5f;
+ // Goobstation end
}
-public sealed partial class StopBeingPulledAlertEvent : BaseAlertEvent;
+public sealed partial class StopBeingPulledAlertEvent : BaseAlertEvent
+{
+};
diff --git a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs
index 197d7cfd7c8..30107ed5821 100644
--- a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs
+++ b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs
@@ -1,4 +1,6 @@
-using Content.Shared.Alert;
+using Content.Shared._Goobstation.MartialArts;
+using Content.Shared._Goobstation.TableSlam; // Goobstation - Table Slam
+using Content.Shared.Alert;
using Content.Shared.Movement.Pulling.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -10,7 +12,7 @@ namespace Content.Shared.Movement.Pulling.Components;
/// Specifies an entity as being able to pull another entity with
///
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
-[Access(typeof(PullingSystem))]
+[Access(typeof(PullingSystem), typeof(TableSlamSystem), typeof(SharedMartialArtsSystem))] // Goobstation - Table Slam
public sealed partial class PullerComponent : Component
{
// My raiding guild
@@ -43,6 +45,69 @@ public sealed partial class PullerComponent : Component
[DataField]
public ProtoId PullingAlert = "Pulling";
+
+ // Goobstation start
+ // Added Grab variables
+ [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 GrabStageDirection GrabStageDirection = GrabStageDirection.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 = 2f; // Goobstation: Was 1f
+
+ [ViewVariables]
+ public List GrabVirtualItems = new();
+
+ [ViewVariables]
+ public Dictionary GrabVirtualItemStageCount = new()
+ {
+ { GrabStage.Suffocate, 1 },
+ };
+
+ [DataField]
+ public float GrabThrownSpeed = 7f;
+
+ [DataField]
+ public float ThrowingDistance = 4f;
+
+ [DataField]
+ public float SoftGrabSpeedModifier = 0.9f;
+
+ [DataField]
+ public float HardGrabSpeedModifier = 0.7f;
+
+ [DataField]
+ public float ChokeGrabSpeedModifier = 0.4f;
+ // Goobstation end
}
public sealed partial class StopPullingAlertEvent : BaseAlertEvent;
diff --git a/Content.Shared/Movement/Pulling/Events/CheckGrabOverridesEvent.cs b/Content.Shared/Movement/Pulling/Events/CheckGrabOverridesEvent.cs
new file mode 100644
index 00000000000..faf3121eb6f
--- /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 369225df2de..3ee9c615f40 100644
--- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
+++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
@@ -1,16 +1,29 @@
+using Content.Shared._Goobstation.MartialArts.Events; // Goobstation - Martial Arts
+using Content.Shared._EinsteinEngines.Contests; // Goobstation - Grab Intent
+using Content.Shared._Goobstation.MartialArts.Components; // Goobstation - Grab Intent
+using Content.Shared._White.Grab; // Goobstation
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
-using Content.Shared.Cuffs.Components;
+using Content.Shared.CombatMode;
+using Content.Shared.CombatMode.Pacification; // Goobstation
+using Content.Shared.Cuffs; // Goobstation
+using Content.Shared.Cuffs.Components; // Goobstation
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components; // Goobstation
+using Content.Shared.Damage.Systems; // Goobstation
using Content.Shared.Database;
+using Content.Shared.Effects; // Goobstation
using Content.Shared.Hands;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Input;
using Content.Shared.Interaction;
+using Content.Shared.Inventory.VirtualItem; // Goobstation
using Content.Shared.Item;
using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components; // Goobstation
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Components;
@@ -18,15 +31,21 @@
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Pulling.Events;
+using Content.Shared.Speech; // Goobstation
using Content.Shared.Standing;
+using Content.Shared.Throwing; // Goobstation
using Content.Shared.Verbs;
+using Robust.Shared.Audio; // Goobstation
+using Robust.Shared.Audio.Systems; // Goobstation
using Robust.Shared.Containers;
using Robust.Shared.Input.Binding;
+using Robust.Shared.Network; // Goobstation
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
+using Robust.Shared.Random; // Goobstation
using Robust.Shared.Timing;
namespace Content.Shared.Movement.Pulling.Systems;
@@ -48,6 +67,18 @@ public sealed class PullingSystem : EntitySystem
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly HeldSpeedModifierSystem _clothingMoveSpeed = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
+ // Goobstation start
+ [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 SharedVirtualItemSystem _virtualSystem = default!;
+ [Dependency] private readonly GrabThrownSystem _grabThrown = default!;
+ [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
+ [Dependency] private readonly ThrowingSystem _throwing = default!;
+ [Dependency] private readonly ContestsSystem _contests = default!; // Goobstation - Grab Intent
+ // Goobstation end
public override void Initialize()
{
@@ -63,6 +94,8 @@ public override void Initialize()
SubscribeLocalEvent(OnPullableContainerInsert);
SubscribeLocalEvent(OnModifyUncuffDuration);
SubscribeLocalEvent(OnStopBeingPulledAlert);
+ SubscribeLocalEvent(OnGrabbedMoveAttempt); // Goobstation
+ SubscribeLocalEvent(OnGrabbedSpeakAttempt); // Goobstation
SubscribeLocalEvent(OnStateChanged, after: [typeof(MobThresholdSystem)]);
SubscribeLocalEvent(OnAfterState);
@@ -72,6 +105,8 @@ public override void Initialize()
SubscribeLocalEvent(OnRefreshMovespeed);
SubscribeLocalEvent(OnDropHandItems);
SubscribeLocalEvent(OnStopPullingAlert);
+ SubscribeLocalEvent(OnVirtualItemThrown); // Goobstation - Grab Intent
+ SubscribeLocalEvent(OnAddCuffDoAfterEvent); // Goobstation - Grab Intent
SubscribeLocalEvent(OnBuckled);
SubscribeLocalEvent(OnGotBuckled);
@@ -81,6 +116,22 @@ public override void Initialize()
.Register();
}
+ // Goobstation - Grab Intent
+ private void OnAddCuffDoAfterEvent(Entity ent, ref AddCuffDoAfterEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ if (!args.Cancelled
+ && TryComp(ent.Comp.Pulling, out var comp)
+ && ent.Comp.Pulling != null)
+ {
+ if(_netManager.IsServer)
+ StopPulling(ent.Comp.Pulling.Value, comp);
+ }
+ }
+ // Goobstation
+
private void OnStateChanged(EntityUid uid, PullerComponent component, ref UpdateMobStateEvent args)
{
if (component.Pulling == null)
@@ -120,7 +171,12 @@ private void OnDropHandItems(EntityUid uid, PullerComponent pullerComp, DropHand
if (!TryComp(pullerComp.Pulling, out PullableComponent? pullableComp))
return;
- TryStopPull(pullerComp.Pulling.Value, pullableComp, uid);
+ // Goobstation - Grab Intent
+ foreach (var item in pullerComp.GrabVirtualItems) // HardLight ent.Comp ent, ref StopPullingAlertEvent args)
@@ -145,7 +201,7 @@ private void OnPullerContainerInsert(Entity ent, ref EntGotInse
private void OnPullableContainerInsert(Entity ent, ref EntGotInsertedIntoContainerMessage args)
{
- TryStopPull(ent.Owner, ent.Comp);
+ TryStopPull(ent.Owner, ent.Comp, ignoreGrab: true); // Goobstation
}
private void OnModifyUncuffDuration(Entity ent, ref ModifyUncuffDurationEvent args)
@@ -179,20 +235,63 @@ private void OnPullerUnpaused(EntityUid uid, PullerComponent component, ref Enti
component.NextThrow += args.PausedTime;
}
- private void OnVirtualItemDeleted(EntityUid uid, PullerComponent component, VirtualItemDeletedEvent args)
+ // Goobstation - Grab Intent Refactor
+ private void OnVirtualItemDeleted(Entity ent, ref VirtualItemDeletedEvent args)
{
// If client deletes the virtual hand then stop the pull.
- if (component.Pulling == null)
+ if (ent.Comp.Pulling == null)
return;
- if (component.Pulling != args.BlockingEntity)
+ if (ent.Comp.Pulling != args.BlockingEntity)
return;
- if (EntityManager.TryGetComponent(args.BlockingEntity, out PullableComponent? comp))
+ if (TryComp(args.BlockingEntity, out PullableComponent? comp))
+ {
+ TryStopPull(ent.Comp.Pulling.Value, comp, ent);
+ }
+
+ foreach (var item in ent.Comp.GrabVirtualItems)
{
- TryStopPull(args.BlockingEntity, comp);
+ if(TryComp(ent, out var virtualItemComponent))
+ _virtualSystem.DeleteVirtualItem((item,virtualItemComponent), ent);
}
+ ent.Comp.GrabVirtualItems.Clear();
+ }
+ // Goobstation - Grab Intent Refactor
+
+ // Goobstation - Grab Intent
+ private void OnVirtualItemThrown(EntityUid uid, PullerComponent component, VirtualItemThrownEvent args)
+ {
+ if (!TryComp(uid, out var throwerPhysics)
+ || component.Pulling == null
+ || component.Pulling != args.BlockingEntity)
+ return;
+
+ if (!TryComp(args.BlockingEntity, out PullableComponent? comp))
+ return;
+
+ if (!_combatMode.IsInCombatMode(uid)
+ || HasComp(args.BlockingEntity)
+ || component.GrabStage <= GrabStage.Soft)
+ return;
+
+ var distanceToCursor = args.Direction.Length();
+ var direction = args.Direction.Normalized() * MathF.Min(distanceToCursor, component.ThrowingDistance);
+
+ var damage = new DamageSpecifier();
+ damage.DamageDict.Add("Blunt", 5);
+
+ TryStopPull(args.BlockingEntity, comp, uid, true);
+ _grabThrown.Throw(args.BlockingEntity,
+ uid,
+ direction,
+ component.GrabThrownSpeed,
+ damage * component.GrabThrowDamageModifier); // Throwing the grabbed person
+ _throwing.TryThrow(uid, -direction * throwerPhysics.InvMass); // Throws back the grabber
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"), uid);
+ component.NextStageChange = _timing.CurTime.Add(TimeSpan.FromSeconds(3f)); // To avoid grab and throw spamming
}
+ // Goobstation
private void AddPullVerbs(EntityUid uid, PullableComponent component, GetVerbsEvent args)
{
@@ -228,30 +327,84 @@ private void AddPullVerbs(EntityUid uid, PullableComponent component, GetVerbsEv
private void OnRefreshMovespeed(EntityUid uid, PullerComponent component, RefreshMovementSpeedModifiersEvent args)
{
- if (TryComp(component.Pulling, out var heldMoveSpeed) && component.Pulling.HasValue)
+ if (TryComp(component.Pulling, out var itemHeldSpeed) && component.Pulling.HasValue)
{
var (walkMod, sprintMod) =
- _clothingMoveSpeed.GetHeldMovementSpeedModifiers(component.Pulling.Value, heldMoveSpeed);
+ _clothingMoveSpeed.GetHeldMovementSpeedModifiers(component.Pulling.Value, itemHeldSpeed);
args.ModifySpeed(walkMod, sprintMod);
+ }
+
+ // Goobstation start - Grab Intent
+ if (TryComp(component.Pulling, out var heldMoveSpeed) && component.Pulling.HasValue)
+ {
+ var (walkMod, sprintMod) = (args.WalkSpeedModifier, args.SprintSpeedModifier);
+
+ switch (component.GrabStage)
+ {
+ case GrabStage.No:
+ args.ModifySpeed(walkMod, sprintMod);
+ break;
+ case GrabStage.Soft:
+ var softGrabSpeedMod = component.SoftGrabSpeedModifier;
+ args.ModifySpeed(walkMod * softGrabSpeedMod, sprintMod * softGrabSpeedMod);
+ break;
+ case GrabStage.Hard:
+ var hardGrabSpeedModifier = component.HardGrabSpeedModifier;
+ args.ModifySpeed(walkMod * hardGrabSpeedModifier, sprintMod * hardGrabSpeedModifier);
+ break;
+ case GrabStage.Suffocate:
+ var chokeSpeedMod = component.ChokeGrabSpeedModifier;
+ args.ModifySpeed(walkMod * chokeSpeedMod, sprintMod * chokeSpeedMod);
+ 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:
+ var softGrabSpeedMod = component.SoftGrabSpeedModifier;
+ args.ModifySpeed(component.WalkSpeedModifier * softGrabSpeedMod, component.SprintSpeedModifier * softGrabSpeedMod);
+ break;
+ case GrabStage.Hard:
+ var hardGrabSpeedModifier = component.HardGrabSpeedModifier;
+ args.ModifySpeed(component.WalkSpeedModifier * hardGrabSpeedModifier, component.SprintSpeedModifier * hardGrabSpeedModifier);
+ break;
+ case GrabStage.Suffocate:
+ var chokeSpeedMod = component.ChokeGrabSpeedModifier;
+ args.ModifySpeed(component.WalkSpeedModifier * chokeSpeedMod, component.SprintSpeedModifier * chokeSpeedMod);
+ break;
+ default:
+ args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
+ break;
+ }
+ // Goobstation end
}
- private void OnPullableMoveInput(EntityUid uid, PullableComponent component, ref MoveInputEvent args)
+ // Goobstation - Grab Intent
+ private void OnPullableMoveInput(Entity ent, ref MoveInputEvent args)
{
// If someone moves then break their pulling.
- if (!component.BeingPulled)
+ if (!ent.Comp.BeingPulled)
return;
var entity = args.Entity;
+ if (ent.Comp.GrabStage == GrabStage.Soft)
+ TryStopPull(ent, ent, ent);
+
if (!_blocker.CanMove(entity))
return;
- TryStopPull(uid, component, user: uid);
+ TryStopPull(ent, ent, user: ent);
}
+ // Goobstation
private void OnPullableCollisionChange(EntityUid uid, PullableComponent component, ref CollisionChangeEvent args)
{
@@ -285,9 +438,6 @@ private void OnJointRemoved(EntityUid uid, PullableComponent component, JointRem
///
private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
{
- if (pullableComp.Puller == null)
- return;
-
if (!_timing.ApplyingState)
{
// Joint shutdown
@@ -309,14 +459,28 @@ private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
pullableComp.PullJointId = null;
pullableComp.Puller = null;
+ // Goobstation - Grab Intent
+ pullableComp.GrabStage = GrabStage.No;
+ pullableComp.GrabEscapeChance = 1f;
+ _blocker.UpdateCanMove(pullableUid);
+ // Goobstation
Dirty(pullableUid, pullableComp);
// No more joints with puller -> force stop pull.
if (TryComp(oldPuller, out var pullerComp))
{
var pullerUid = oldPuller.Value;
- _alertsSystem.ClearAlert(pullerUid, pullerComp.PullingAlert);
+ if (_netManager.IsServer)
+ _alertsSystem.ClearAlert(pullerUid, pullerComp.PullingAlert);
pullerComp.Pulling = null;
+ // Goobstation - Grab Intent
+ pullerComp.GrabStage = GrabStage.No;
+ var virtItems = pullerComp.GrabVirtualItems;
+ foreach (var item in virtItems)
+ QueueDel(item);
+
+ pullerComp.GrabVirtualItems.Clear();
+ // Goobstation
Dirty(oldPuller.Value, pullerComp);
// Messaging
@@ -328,7 +492,8 @@ private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
RaiseLocalEvent(pullableUid, message);
}
- _alertsSystem.ClearAlert(pullableUid, pullableComp.PulledAlert);
+ if (_netManager.IsServer)
+ _alertsSystem.ClearAlert(pullableUid, pullableComp.PulledAlert);
}
public bool IsPulled(EntityUid uid, PullableComponent? component = null)
@@ -364,7 +529,7 @@ private void OnReleasePulledObject(ICommonSession? session)
return;
}
- TryStopPull(pullerComp.Pulling.Value, pullableComp, user: player);
+ TryStopPull(pullerComp.Pulling.Value, pullableComp, user: player, true); // Goobstation
}
public bool CanPull(EntityUid puller, EntityUid pullableUid, PullerComponent? pullerComp = null)
@@ -386,7 +551,7 @@ public bool CanPull(EntityUid puller, EntityUid pullableUid, PullerComponent? pu
return false;
}
- if (!EntityManager.TryGetComponent(pullableUid, out var physics))
+ if (!TryComp(pullableUid, out var physics)) // Goobstation
{
return false;
}
@@ -413,18 +578,24 @@ public bool CanPull(EntityUid puller, EntityUid pullableUid, PullerComponent? pu
return !startPull.Cancelled && !getPulled.Cancelled;
}
+ // Goobstation start - Grab Intent
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.Comp);
+
+ if (TryGrab((pullable, pullable.Comp), pullerUid))
+ return true;
+
+ if (!_combatMode.IsInCombatMode(pullable))
+ return TryStopPull(pullable, pullable.Comp, ignoreGrab: true);
- return TryStartPull(pullerUid, pullable, pullableComp: pullable);
+ return false;
}
+ // Goobstation end
public bool TogglePull(EntityUid pullerUid, PullerComponent puller)
{
@@ -454,7 +625,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)) // Goobstation
return false;
// Stop anyone else pulling the entity we want to pull
@@ -464,8 +635,40 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid,
if (pullableComp.Puller == pullerUid)
return false;
+ // Goobstation start - Grab Intent
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);
+ }
+ }
+ // Goobstation end
}
var pullAttempt = new PullAttemptEvent(pullerUid, pullableUid);
@@ -516,8 +719,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); // Goobstation
+ _alertsSystem.ShowAlert(pullableUid, pullableComp.PulledAlert, 0); // Goobstation
RaiseLocalEvent(pullerUid, message);
RaiseLocalEvent(pullableUid, message);
@@ -531,10 +734,14 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid,
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(pullerUid):user} started pulling {ToPrettyString(pullableUid):target}");
+
+ if (_combatMode.IsInCombatMode(pullerUid)) // Goobstation
+ TryGrab(pullableUid, pullerUid); // Goobstation
+
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;
@@ -550,7 +757,332 @@ public bool TryStopPull(EntityUid pullableUid, PullableComponent pullable, Entit
if (msg.Cancelled)
return false;
+ // Goobstation start - Grab Intent
+ if (!ignoreGrab)
+ {
+ if (_netManager.IsServer && user != null && user.Value == pullableUid)
+ {
+ var releaseAttempt = AttemptGrabRelease(pullableUid);
+ if (!releaseAttempt)
+ {
+ _popup.PopupEntity(Loc.GetString("popup-grab-release-fail-self"),
+ pullableUid,
+ pullableUid,
+ PopupType.SmallCaution);
+ return false;
+ }
+
+ _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);
+ }
+ }
+ // Goobstation end
+
StopPulling(pullableUid, pullable);
return true;
}
+
+ public void StopAllPulls(EntityUid uid) // Goobstation
+ {
+ if (TryComp(uid, out var pullable) && IsPulled(uid, pullable))
+ TryStopPull(uid, pullable);
+
+ if (TryComp(uid, out var puller) &&
+ TryComp(puller.Pulling, out PullableComponent? pullableEnt))
+ TryStopPull(puller.Pulling.Value, pullableEnt);
+ }
+
+ // Goobstation - Grab Intent
+ ///
+ /// 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;
+
+ // Prevent you from grabbing someone else while being grabbed
+ if (TryComp(puller, out var pullerAsPullable) && pullerAsPullable.Puller != null)
+ return false;
+
+ if (HasComp(puller))
+ return false;
+
+ if (pullable.Comp.Puller != puller ||
+ puller.Comp.Pulling != pullable)
+ return false;
+
+ if (puller.Comp.NextStageChange > _timing.CurTime)
+ return true;
+
+ // 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 grab intent
+ if (!ignoreCombatMode)
+ if (!_combatMode.IsInCombatMode(puller))
+ 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
+ {
+ GrabStageDirection.Increase => 1,
+ GrabStageDirection.Decrease => -1,
+ _ => throw new ArgumentOutOfRangeException(),
+ };
+
+ var newStage = puller.Comp.GrabStage + nextStageAddition;
+
+ if (HasComp(puller)
+ && TryComp(pullable, out var layingDown)
+ && layingDown.Active)
+ {
+ var ev = new CheckGrabOverridesEvent(newStage);
+ RaiseLocalEvent(puller, ev);
+ newStage = ev.Stage;
+ }
+
+ if (!TrySetGrabStages((puller, puller.Comp), (pullable, pullable.Comp), newStage))
+ return false;
+
+ _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).Coordinates)
+ .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()
+ };
+
+ var massModifier = _contests.MassContest(puller, pullable);
+ pullable.Comp.GrabEscapeChance = Math.Clamp(puller.Comp.EscapeChances[stage] / massModifier, 0f, 1f);
+
+ _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);
+
+ var comboEv = new ComboAttackPerformedEvent(puller.Owner, pullable.Owner, puller.Owner, ComboAttackType.Grab);
+ RaiseLocalEvent(puller.Owner, comboEv);
+
+ 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)
+ return true;
+ var delta = newVirtualItemsCount - virtualItemsCount;
+
+ // Adding new virtual items
+ if (delta > 0)
+ {
+ for (var i = 0; i < delta; i++)
+ {
+ var emptyHand = _handsSystem.TryGetEmptyHand(puller, out _);
+ if (!emptyHand)
+ {
+ if (_netManager.IsServer)
+ _popup.PopupEntity(Loc.GetString("popup-grab-need-hand"), puller, puller, PopupType.Medium);
+
+ return false;
+ }
+
+ 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)
+ return true;
+ 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);
+ if(TryComp(item, out var virtualItemComponent))
+ _virtualSystem.DeleteVirtualItem((item,virtualItemComponent), puller);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Attempts to release entity from grab
+ ///
+ /// Grabbed entity
+ ///
+ private 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(3));
+ 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);
+ Dirty(puller);
+
+ if (!ignoreCombatMode && _combatMode.IsInCombatMode(puller.Owner))
+ {
+ 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 GrabStageDirection
+{
+ Increase,
+ Decrease,
+}
+
+// Goobstation
diff --git a/Content.Shared/Security/Systems/DeployableBarrierSystem.cs b/Content.Shared/Security/Systems/DeployableBarrierSystem.cs
index 622edc4b62e..a4d8f4ad588 100644
--- a/Content.Shared/Security/Systems/DeployableBarrierSystem.cs
+++ b/Content.Shared/Security/Systems/DeployableBarrierSystem.cs
@@ -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); // Goobstation
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 3407fe1f5d6..fbf737d51d7 100644
--- a/Content.Shared/Teleportation/Systems/SharedPortalSystem.cs
+++ b/Content.Shared/Teleportation/Systems/SharedPortalSystem.cs
@@ -63,11 +63,11 @@ private void OnGetVerbs(EntityUid uid, PortalComponent component, GetVerbsEvent<
return;
var ent = link.LinkedEntities.First();
-
+
// Validate the entity exists and has a transform before attempting teleport
if (!Exists(ent) || !TryComp(ent, out TransformComponent? entXform))
return;
-
+
TeleportEntity(uid, args.User, entXform.Coordinates, ent, false);
},
Disabled = disabled,
@@ -106,13 +106,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); // Goobstation
}
if (TryComp(subject, out var pullerComp)
&& TryComp(pullerComp.Pulling, out var subjectPulling))
{
- _pulling.TryStopPull(pullerComp.Pulling.Value, subjectPulling);
+ _pulling.TryStopPull(pullerComp.Pulling.Value, subjectPulling, ignoreGrab: true); // Goobstation
}
// if they came from another portal, just return and wait for them to exit the portal
diff --git a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
index 15390f5ea78..75d96bc630f 100644
--- a/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
+++ b/Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
+using Content.Shared._Goobstation.MartialArts.Events; // Goobstation - Martial Arts
using Content.Shared.ActionBlocker;
using Content.Shared.Actions.Events;
using Content.Shared.Administration.Components;
@@ -566,6 +567,8 @@ protected virtual void DoLightAttack(EntityUid user, LightAttackEvent ev, Entity
var modifiedDamage = DamageSpecifier.ApplyModifierSets(damage + hitEvent.BonusDamage + attackedEvent.BonusDamage, hitEvent.ModifiersList);
var damageResult = Damageable.TryChangeDamage(target, modifiedDamage, ignoreResistances: resistanceBypass, origin: user, partMultiplier: component.ClickPartDamageMultiplier); // Shitmed Change
+ var comboEv = new ComboAttackPerformedEvent(user, target.Value, meleeUid, ComboAttackType.Harm);
+ RaiseLocalEvent(user, comboEv);
if (damageResult is {Empty: false})
{
@@ -738,6 +741,9 @@ private bool DoHeavyAttack(EntityUid user, HeavyAttackEvent ev, EntityUid meleeU
var damageResult = Damageable.TryChangeDamage(entity, modifiedDamage, ignoreResistances: resistanceBypass, origin: user, partMultiplier: component.HeavyPartDamageMultiplier); // Shitmed Change
+ var comboEv = new ComboAttackPerformedEvent(user, entity, meleeUid, ComboAttackType.HarmLight);
+ RaiseLocalEvent(user, comboEv);
+
if (damageResult != null && damageResult.GetTotal() > FixedPoint2.Zero)
{
// If the target has stamina and is taking blunt damage, they should also take stamina damage based on their blunt to stamina factor
@@ -885,7 +891,6 @@ private bool DoDisarm(EntityUid user, DisarmAttackEvent ev, EntityUid meleeUid,
return false;
}
-
if (MobState.IsIncapacitated(target.Value))
{
return false;
diff --git a/Content.Shared/_EinsteinEngines/Contests/ContestsSystem.cs b/Content.Shared/_EinsteinEngines/Contests/ContestsSystem.cs
new file mode 100644
index 00000000000..cb3c134f693
--- /dev/null
+++ b/Content.Shared/_EinsteinEngines/Contests/ContestsSystem.cs
@@ -0,0 +1,116 @@
+using Content.Shared.CCVar; // Goob Edit
+using Robust.Shared.Configuration;
+using Robust.Shared.Physics.Components;
+
+namespace Content.Shared._EinsteinEngines.Contests // Goob Edit
+{
+ public sealed partial class ContestsSystem : EntitySystem
+ {
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
+ ///
+ /// The presumed average mass of a player entity
+ /// Defaulted to the average mass of an adult human
+ ///
+ private const float AverageMass = 71f;
+
+ #region Mass Contests
+ ///
+ /// Outputs the ratio of mass between a performer and the average human mass
+ ///
+ /// Uid of Performer
+ public float MassContest(EntityUid performerUid, float otherMass = AverageMass)
+ {
+ if (_cfg.GetCVar(CCVars.DoMassContests) // Goob edit
+ && TryComp(performerUid, out var performerPhysics)
+ && performerPhysics.Mass != 0)
+ return Math.Clamp(performerPhysics.Mass / otherMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage));// Goob edit
+
+ return 1f;
+ }
+
+ ///
+ ///
+ /// MaybeMassContest, in case your entity doesn't exist
+ ///
+ public float MassContest(EntityUid? performerUid, float otherMass = AverageMass)
+ {
+ if (_cfg.GetCVar(CCVars.DoMassContests)) // Goob edit
+ {
+ var ratio = performerUid is { } uid ? MassContest(uid, otherMass) : 1f;
+ return ratio;
+ }
+
+ return 1f;
+ }
+
+ ///
+ /// Outputs the ratio of mass between a performer and the average human mass
+ /// If a function already has the performer's physics component, this is faster
+ ///
+ ///
+ public float MassContest(PhysicsComponent performerPhysics, float otherMass = AverageMass)
+ {
+ if (_cfg.GetCVar(CCVars.DoMassContests) // Goob edit
+ && performerPhysics.Mass != 0)
+ return Math.Clamp(performerPhysics.Mass / otherMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage));
+
+ return 1f;
+ }
+
+ ///
+ /// Outputs the ratio of mass between a performer and a target, accepts either EntityUids or PhysicsComponents in any combination
+ /// If you have physics components already in your function, use instead
+ ///
+ ///
+ ///
+ public float MassContest(EntityUid performerUid, EntityUid targetUid)
+ {
+ if (_cfg.GetCVar(CCVars.DoMassContests) // Goob edit
+ && TryComp(performerUid, out var performerPhysics)
+ && TryComp(targetUid, out var targetPhysics)
+ && performerPhysics.Mass != 0
+ && targetPhysics.InvMass != 0)
+ return Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage)); // Goob edit
+
+ return 1f; // Goob edit
+ }
+
+ ///
+ public float MassContest(EntityUid performerUid, PhysicsComponent targetPhysics)
+ {
+ if (_cfg.GetCVar(CCVars.DoMassContests) // Goob edit
+ && TryComp(performerUid, out var performerPhysics)
+ && performerPhysics.Mass != 0
+ && targetPhysics.InvMass != 0)
+ return Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage));
+
+ return 1f;
+ }
+
+ ///
+ public float MassContest(PhysicsComponent performerPhysics, EntityUid targetUid)
+ {
+ if (_cfg.GetCVar(CCVars.DoMassContests) // Goob edit
+ && TryComp(targetUid, out var targetPhysics)
+ && performerPhysics.Mass != 0
+ && targetPhysics.InvMass != 0)
+ return Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage)); // Goob edit
+
+ return 1f;
+ }
+
+ ///
+ public float MassContest(PhysicsComponent performerPhysics, PhysicsComponent targetPhysics)
+ {
+ if (_cfg.GetCVar(CCVars.DoMassContests) // Goob edit
+ && performerPhysics.Mass != 0
+ && targetPhysics.InvMass != 0)
+ return Math.Clamp(performerPhysics.Mass * targetPhysics.InvMass, 1 - _cfg.GetCVar(CCVars.MassContestsMaxPercentage), 1 + _cfg.GetCVar(CCVars.MassContestsMaxPercentage)); // Goob edit
+
+ return 1f;
+ }
+
+ #endregion
+ }
+}
diff --git a/Content.Shared/_Goobstation/CCVar/CCVars.Goob.cs b/Content.Shared/_Goobstation/CCVar/CCVars.Goob.cs
new file mode 100644
index 00000000000..38e58982333
--- /dev/null
+++ b/Content.Shared/_Goobstation/CCVar/CCVars.Goob.cs
@@ -0,0 +1,246 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared._Goobstation.CCVar;
+
+[CVarDefs]
+public sealed partial class GoobCVars
+{
+ ///
+ /// Whether pipes will unanchor on ANY conflicting connection. May break maps.
+ /// If false, allows you to stack pipes as long as new directions are added (i.e. in a new pipe rotation, layer or multi-Z link), otherwise unanchoring them.
+ ///
+ public static readonly CVarDef StrictPipeStacking =
+ CVarDef.Create("atmos.strict_pipe_stacking", false, CVar.SERVERONLY);
+
+ ///
+ /// If an object's mass is below this number, then this number is used in place of mass to determine whether air pressure can throw an object.
+ /// This has nothing to do with throwing force, only acting as a way of reducing the odds of tiny 5 gram objects from being yeeted by people's breath
+ ///
+ ///
+ /// If you are reading this because you want to change it, consider looking into why almost every item in the game weighs only 5 grams
+ /// And maybe do your part to fix that? :)
+ ///
+ public static readonly CVarDef SpaceWindMinimumCalculatedMass =
+ CVarDef.Create("atmos.space_wind_minimum_calculated_mass", 10f, CVar.SERVERONLY);
+
+ ///
+ /// Calculated as 1/Mass, where Mass is the physics.Mass of the desired threshold.
+ /// If an object's inverse mass is lower than this, it is capped at this. Basically, an upper limit to how heavy an object can be before it stops resisting space wind more.
+ ///
+ public static readonly CVarDef SpaceWindMaximumCalculatedInverseMass =
+ CVarDef.Create("atmos.space_wind_maximum_calculated_inverse_mass", 0.04f, CVar.SERVERONLY);
+
+ ///
+ /// Increases default airflow calculations to O(n^2) complexity, for use with heavy space wind optimizations. Potato servers BEWARE
+ /// This solves the problem of objects being trapped in an infinite loop of slamming into a wall repeatedly.
+ ///
+ public static readonly CVarDef MonstermosUseExpensiveAirflow =
+ CVarDef.Create("atmos.mmos_expensive_airflow", true, CVar.SERVERONLY);
+
+ ///
+ /// A multiplier on the amount of force applied to Humanoid entities, as tracked by HumanoidAppearanceComponent
+ /// This multiplier is added after all other checks are made, and applies to both throwing force, and how easy it is for an entity to be thrown.
+ ///
+ public static readonly CVarDef AtmosHumanoidThrowMultiplier =
+ CVarDef.Create("atmos.humanoid_throw_multiplier", 2f, CVar.SERVERONLY);
+
+ ///
+ /// Taken as the cube of a tile's mass, this acts as a minimum threshold of mass for which air pressure calculates whether or not to rip a tile from the floor
+ /// This should be set by default to the cube of the game's lowest mass tile as defined in their prototypes, but can be increased for server performance reasons
+ ///
+ public static readonly CVarDef MonstermosRipTilesMinimumPressure =
+ CVarDef.Create("atmos.monstermos_rip_tiles_min_pressure", 7500f, CVar.SERVERONLY);
+
+ ///
+ /// Taken after the minimum pressure is checked, the effective pressure is multiplied by this amount.
+ /// This allows server hosts to finely tune how likely floor tiles are to be ripped apart by air pressure
+ ///
+ public static readonly CVarDef MonstermosRipTilesPressureOffset =
+ CVarDef.Create("atmos.monstermos_rip_tiles_pressure_offset", 0.44f, CVar.SERVERONLY);
+
+ ///
+ /// Indicates how much players are required for the round to be considered lowpop.
+ /// Used for dynamic gamemode.
+ ///
+ public static readonly CVarDef LowpopThreshold =
+ CVarDef.Create("game.players.lowpop_threshold", 15f, CVar.SERVERONLY);
+
+ ///
+ /// Indicates how much players are required for the round to be considered highpop.
+ /// Used for dynamic gamemode.
+ ///
+ public static readonly CVarDef HighpopThreshold =
+ CVarDef.Create("game.players.highpop_threshold", 50f, CVar.SERVERONLY);
+
+ ///
+ /// Is ore silo enabled.
+ ///
+ public static readonly CVarDef SiloEnabled =
+ CVarDef.Create("goob.silo_enabled", true, CVar.SERVER | CVar.REPLICATED);
+
+ #region Player Listener
+
+ ///
+ /// Enable Dorm Notifier
+ ///
+ public static readonly CVarDef DormNotifier =
+ CVarDef.Create("dorm_notifier.enable", true, CVar.SERVER);
+
+ ///
+ /// Check for dorm activity every X amount of ticks
+ /// Default is 10.
+ ///
+ public static readonly CVarDef DormNotifierFrequency =
+ CVarDef.Create("dorm_notifier.frequency", 10, CVar.SERVER);
+
+ ///
+ /// Time given to be found to be engaging in dorm activity
+ /// Default is 120.
+ ///
+ public static readonly CVarDef DormNotifierPresenceTimeout =
+ CVarDef.Create("dorm_notifier.timeout", 120, CVar.SERVER, "Mark as condemned if present near a dorm marker for more than X amount of seconds.");
+
+ ///
+ /// Time given to be found engaging in dorm activity if any of the sinners are nude
+ /// Default if 25.
+ ///
+ public static readonly CVarDef DormNotifierPresenceTimeoutNude =
+ CVarDef.Create("dorm_notifier.timeout_nude", 25, CVar.SERVER, "Mark as condemned if present near a dorm marker for more than X amount of seconds while being nude.");
+
+ ///
+ /// Broadcast to all players that a player has ragequit.
+ ///
+ public static readonly CVarDef PlayerRageQuitNotify =
+ CVarDef.Create("ragequit.notify", true, CVar.SERVERONLY);
+
+ ///
+ /// Time between being eligible for a "rage quit" after reaching a damage threshold.
+ /// Default is 5f.
+ ///
+ public static readonly CVarDef PlayerRageQuitTimeThreshold =
+ CVarDef.Create("ragequit.threshold", 30f, CVar.SERVERONLY);
+
+ ///
+ /// Log ragequits to a discord webhook, set to empty to disable.
+ ///
+ public static readonly CVarDef PlayerRageQuitDiscordWebhook =
+ CVarDef.Create("ragequit.discord_webhook", "", CVar.SERVERONLY | CVar.CONFIDENTIAL);
+
+ #endregion PlayerListener
+
+ #region Discord AHelp Reply System
+
+ ///
+ /// If an admin replies to users from discord, should it use their discord role color? (if applicable)
+ /// Overrides DiscordReplyColor and AdminBwoinkColor.
+ ///
+ public static readonly CVarDef UseDiscordRoleColor =
+ CVarDef.Create("admin.use_discord_role_color", true, CVar.SERVERONLY);
+
+ ///
+ /// If an admin replies to users from discord, should it use their discord role name? (if applicable)
+ ///
+ public static readonly CVarDef UseDiscordRoleName =
+ CVarDef.Create("admin.use_discord_role_name", true, CVar.SERVERONLY);
+
+ ///
+ /// The text before an admin's name when replying from discord to indicate they're speaking from discord.
+ ///
+ public static readonly CVarDef DiscordReplyPrefix =
+ CVarDef.Create("admin.discord_reply_prefix", "(DISCORD) ", CVar.SERVERONLY);
+
+ ///
+ /// The color of the names of admins. This is the fallback color for admins.
+ ///
+ public static readonly CVarDef AdminBwoinkColor =
+ CVarDef.Create("admin.admin_bwoink_color", "red", CVar.SERVERONLY);
+
+ ///
+ /// The color of the names of admins who reply from discord. Leave empty to disable.
+ /// Overrides AdminBwoinkColor.
+ ///
+ public static readonly CVarDef DiscordReplyColor =
+ CVarDef.Create("admin.discord_reply_color", string.Empty, CVar.SERVERONLY);
+
+ ///
+ /// Use the admin's Admin OOC color in bwoinks.
+ /// If either the ooc color or this is not set, uses the admin.admin_bwoink_color value.
+ ///
+ public static readonly CVarDef UseAdminOOCColorInBwoinks =
+ CVarDef.Create("admin.bwoink_use_admin_ooc_color", true, CVar.SERVERONLY);
+
+ #endregion
+
+ ///
+ /// Should the player automatically get up after being knocked down
+ ///
+ public static readonly CVarDef AutoGetUp =
+ CVarDef.Create("white.auto_get_up", true, CVar.CLIENT | CVar.ARCHIVE | CVar.REPLICATED); // WD EDIT
+
+ #region Blob
+ public static readonly CVarDef BlobMax =
+ CVarDef.Create("blob.max", 3, CVar.SERVERONLY);
+
+ public static readonly CVarDef BlobPlayersPer =
+ CVarDef.Create("blob.players_per", 20, CVar.SERVERONLY);
+
+ public static readonly CVarDef BlobCanGrowInSpace =
+ CVarDef.Create("blob.grow_space", true, CVar.SERVER);
+
+ #endregion
+
+ #region RMC
+
+ public static readonly CVarDef RMCPatronLobbyMessageTimeSeconds =
+ CVarDef.Create("rmc.patron_lobby_message_time_seconds", 30, CVar.REPLICATED | CVar.SERVER);
+
+ public static readonly CVarDef RMCPatronLobbyMessageInitialDelaySeconds =
+ CVarDef.Create("rmc.patron_lobby_message_initial_delay_seconds", 5, CVar.REPLICATED | CVar.SERVER);
+
+ public static readonly CVarDef RMCDiscordAccountLinkingMessageLink =
+ CVarDef.Create("rmc.discord_account_linking_message_link", "", CVar.REPLICATED | CVar.SERVER);
+
+ #endregion
+
+ #region Goobcoins
+
+ public static readonly CVarDef GoobcoinsPerPlayer =
+ CVarDef.Create("goob.coins_per_player", 10, CVar.SERVERONLY);
+
+ public static readonly CVarDef GoobcoinsPerGreentext =
+ CVarDef.Create("goob.coins_per_greentext", 5, CVar.SERVERONLY);
+
+ public static readonly CVarDef GoobcoinNonAntagMultiplier =
+ CVarDef.Create("goob.coins_non_antag_multiplier", 3, CVar.SERVERONLY);
+
+ public static readonly CVarDef GoobcoinServerMultiplier =
+ CVarDef.Create("goob.coins_server_multiplier", 1, CVar.SERVERONLY);
+
+ public static readonly CVarDef GoobcoinMinPlayers =
+ CVarDef.Create("goob.coins_min_players", 5, CVar.SERVERONLY);
+
+ #endregion
+
+ #region Chat highlights
+
+ ///
+ /// A string containing a list of newline-separated words to be highlighted in the chat.
+ ///
+ public static readonly CVarDef ChatHighlights =
+ CVarDef.Create("chat.highlights", "", CVar.CLIENTONLY | CVar.ARCHIVE, "A list of newline-separated words to be highlighted in the chat.");
+
+ ///
+ /// An option to toggle the automatic filling of the highlights with the character's info, if available.
+ ///
+ public static readonly CVarDef ChatAutoFillHighlights =
+ CVarDef.Create("chat.auto_fill_highlights", false, CVar.CLIENTONLY | CVar.ARCHIVE, "Toggles automatically filling the highlights with the character's information.");
+
+ ///
+ /// The color in which the highlights will be displayed.
+ ///
+ public static readonly CVarDef ChatHighlightsColor =
+ CVarDef.Create("chat.highlights_color", "#17FFC1FF", CVar.CLIENTONLY | CVar.ARCHIVE, "The color in which the highlights will be displayed.");
+
+ #endregion
+
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/ComboPrototype.cs b/Content.Shared/_Goobstation/MartialArts/ComboPrototype.cs
new file mode 100644
index 00000000000..9f1b4e6b8af
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/ComboPrototype.cs
@@ -0,0 +1,70 @@
+using Content.Shared._Goobstation.MartialArts.Components;
+using Content.Shared._Goobstation.MartialArts.Events;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Goobstation.MartialArts;
+
+[Prototype("combo")]
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class ComboPrototype : IPrototype
+{
+ [IdDataField] public string ID { get; private set; } = default!;
+
+ [DataField(required: true)]
+ public MartialArtsForms MartialArtsForm;
+
+ [DataField("attacks", required: true)]
+ public List AttackTypes = new();
+
+ //[DataField("weapon")] // Will be done later
+ //public string? WeaponProtoId;
+ [DataField("event", required: true)]
+ public object? ResultEvent;
+
+ ///
+ /// How much extra damage should this move do on perform?
+ ///
+ [DataField]
+ public int ExtraDamage;
+
+ ///
+ /// Stun time in seconds
+ ///
+ [DataField]
+ public int ParalyzeTime;
+
+ ///
+ /// How much stamina damage should this move do on perform.
+ ///
+ [DataField]
+ public float StaminaDamage;
+
+ ///
+ /// Blunt, Slash, etc.
+ ///
+ [DataField]
+ public string DamageType = "Blunt";
+
+ ///
+ /// How fast people are thrown on combo
+ ///
+ [DataField]
+ public float ThrownSpeed = 7f;
+
+ ///
+ /// Name of the move
+ ///
+ [DataField(required: true)]
+ public string Name = string.Empty;
+
+}
+
+[Prototype("comboList")]
+public sealed partial class ComboListPrototype : IPrototype
+{
+ [IdDataField] public string ID { get; private init; } = default!;
+
+ [DataField( required: true)]
+ public List> Combos = new();
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/Components/CanPerformComboComponent.cs b/Content.Shared/_Goobstation/MartialArts/Components/CanPerformComboComponent.cs
new file mode 100644
index 00000000000..0d9e74e896b
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Components/CanPerformComboComponent.cs
@@ -0,0 +1,30 @@
+using Content.Shared._Goobstation.MartialArts.Events;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._Goobstation.MartialArts.Components;
+[RegisterComponent]
+[NetworkedComponent]
+public sealed partial class CanPerformComboComponent : Component
+{
+ [DataField]
+ public EntityUid? CurrentTarget;
+
+ [DataField]
+ public ProtoId BeingPerformed;
+
+ [DataField]
+ public List LastAttacks = new();
+
+ [DataField]
+ public List AllowedCombos = new();
+
+ [DataField]
+ public List> RoundstartCombos = new();
+
+ [DataField]
+ public TimeSpan ResetTime = TimeSpan.Zero;
+
+ [DataField]
+ public int ConsecutiveGnashes = 0;
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/Components/GrabStagesOverrideComponent.cs b/Content.Shared/_Goobstation/MartialArts/Components/GrabStagesOverrideComponent.cs
new file mode 100644
index 00000000000..5874c3a6113
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Components/GrabStagesOverrideComponent.cs
@@ -0,0 +1,12 @@
+using Content.Shared.Movement.Pulling.Systems;
+
+namespace Content.Shared._Goobstation.MartialArts.Components;
+
+///
+/// Base component for martial arts that override the normal grab stages.
+/// Allows martial arts to start at more advanced grab stages like Hard grabs.
+///
+public abstract partial class GrabStagesOverrideComponent : Component
+{
+ public GrabStage StartingStage = GrabStage.Hard;
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/Components/GrantMartialArtKnowledgeComponent.cs b/Content.Shared/_Goobstation/MartialArts/Components/GrantMartialArtKnowledgeComponent.cs
new file mode 100644
index 00000000000..b47ac30c3d8
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Components/GrantMartialArtKnowledgeComponent.cs
@@ -0,0 +1,49 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._Goobstation.MartialArts.Components;
+
+public abstract partial class GrantMartialArtKnowledgeComponent : Component
+{
+ [DataField]
+ public bool Used;
+
+ [DataField]
+ public virtual MartialArtsForms MartialArtsForm { get; set; } = MartialArtsForms.CloseQuartersCombat;
+}
+
+[RegisterComponent]
+public sealed partial class GrantCqcComponent : GrantMartialArtKnowledgeComponent
+{
+ [DataField]
+ public bool IsBlocked;
+}
+
+[RegisterComponent]
+public sealed partial class GrantCorporateJudoComponent : GrantMartialArtKnowledgeComponent
+{
+ [DataField]
+ public override MartialArtsForms MartialArtsForm { get; set; } = MartialArtsForms.CorporateJudo;
+}
+
+[RegisterComponent]
+public sealed partial class GrantSleepingCarpComponent : GrantMartialArtKnowledgeComponent
+{
+ [DataField]
+ public override MartialArtsForms MartialArtsForm { get; set; } = MartialArtsForms.SleepingCarp;
+}
+
+[RegisterComponent]
+public sealed partial class SleepingCarpStudentComponent : Component
+{
+ [DataField]
+ public int Stage = 1;
+
+ [ViewVariables(VVAccess.ReadOnly)]
+ public TimeSpan UseAgainTime = TimeSpan.Zero;
+
+ [DataField]
+ public int MaxUseDelay = 90;
+
+ [DataField]
+ public int MinUseDelay = 30;
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/Components/KravMagaComponents.cs b/Content.Shared/_Goobstation/MartialArts/Components/KravMagaComponents.cs
new file mode 100644
index 00000000000..a4e14654759
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Components/KravMagaComponents.cs
@@ -0,0 +1,78 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._Goobstation.MartialArts.Components;
+
+///
+/// This is used for...
+///
+[RegisterComponent]
+public sealed partial class KravMagaActionComponent : Component
+{
+ [DataField]
+ public KravMagaMoves Configuration;
+
+ [DataField]
+ public string Name;
+
+ [DataField]
+ public float StaminaDamage;
+
+ [DataField]
+ public int EffectTime;
+}
+
+[RegisterComponent]
+public sealed partial class KravMagaComponent : GrabStagesOverrideComponent
+{
+ [DataField]
+ public KravMagaMoves? SelectedMove;
+
+ [DataField]
+ public KravMagaActionComponent? SelectedMoveComp;
+
+ public readonly List BaseKravMagaMoves = new()
+ {
+ "ActionLegSweep",
+ "ActionNeckChop",
+ "ActionLungPunch",
+ };
+
+ public readonly List KravMagaMoveEntities = new()
+ {
+ };
+
+ [DataField]
+ public int BaseDamage = 5;
+
+ [DataField]
+ public int DownedDamageModifier = 2;
+}
+///
+/// Tracks when an entity is silenced through Krav Maga techniques.
+/// Prevents the affected entity from using voice-activated abilities or speaking.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class KravMagaSilencedComponent : Component
+{
+ [DataField]
+ public TimeSpan SilencedTime = TimeSpan.Zero;
+}
+
+///
+/// Tracks when an entity's breathing is blocked through Krav Maga techniques.
+/// May cause suffocation damage over time when integrated with respiration systems.
+///
+[RegisterComponent, NetworkedComponent]
+public sealed partial class KravMagaBlockedBreathingComponent : Component
+{
+ [DataField]
+ public TimeSpan BlockedTime = TimeSpan.Zero;
+}
+
+public enum KravMagaMoves
+{
+ LegSweep,
+ NeckChop,
+ LungPunch,
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/Components/MartialArtsComponents.cs b/Content.Shared/_Goobstation/MartialArts/Components/MartialArtsComponents.cs
new file mode 100644
index 00000000000..9aa403a83d5
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Components/MartialArtsComponents.cs
@@ -0,0 +1,39 @@
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Goobstation.MartialArts.Components;
+
+[RegisterComponent]
+public sealed partial class MartialArtBlockedComponent : Component
+{
+ [DataField]
+ public MartialArtsForms Form;
+}
+
+[RegisterComponent]
+[NetworkedComponent]
+[AutoGenerateComponentState]
+public sealed partial class MartialArtsKnowledgeComponent : GrabStagesOverrideComponent
+{
+ [DataField]
+ [AutoNetworkedField]
+ public MartialArtsForms MartialArtsForm = MartialArtsForms.CloseQuartersCombat;
+
+ [DataField]
+ [AutoNetworkedField]
+ public bool Blocked;
+
+ [DataField]
+ [AutoNetworkedField]
+ public DamageSpecifier OriginalFistDamage;
+}
+
+public enum MartialArtsForms
+{
+ CorporateJudo,
+ CloseQuartersCombat,
+ SleepingCarp,
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/Events/CQCEvents.cs b/Content.Shared/_Goobstation/MartialArts/Events/CQCEvents.cs
new file mode 100644
index 00000000000..d4af6bbc182
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Events/CQCEvents.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Goobstation.MartialArts.Events;
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class CqcSlamPerformedEvent : EntityEventArgs;
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class CqcKickPerformedEvent : EntityEventArgs;
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class CqcRestrainPerformedEvent : EntityEventArgs;
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class CqcPressurePerformedEvent : EntityEventArgs;
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class CqcConsecutivePerformedEvent : EntityEventArgs;
diff --git a/Content.Shared/_Goobstation/MartialArts/Events/ComboAttackPerformedEvent.cs b/Content.Shared/_Goobstation/MartialArts/Events/ComboAttackPerformedEvent.cs
new file mode 100644
index 00000000000..435180c5dc1
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Events/ComboAttackPerformedEvent.cs
@@ -0,0 +1,30 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Goobstation.MartialArts.Events;
+
+///
+/// Raised when a martial arts combo attack is performed. Contains information about
+/// the performer, target, weapon used, and the type of combo attack.
+///
+public sealed class ComboAttackPerformedEvent(
+ EntityUid performer,
+ EntityUid target,
+ EntityUid weapon,
+ ComboAttackType type)
+ : CancellableEntityEventArgs
+{
+ public EntityUid Performer { get; } = performer;
+ public EntityUid Target { get; } = target;
+ public EntityUid Weapon { get; } = weapon;
+ public ComboAttackType Type { get; } = type;
+}
+
+[Serializable,NetSerializable]
+public enum ComboAttackType : byte
+{
+ Harm,
+ HarmLight,
+ Disarm,
+ Grab,
+ Hug,
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/Events/ComboBeingPerformedEvent.cs b/Content.Shared/_Goobstation/MartialArts/Events/ComboBeingPerformedEvent.cs
new file mode 100644
index 00000000000..10f1033d053
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Events/ComboBeingPerformedEvent.cs
@@ -0,0 +1,10 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Goobstation.MartialArts.Events;
+
+[Serializable,NetSerializable]
+public sealed class ComboBeingPerformedEvent(ProtoId protoId) : EntityEventArgs
+{
+ public ProtoId ProtoId = protoId;
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/Events/JudoEvents.cs b/Content.Shared/_Goobstation/MartialArts/Events/JudoEvents.cs
new file mode 100644
index 00000000000..e41ad528350
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Events/JudoEvents.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Goobstation.MartialArts.Events;
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class JudoThrowPerformedEvent : EntityEventArgs;
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class JudoEyePokePerformedEvent : EntityEventArgs;
+
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class JudoArmbarPerformedEvent : EntityEventArgs;
+
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class JudoGoldenBlastPerformedEvent : EntityEventArgs;
diff --git a/Content.Shared/_Goobstation/MartialArts/Events/KravMagaActionEvent.cs b/Content.Shared/_Goobstation/MartialArts/Events/KravMagaActionEvent.cs
new file mode 100644
index 00000000000..4c3c20a0949
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Events/KravMagaActionEvent.cs
@@ -0,0 +1,10 @@
+using Content.Shared.Actions;
+
+namespace Content.Shared._Goobstation.MartialArts.Events;
+
+///
+/// This handles selecting your krav maga action
+///
+public sealed partial class KravMagaActionEvent : InstantActionEvent
+{
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/Events/SleepingCarpEvents.cs b/Content.Shared/_Goobstation/MartialArts/Events/SleepingCarpEvents.cs
new file mode 100644
index 00000000000..3b89c210aa5
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/Events/SleepingCarpEvents.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Goobstation.MartialArts.Events;
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class SleepingCarpGnashingTeethPerformedEvent : EntityEventArgs;
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class SleepingCarpKneeHaulPerformedEvent : EntityEventArgs;
+
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class SleepingCarpCrashingWavesPerformedEvent : EntityEventArgs;
+
+[Serializable,NetSerializable]
+public sealed class SleepingCarpSaying(LocId saying) : EntityEventArgs
+{
+ public LocId Saying = saying;
+};
diff --git a/Content.Shared/_Goobstation/MartialArts/MartialArtPrototype.cs b/Content.Shared/_Goobstation/MartialArts/MartialArtPrototype.cs
new file mode 100644
index 00000000000..2500452d6ec
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/MartialArtPrototype.cs
@@ -0,0 +1,36 @@
+using Content.Shared._Goobstation.MartialArts.Components;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._Goobstation.MartialArts;
+
+[Prototype("martialArt")]
+public sealed class MartialArtPrototype : IPrototype
+{
+ [IdDataField]
+ public string ID { get; private init; } = default!;
+
+ [DataField]
+ public MartialArtsForms MartialArtsForm = MartialArtsForms.CloseQuartersCombat;
+
+ [DataField]
+ public int MinRandomDamageModifier;
+
+ [DataField]
+ public int MaxRandomDamageModifier = 5;
+
+ [DataField]
+ public FixedPoint2 BaseDamageModifier;
+
+ [DataField]
+ public bool RandomDamageModifier;
+
+ [DataField]
+ public ProtoId RoundstartCombos = "CQCMoves";
+
+ [DataField]
+ public List RandomSayings = [];
+
+ [DataField]
+ public List RandomSayingsDowned = [];
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.CQC.cs b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.CQC.cs
new file mode 100644
index 00000000000..e13bf7bc571
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.CQC.cs
@@ -0,0 +1,191 @@
+using Content.Shared._Goobstation.MartialArts.Components;
+using Content.Shared._Goobstation.MartialArts.Events;
+using Content.Shared._Shitmed.Targeting;
+using Content.Shared.Bed.Sleep;
+using Content.Shared.Damage.Components;
+using Content.Shared.Examine;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Movement.Pulling.Components;
+using Robust.Shared.Audio;
+
+namespace Content.Shared._Goobstation.MartialArts;
+
+public partial class SharedMartialArtsSystem
+{
+ private void InitializeCqc()
+ {
+ SubscribeLocalEvent(OnCQCSlam);
+ SubscribeLocalEvent(OnCQCKick);
+ SubscribeLocalEvent(OnCQCRestrain);
+ SubscribeLocalEvent(OnCQCPressure);
+ SubscribeLocalEvent(OnCQCConsecutive);
+ SubscribeLocalEvent(OnCQCAttackPerformed);
+
+ SubscribeLocalEvent(OnGrantCQCUse);
+ SubscribeLocalEvent(OnMapInitEvent);
+ SubscribeLocalEvent(OnGrantCQCExamine);
+ }
+
+
+ #region Generic Methods
+
+ private void OnMapInitEvent(Entity ent, ref MapInitEvent args)
+ {
+ if (!HasComp(ent))
+ return;
+
+ if (!TryGrantMartialArt(ent, ent.Comp))
+ return;
+
+ if (TryComp(ent, out var knowledge))
+ knowledge.Blocked = true;
+ }
+
+ private void OnGrantCQCUse(Entity ent, ref UseInHandEvent args)
+ {
+ if (!_netManager.IsServer)
+ return;
+
+ if (ent.Comp.Used)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("cqc-fail-used", ("manual", Identity.Entity(ent, EntityManager))),
+ args.User,
+ args.User);
+ return;
+ }
+
+ if (!TryGrantMartialArt(args.User, ent.Comp))
+ return;
+ _popupSystem.PopupEntity(Loc.GetString("cqc-success-learned"), args.User, args.User);
+ ent.Comp.Used = true;
+ }
+
+ private void OnGrantCQCExamine(Entity ent, ref ExaminedEvent args)
+ {
+ if (ent.Comp.Used)
+ args.PushMarkup(Loc.GetString("cqc-manual-used", ("manual", Identity.Entity(ent, EntityManager))));
+ }
+
+ private void OnCQCAttackPerformed(Entity ent, ref ComboAttackPerformedEvent args)
+ {
+ if (!TryComp(ent, out var knowledgeComponent))
+ return;
+
+ if (knowledgeComponent.MartialArtsForm != MartialArtsForms.CloseQuartersCombat)
+ return;
+
+ if(knowledgeComponent.Blocked)
+ return;
+
+ switch (args.Type)
+ {
+ case ComboAttackType.Disarm:
+ _stamina.TakeStaminaDamage(args.Target, 25f);
+ break;
+ case ComboAttackType.Harm:
+ if (!TryComp(ent, out var standing)
+ || !standing.Active)
+ return;
+ _stun.TryKnockdown(args.Target, TimeSpan.FromSeconds(5), true);
+ _standingState.Stand(ent);
+ break;
+ }
+
+
+ }
+
+ #endregion
+
+ #region Combo Methods
+
+ private void OnCQCSlam(Entity ent, ref CqcSlamPerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out var downed)
+ || downed)
+ return;
+
+ DoDamage(ent, target, proto.DamageType, proto.ExtraDamage, out _);
+ _stun.TryKnockdown(target, TimeSpan.FromSeconds(proto.ParalyzeTime), true);
+ if (TryComp(target, out var pullable))
+ _pulling.TryStopPull(target, pullable, ent, true);
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit3.ogg"), target);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ private void OnCQCKick(Entity ent, ref CqcKickPerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out var downed))
+ return;
+
+ var mapPos = _transform.GetMapCoordinates(ent).Position;
+ var hitPos = _transform.GetMapCoordinates(target).Position;
+ var dir = hitPos - mapPos;
+ dir *= 1f / dir.Length();
+
+ if (downed)
+ {
+ if (TryComp(target, out var stamina) && stamina.Critical)
+ _status.TryAddStatusEffect(target, "ForcedSleep", TimeSpan.FromSeconds(10), true);
+ DoDamage(ent, target, proto.DamageType, proto.ExtraDamage, out _, TargetBodyPart.Head);
+ _stamina.TakeStaminaDamage(target, proto.StaminaDamage * 2 + 5, source: ent);
+ }
+ else
+ {
+ _stamina.TakeStaminaDamage(target, proto.StaminaDamage, source: ent);
+ }
+
+ if (TryComp(target, out var pullable))
+ _pulling.TryStopPull(target, pullable, ent, true);
+ _grabThrowing.Throw(target, ent, dir, proto.ThrownSpeed);
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit2.ogg"), target);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ private void OnCQCRestrain(Entity ent, ref CqcRestrainPerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out _))
+ return;
+
+ _stun.TryKnockdown(target, TimeSpan.FromSeconds(proto.ParalyzeTime), true);
+ _stamina.TakeStaminaDamage(target, proto.StaminaDamage, source: ent);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ private void OnCQCPressure(Entity ent, ref CqcPressurePerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out _))
+ return;
+
+ _stamina.TakeStaminaDamage(target, proto.StaminaDamage, source: ent);
+ if (!_hands.TryGetActiveItem(target, out var activeItem))
+ return;
+ if(!_hands.TryDrop(target, activeItem.Value))
+ return;
+ if (!_hands.TryGetEmptyHand(target, out var emptyHand))
+ return;
+ if(!_hands.TryPickupAnyHand(ent, activeItem.Value))
+ return;
+ _hands.SetActiveHand(ent, emptyHand);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ private void OnCQCConsecutive(Entity ent, ref CqcConsecutivePerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out _))
+ return;
+
+ DoDamage(ent, target, proto.DamageType, proto.ExtraDamage, out _);
+ _stamina.TakeStaminaDamage(target, proto.StaminaDamage, source: ent);
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit1.ogg"), target);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ #endregion
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.CanPerformCombo.cs b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.CanPerformCombo.cs
new file mode 100644
index 00000000000..ad59331a2ec
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.CanPerformCombo.cs
@@ -0,0 +1,81 @@
+using System.Linq;
+using Content.Shared._Goobstation.MartialArts.Components;
+using Content.Shared._Goobstation.MartialArts.Events;
+using Content.Shared.Mobs.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._Goobstation.MartialArts;
+
+///
+/// This handles determining if a combo was performed.
+///
+public partial class SharedMartialArtsSystem
+{
+ private void InitializeCanPerformCombo()
+ {
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnAttackPerformed);
+ SubscribeLocalEvent(OnComboBeingPerformed);
+ }
+
+ private void OnMapInit(EntityUid uid, CanPerformComboComponent component, MapInitEvent args)
+ {
+ foreach (var item in component.RoundstartCombos)
+ {
+ component.AllowedCombos.Add(_proto.Index(item));
+ }
+ }
+
+ private void OnAttackPerformed(EntityUid uid, CanPerformComboComponent component, ComboAttackPerformedEvent args)
+ {
+ if (!HasComp(args.Target))
+ return;
+
+ if (component.CurrentTarget != null && args.Target != component.CurrentTarget.Value)
+ {
+ component.LastAttacks.Clear();
+ }
+
+ if (args.Weapon != uid)
+ {
+ component.LastAttacks.Clear();
+ return;
+ }
+
+ component.CurrentTarget = args.Target;
+ component.ResetTime = _timing.CurTime + TimeSpan.FromSeconds(4);
+ component.LastAttacks.Add(args.Type);
+ CheckCombo(uid, component);
+ }
+
+ private void CheckCombo(EntityUid uid, CanPerformComboComponent comp)
+ {
+ var success = false;
+
+ foreach (var proto in comp.AllowedCombos)
+ {
+ if (success)
+ break;
+
+ var sum = comp.LastAttacks.Count - proto.AttackTypes.Count;
+ if (sum < 0)
+ continue;
+
+ var list = comp.LastAttacks.GetRange(sum, proto.AttackTypes.Count).AsEnumerable();
+ var attackList = proto.AttackTypes.AsEnumerable();
+
+ if (!list.SequenceEqual(attackList) || proto.ResultEvent == null)
+ continue;
+ var beingPerformedEv = new ComboBeingPerformedEvent(proto.ID);
+ var ev = proto.ResultEvent;
+ RaiseLocalEvent(uid, beingPerformedEv);
+ RaiseLocalEvent(uid, ev);
+ comp.LastAttacks.Clear();
+ }
+ }
+ private void OnComboBeingPerformed(Entity ent, ref ComboBeingPerformedEvent args)
+ {
+ ent.Comp.BeingPerformed = args.ProtoId;
+ Dirty(ent, ent.Comp);
+ }
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.CorporateJudo.cs b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.CorporateJudo.cs
new file mode 100644
index 00000000000..d4385f1d5d6
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.CorporateJudo.cs
@@ -0,0 +1,140 @@
+using Content.Shared._Goobstation.MartialArts.Components;
+using Content.Shared._Goobstation.MartialArts.Events;
+using Content.Shared.Clothing;
+using Content.Shared.Damage;
+using Content.Shared.Eye.Blinding.Components;
+using Content.Shared.Movement.Pulling.Components;
+using Content.Shared.StatusEffect;
+using Content.Shared.Weapons.Melee;
+using Robust.Shared.Audio;
+
+namespace Content.Shared._Goobstation.MartialArts;
+
+public partial class SharedMartialArtsSystem
+{
+ private void InitializeCorporateJudo()
+ {
+ SubscribeLocalEvent(OnJudoThrow);
+ SubscribeLocalEvent(OnJudoEyepoke);
+ SubscribeLocalEvent(OnJudoArmbar);
+
+ SubscribeLocalEvent(OnGrantCorporateJudo);
+ SubscribeLocalEvent(OnRemoveCorporateJudo);
+ //SubscribeLocalEvent(OnJudoGoldenBlast); -- rework
+ // Wheel throw
+ // Discombobulate
+ }
+
+ #region Generic Methods
+
+ private void OnGrantCorporateJudo(Entity ent, ref ClothingGotEquippedEvent args)
+ {
+ if (!_netManager.IsServer)
+ return;
+
+ var user = args.Wearer;
+ TryGrantMartialArt(user, ent.Comp);
+ }
+
+ private void OnRemoveCorporateJudo(Entity ent, ref ClothingGotUnequippedEvent args)
+ {
+ var user = args.Wearer;
+ if (!TryComp(user, out var martialArtsKnowledge))
+ return;
+
+ if (martialArtsKnowledge.MartialArtsForm != MartialArtsForms.CorporateJudo)
+ return;
+
+ if(!TryComp(args.Wearer, out var meleeWeaponComponent))
+ return;
+
+ meleeWeaponComponent.Damage = martialArtsKnowledge.OriginalFistDamage;
+
+ RemComp(user);
+ RemComp(user);
+ }
+
+ #endregion
+
+ #region Combo Methods
+
+ private void OnJudoThrow(Entity ent, ref JudoThrowPerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out var downed)
+ || downed)
+ return;
+
+ _stun.TryKnockdown(target, TimeSpan.FromSeconds(proto.ParalyzeTime), false);
+ _stamina.TakeStaminaDamage(target, proto.StaminaDamage);
+ if (TryComp(target, out var pullable))
+ _pulling.TryStopPull(target, pullable, ent, true);
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit3.ogg"), target);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ private void OnJudoEyepoke(Entity ent, ref JudoEyePokePerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out _))
+ return;
+
+ if (!TryComp(target, out StatusEffectsComponent? status))
+ return;
+
+ _status.TryAddStatusEffect(target,
+ "TemporaryBlindness",
+ TimeSpan.FromSeconds(2),
+ true,
+ status);
+ _status.TryAddStatusEffect(target,
+ "BlurryVision",
+ TimeSpan.FromSeconds(5),
+ false,
+ status);
+ DoDamage(ent, target, proto.DamageType, proto.ExtraDamage, out _);
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit3.ogg"), target);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ private void OnJudoArmbar(Entity ent, ref JudoArmbarPerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out var downed))
+ return;
+
+ switch (downed)
+ {
+ case false:
+ var item = _hands.GetActiveItem(target);
+ if (item != null)
+ _hands.TryDrop(target, item.Value);
+ break;
+ case true:
+ _stamina.TakeStaminaDamage(target, proto.StaminaDamage);
+ _stun.TryKnockdown(target, TimeSpan.FromSeconds(proto.ParalyzeTime), false);
+ break;
+ }
+
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit3.ogg"), target);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ /* Pending Implement
+ private void OnJudoGoldenBlast(Entity ent, ref JudoGoldenBlastPerformedEvent args)
+ {
+ if (!TryUseMartialArt(ent, MartialArtsForms.CorporateJudo, out var target, out var downed))
+ return;
+
+ if (downed)
+ return;
+
+ _stun.TryParalyze(target, TimeSpan.FromSeconds(30), false);
+ if (TryComp(target, out var pullable))
+ _pulling.TryStopPull(target, pullable, ent, true);
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit3.ogg"), target);
+ }
+ */
+
+ #endregion
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.KravMaga.cs b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.KravMaga.cs
new file mode 100644
index 00000000000..34a3ed53776
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.KravMaga.cs
@@ -0,0 +1,109 @@
+using Content.Shared._Goobstation.MartialArts.Components;
+using Content.Shared._Goobstation.MartialArts.Events;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Weapons.Melee.Events;
+
+namespace Content.Shared._Goobstation.MartialArts;
+
+///
+/// This handles...
+///
+public abstract partial class SharedMartialArtsSystem
+{
+ private void InitializeKravMaga()
+ {
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnKravMagaAction);
+ SubscribeLocalEvent(OnMeleeHitEvent);
+ SubscribeLocalEvent(OnKravMagaShutdown);
+ }
+
+ private void OnMeleeHitEvent(Entity ent, ref MeleeHitEvent args)
+ {
+ if (args.HitEntities.Count <= 0)
+ return;
+
+ foreach (var hitEntity in args.HitEntities)
+ {
+ if (!HasComp(hitEntity))
+ continue;
+ if (!TryComp(hitEntity, out var isDowned))
+ continue;
+
+ DoKravMaga(ent, hitEntity, isDowned);
+ }
+ }
+
+ private void DoKravMaga(Entity ent, EntityUid hitEntity, RequireProjectileTargetComponent reguireProjectileTargetComponent)
+ {
+ if (ent.Comp.SelectedMoveComp == null)
+ return;
+ var moveComp = ent.Comp.SelectedMoveComp;
+
+ switch (ent.Comp.SelectedMove)
+ {
+ case KravMagaMoves.LegSweep:
+ if(_netManager.IsClient)
+ return;
+ _stun.TryKnockdown(hitEntity, TimeSpan.FromSeconds(4), true);
+ break;
+ case KravMagaMoves.NeckChop:
+ var comp = EnsureComp(hitEntity);
+ comp.SilencedTime = _timing.CurTime + TimeSpan.FromSeconds(moveComp.EffectTime);
+ break;
+ case KravMagaMoves.LungPunch:
+ _stamina.TakeStaminaDamage(hitEntity, moveComp.StaminaDamage);
+ var blockedBreathingComponent = EnsureComp(hitEntity);
+ blockedBreathingComponent.BlockedTime = _timing.CurTime + TimeSpan.FromSeconds(moveComp.EffectTime);
+ break;
+ case null:
+ var damage = ent.Comp.BaseDamage;
+ if (reguireProjectileTargetComponent.Active)
+ damage *= ent.Comp.DownedDamageModifier;
+
+ DoDamage(ent.Owner, hitEntity, "Blunt", damage, out _);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ ent.Comp.SelectedMove = null;
+ ent.Comp.SelectedMoveComp = null;
+ }
+
+ private void OnKravMagaAction(Entity ent, ref KravMagaActionEvent args)
+ {
+ var actionEnt = args.Action.Owner;
+ if (!TryComp(actionEnt, out var kravActionComp))
+ return;
+
+ _popupSystem.PopupClient(Loc.GetString("krav-maga-ready", ("action", kravActionComp.Name)), ent, ent);
+ ent.Comp.SelectedMove = kravActionComp.Configuration;
+ ent.Comp.SelectedMoveComp = kravActionComp;
+ }
+
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ if (HasComp(ent))
+ return;
+ foreach (var actionId in ent.Comp.BaseKravMagaMoves)
+ {
+ var actions = _actions.AddAction(ent, actionId);
+ if (actions != null)
+ ent.Comp.KravMagaMoveEntities.Add(actions.Value);
+ }
+ }
+
+ private void OnKravMagaShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ if (!TryComp(ent, out var kravMaga))
+ return;
+
+ foreach (var action in ent.Comp.KravMagaMoveEntities)
+ {
+ _actions.RemoveAction(action);
+ }
+ }
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.SleepingCarp.cs b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.SleepingCarp.cs
new file mode 100644
index 00000000000..32c98f555b5
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.SleepingCarp.cs
@@ -0,0 +1,161 @@
+using System.Linq;
+using Content.Shared._Goobstation.MartialArts.Components;
+using Content.Shared._Goobstation.MartialArts.Events;
+using Content.Shared._Shitmed.Targeting;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Movement.Pulling.Components;
+using Content.Shared.Popups;
+using Content.Shared.Weapons.Reflect;
+using Robust.Shared.Audio;
+
+namespace Content.Shared._Goobstation.MartialArts;
+
+public partial class SharedMartialArtsSystem
+{
+ private void InitializeSleepingCarp()
+ {
+ SubscribeLocalEvent(OnSleepingCarpGnashing);
+ SubscribeLocalEvent(OnSleepingCarpKneeHaul);
+ SubscribeLocalEvent(OnSleepingCarpCrashingWaves);
+
+ SubscribeLocalEvent(OnGrantSleepingCarp);
+ }
+
+ #region Generic Methods
+
+ private void OnGrantSleepingCarp(Entity ent, ref UseInHandEvent args)
+ {
+ if (!_netManager.IsServer)
+ return;
+
+ var studentComp = EnsureComp(args.User);
+
+ if (studentComp.UseAgainTime == TimeSpan.Zero)
+ {
+ CarpScrollDelay((args.User, studentComp));
+ return;
+ }
+
+ if (_timing.CurTime < studentComp.UseAgainTime)
+ {
+ _popupSystem.PopupEntity(
+ Loc.GetString("carp-scroll-waiting"),
+ ent,
+ args.User,
+ PopupType.MediumCaution);
+ return;
+ }
+
+ switch (studentComp.Stage)
+ {
+ case < 3:
+ CarpScrollDelay((args.User, studentComp));
+ break;
+ case >= 3:
+ if (!TryGrantMartialArt(args.User, ent.Comp))
+ return;
+ var userReflect = EnsureComp(args.User);
+ userReflect.ReflectProb = 1;
+ userReflect.Spread = 60;
+ _popupSystem.PopupEntity(
+ Loc.GetString("carp-scroll-complete"),
+ ent,
+ args.User,
+ PopupType.LargeCaution);
+ return;
+ }
+ }
+
+ private void CarpScrollDelay(Entity ent)
+ {
+ var time = new System.Random().Next(ent.Comp.MinUseDelay, ent.Comp.MaxUseDelay);
+ ent.Comp.UseAgainTime = _timing.CurTime + TimeSpan.FromSeconds(time);
+ ent.Comp.Stage++;
+ _popupSystem.PopupEntity(
+ Loc.GetString("carp-scroll-advance"),
+ ent,
+ ent,
+ PopupType.Medium);
+ }
+
+ #endregion
+
+ #region Combo Methods
+
+ private void OnSleepingCarpGnashing(Entity ent,
+ ref SleepingCarpGnashingTeethPerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !_proto.TryIndex(proto.MartialArtsForm.ToString(), out var martialArtProto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out var downed))
+ return;
+
+ DoDamage(ent, target, proto.DamageType, proto.ExtraDamage + ent.Comp.ConsecutiveGnashes * 5, out _);
+ ent.Comp.ConsecutiveGnashes++;
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit1.ogg"), target);
+ if (!downed)
+ {
+ var saying =
+ martialArtProto.RandomSayings.ElementAt(
+ _random.Next(martialArtProto.RandomSayings.Count));
+ var ev = new SleepingCarpSaying(saying);
+ RaiseLocalEvent(ent, ev);
+ }
+ else
+ {
+ var saying =
+ martialArtProto.RandomSayingsDowned.ElementAt(
+ _random.Next(martialArtProto.RandomSayingsDowned.Count));
+ var ev = new SleepingCarpSaying(saying);
+ RaiseLocalEvent(ent, ev);
+ }
+ }
+
+ private void OnSleepingCarpKneeHaul(Entity ent,
+ ref SleepingCarpKneeHaulPerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out var downed))
+ return;
+
+ if (!downed)
+ {
+ DoDamage(ent, target, proto.DamageType, proto.ExtraDamage, out _);
+ _stamina.TakeStaminaDamage(target, proto.StaminaDamage);
+ _stun.TryKnockdown(target, TimeSpan.FromSeconds(proto.ParalyzeTime), true);
+ }
+ else
+ {
+ DoDamage(ent, target, proto.DamageType, proto.ExtraDamage / 2, out _);
+ _stamina.TakeStaminaDamage(target, proto.StaminaDamage - 20);
+ _hands.TryDrop(target);
+ }
+ if (TryComp(target, out var pullable))
+ _pulling.TryStopPull(target, pullable, ent, true);
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit3.ogg"), target);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ private void OnSleepingCarpCrashingWaves(Entity ent,
+ ref SleepingCarpCrashingWavesPerformedEvent args)
+ {
+ if (!_proto.TryIndex(ent.Comp.BeingPerformed, out var proto)
+ || !TryUseMartialArt(ent, proto.MartialArtsForm, out var target, out var downed)
+ || downed)
+ return;
+
+ DoDamage(ent, target, proto.DamageType, proto.ExtraDamage, out var damage);
+ var mapPos = _transform.GetMapCoordinates(ent).Position;
+ var hitPos = _transform.GetMapCoordinates(target).Position;
+ var dir = hitPos - mapPos;
+ if (TryComp(target, out var pullable))
+ _pulling.TryStopPull(target, pullable, ent, true);
+ _grabThrowing.Throw(target, ent, dir, proto.ThrownSpeed, damage);
+ _audio.PlayPvs(new SoundPathSpecifier("/Audio/Weapons/genhit2.ogg"), target);
+ ComboPopup(ent, target, proto.Name);
+ }
+
+ #endregion
+}
diff --git a/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.cs b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.cs
new file mode 100644
index 00000000000..327e4d79055
--- /dev/null
+++ b/Content.Shared/_Goobstation/MartialArts/SharedMartialArtsSystem.cs
@@ -0,0 +1,294 @@
+using Content.Shared._Goobstation.MartialArts.Components;
+using Content.Shared._Shitmed.Targeting;
+using Content.Shared._White.Grab;
+using Content.Shared.Actions;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Movement.Pulling.Components;
+using Content.Shared.Movement.Pulling.Events;
+using Content.Shared.Movement.Pulling.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Speech;
+using Content.Shared.Standing;
+using Content.Shared.StatusEffect;
+using Content.Shared.Stunnable;
+using Content.Shared.Weapons.Melee;
+using Content.Shared.Weapons.Melee.Events;
+using Content.Shared.Weapons.Ranged.Events;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Shared._Goobstation.MartialArts;
+
+///
+/// Handles most of Martial Arts Systems.
+///
+public abstract partial class SharedMartialArtsSystem : EntitySystem
+{
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly PullingSystem _pulling = default!;
+ [Dependency] private readonly StatusEffectsSystem _status = default!;
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+ [Dependency] private readonly StaminaSystem _stamina = default!;
+ [Dependency] private readonly GrabThrownSystem _grabThrowing = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedStunSystem _stun = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+ [Dependency] private readonly INetManager _netManager = default!;
+ [Dependency] private readonly StandingStateSystem _standingState = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ InitializeKravMaga();
+ InitializeSleepingCarp();
+ InitializeCqc();
+ InitializeCorporateJudo();
+ InitializeCanPerformCombo();
+
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(CheckGrabStageOverride);
+ SubscribeLocalEvent(OnMeleeHit);
+ SubscribeLocalEvent(OnShotAttempt);
+ SubscribeLocalEvent(OnSilencedSpeakAttempt);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out _, out var comp))
+ {
+ if (_timing.CurTime < comp.ResetTime || comp.LastAttacks.Count <= 0)
+ continue;
+ comp.LastAttacks.Clear();
+ comp.ConsecutiveGnashes = 0;
+ }
+
+ var kravSilencedQuery = EntityQueryEnumerator();
+ while (kravSilencedQuery.MoveNext(out var ent, out var comp))
+ {
+ if (_timing.CurTime < comp.SilencedTime)
+ continue;
+ RemComp(ent);
+ }
+
+ var kravBlockedQuery = EntityQueryEnumerator();
+ while (kravBlockedQuery.MoveNext(out var ent, out var comp))
+ {
+ if (_timing.CurTime < comp.BlockedTime)
+ continue;
+ RemComp(ent);
+ }
+ }
+
+ #region Event Methods
+
+ private void OnMeleeHit(Entity ent, ref MeleeHitEvent args)
+ {
+ if (args.Handled)
+ return;
+
+ if(!_proto.TryIndex(ent.Comp.MartialArtsForm.ToString(), out var martialArtsPrototype))
+ return;
+
+ if (!martialArtsPrototype.RandomDamageModifier)
+ return;
+
+ var randomDamage = _random.Next(martialArtsPrototype.MinRandomDamageModifier, martialArtsPrototype.MaxRandomDamageModifier);
+ var bonusDamageSpec = new DamageSpecifier();
+ bonusDamageSpec.DamageDict.Add("Blunt", randomDamage);
+ args.BonusDamage += bonusDamageSpec;
+ }
+
+ private void OnShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ if(TryComp(ent, out var comboComponent))
+ comboComponent.AllowedCombos.Clear();
+ }
+
+ private void CheckGrabStageOverride(EntityUid uid, T component, CheckGrabOverridesEvent args)
+ where T : GrabStagesOverrideComponent
+ {
+ if (args.Stage == GrabStage.Soft)
+ args.Stage = component.StartingStage;
+ }
+
+ private void OnSilencedSpeakAttempt(Entity ent, ref SpeakAttemptEvent args)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("popup-grabbed-cant-speak"),
+ ent,
+ ent); // You cant speak while someone is choking you
+ args.Cancel();
+ }
+
+ private void OnShotAttempt(Entity ent, ref ShotAttemptedEvent args)
+ {
+ if (ent.Comp.MartialArtsForm != MartialArtsForms.SleepingCarp)
+ return;
+ _popupSystem.PopupClient(Loc.GetString("gun-disabled"), ent, ent);
+ args.Cancel();
+ }
+
+ private void ComboPopup(EntityUid user, EntityUid target, string comboName)
+ {
+ if (!_netManager.IsServer)
+ return;
+ var userName = Identity.Entity(user, EntityManager);
+ var targetName = Identity.Entity(target, EntityManager);
+ _popupSystem.PopupEntity(Loc.GetString("martial-arts-action-sender",
+ ("name", targetName),
+ ("move", comboName)),
+ user,
+ user);
+ _popupSystem.PopupEntity(Loc.GetString("martial-arts-action-receiver",
+ ("name", userName),
+ ("move", comboName)),
+ target,
+ target);
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ ///
+ /// Tries to grant a martial art to a user. Use this method.
+ ///
+ ///
+ ///
+ ///
+ private bool TryGrantMartialArt(EntityUid user, GrantMartialArtKnowledgeComponent comp)
+ {
+ if (!_netManager.IsServer || MetaData(user).EntityLifeStage >= EntityLifeStage.Terminating)
+ return false;
+
+ if (HasComp(user))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("cqc-fail-knowanother"), user, user);
+ return false;
+ }
+
+ if (!HasComp(user))
+ {
+ return GrantMartialArt(comp, user);
+ }
+
+ if (!TryComp(user, out var cqc))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("cqc-fail-knowanother"), user, user);
+ return false;
+ }
+
+ if (cqc.Blocked && comp.MartialArtsForm == MartialArtsForms.CloseQuartersCombat)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("cqc-success-unblocked"), user, user);
+ cqc.Blocked = false;
+ comp.Used = true;
+ return false;
+ }
+
+ _popupSystem.PopupEntity(Loc.GetString("cqc-fail-already"), user, user);
+ return false;
+ }
+
+ private bool GrantMartialArt(GrantMartialArtKnowledgeComponent comp, EntityUid user)
+ {
+ var canPerformComboComponent = EnsureComp(user);
+ var martialArtsKnowledgeComponent = EnsureComp(user);
+ var pullerComponent = EnsureComp(user);
+
+ if (!_proto.TryIndex(comp.MartialArtsForm.ToString(), out var martialArtsPrototype)
+ || !TryComp(user, out var meleeWeaponComponent))
+ return false;
+
+ martialArtsKnowledgeComponent.MartialArtsForm = martialArtsPrototype.MartialArtsForm;
+ LoadCombos(martialArtsPrototype.RoundstartCombos, canPerformComboComponent);
+ martialArtsKnowledgeComponent.Blocked = false;
+ pullerComponent.StageChangeCooldown /= 2;
+
+ martialArtsKnowledgeComponent.OriginalFistDamage = meleeWeaponComponent.Damage;
+ var newDamage = new DamageSpecifier();
+ newDamage.DamageDict.Add("Blunt", martialArtsPrototype.BaseDamageModifier);
+ meleeWeaponComponent.Damage += newDamage;
+
+ Dirty(user, canPerformComboComponent);
+ Dirty(user, pullerComponent);
+ return true;
+ }
+
+ private void LoadCombos(ProtoId list, CanPerformComboComponent combo)
+ {
+ combo.AllowedCombos.Clear();
+ if (!_proto.TryIndex(list, out var comboListPrototype))
+ return;
+ foreach (var item in comboListPrototype.Combos)
+ {
+ combo.AllowedCombos.Add(_proto.Index(item));
+ }
+ }
+
+ private bool TryUseMartialArt(Entity ent,
+ MartialArtsForms form,
+ out EntityUid target,
+ out bool downed)
+ {
+ target = EntityUid.Invalid;
+ downed = false;
+
+ if (ent.Comp.CurrentTarget == null)
+ return false;
+
+ if (!TryComp(ent, out var knowledgeComponent))
+ return false;
+
+ if (!TryComp(ent.Comp.CurrentTarget, out var isDowned))
+ return false;
+
+ downed = isDowned.Active;
+ target = ent.Comp.CurrentTarget.Value;
+
+ if (knowledgeComponent.MartialArtsForm == form && !knowledgeComponent.Blocked)
+ {
+ return true;
+ }
+
+ foreach (var entInRange in _lookup.GetEntitiesInRange(ent, 8f))
+ {
+ if (!TryPrototype(entInRange, out var proto) || proto.ID != "SpawnPointChef" || !knowledgeComponent.Blocked)
+ continue;
+ return true;
+ }
+
+ return false;
+ }
+
+ private void DoDamage(EntityUid ent,
+ EntityUid target,
+ string damageType,
+ int damageAmount,
+ out DamageSpecifier damage,
+ TargetBodyPart? targetBodyPart = null)
+ {
+ damage = new DamageSpecifier();
+ if(!TryComp(ent, out var targetingComponent))
+ return;
+ damage.DamageDict.Add(damageType, damageAmount);
+ _damageable.TryChangeDamage(target, damage, origin: ent, targetPart: targetBodyPart ?? targetingComponent.Target);
+ }
+
+ #endregion
+}
diff --git a/Content.Shared/_Goobstation/TableSlam/PostTabledComponent.cs b/Content.Shared/_Goobstation/TableSlam/PostTabledComponent.cs
new file mode 100644
index 00000000000..97cd0ca9ae2
--- /dev/null
+++ b/Content.Shared/_Goobstation/TableSlam/PostTabledComponent.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._Goobstation.TableSlam;
+
+///
+/// This is used for...
+///
+[RegisterComponent]
+public sealed partial class PostTabledComponent : Component
+{
+ [DataField]
+ public TimeSpan PostTabledShovableTime = TimeSpan.Zero;
+
+ [DataField]
+ public float ParalyzeChance = 0.35f;
+}
diff --git a/Content.Shared/_Goobstation/TableSlam/TableSlamSystem.cs b/Content.Shared/_Goobstation/TableSlam/TableSlamSystem.cs
new file mode 100644
index 00000000000..8f9fbc88d4f
--- /dev/null
+++ b/Content.Shared/_Goobstation/TableSlam/TableSlamSystem.cs
@@ -0,0 +1,148 @@
+using System.Linq;
+using Content.Shared._EinsteinEngines.Contests;
+using Content.Shared._Shitmed.Targeting;
+using Content.Shared.Actions.Events;
+using Content.Shared.Climbing.Components;
+using Content.Shared.CombatMode;
+using Content.Shared.Coordinates;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Events;
+using Content.Shared.Damage.Systems;
+using Content.Shared.FixedPoint;
+using Content.Shared.Interaction;
+using Content.Shared.Movement.Pulling.Components;
+using Content.Shared.Movement.Pulling.Systems;
+using Content.Shared.Standing;
+using Content.Shared.StatusEffect;
+using Content.Shared.Stunnable;
+using Content.Shared.Throwing;
+using Content.Shared.Weapons.Melee;
+using Content.Shared.Weapons.Melee.Events;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Shared._Goobstation.TableSlam;
+
+///
+/// This handles...
+///
+public sealed class TableSlamSystem : EntitySystem
+{
+ [Dependency] private readonly PullingSystem _pullingSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ [Dependency] private readonly StandingStateSystem _standing = default!;
+ [Dependency] private readonly ThrowingSystem _throwingSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly StaminaSystem _staminaSystem = default!;
+ [Dependency] private readonly SharedStunSystem _stunSystem = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly ContestsSystem _contestsSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnMeleeHit);
+ SubscribeLocalEvent(OnStartCollide);
+ SubscribeLocalEvent(OnDisarmAttemptEvent);
+ }
+
+ private void OnDisarmAttemptEvent(Entity ent, ref DisarmAttemptEvent args)
+ {
+ if(!_random.Prob(ent.Comp.ParalyzeChance))
+ return;
+
+ _stunSystem.TryParalyze(ent, TimeSpan.FromSeconds(3), false);
+ RemComp