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(ent); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var tabledQuery = EntityQueryEnumerator(); + while (tabledQuery.MoveNext(out var uid, out var comp)) + { + if (_gameTiming.CurTime >= comp.PostTabledShovableTime) + RemComp(uid); + } + } + + private void OnMeleeHit(Entity ent, ref MeleeHitEvent args) + { + if (ent.Comp.GrabStage < GrabStage.Suffocate + || ent.Comp.Pulling == null) + return; + + if(!TryComp(ent.Comp.Pulling, out var pullableComponent)) + return; + + if (args.Direction != null) + return; + if (args.HitEntities.Count is > 1 or 0) + return; + + var target = args.HitEntities.ElementAt(0); + if (!HasComp(target)) // checks if its a table. + return; + + var massContest = _contestsSystem.MassContest(ent, ent.Comp.Pulling.Value); + var attemptChance = Math.Clamp(1 * massContest, 0, 1); + var attemptRoundedToNearestQuarter = Math.Round(attemptChance * 4, MidpointRounding.ToEven) / 4; + if(_random.Prob((float) attemptRoundedToNearestQuarter)) // base chance to table slam someone is 1 if your mass ratio is less than 1 then your going to have a harder time slamming somebody. + TryTableSlam((ent.Comp.Pulling.Value, pullableComponent), ent, target); + } + + public void TryTableSlam(Entity ent, Entity pullerEnt, EntityUid tableUid) + { + if(!_transformSystem.InRange(ent.Owner.ToCoordinates(), tableUid.ToCoordinates(), 2f )) + return; + + _standing.Down(ent); + + _pullingSystem.TryStopPull(ent, ent.Comp, pullerEnt, ignoreGrab: true); + _throwingSystem.TryThrow(ent, tableUid.ToCoordinates() , ent.Comp.BasedTabledForceSpeed, animated: false, doSpin: false); + pullerEnt.Comp.NextStageChange = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3)); // prevent table slamming spam + ent.Comp.BeingTabled = true; + } + + private void OnStartCollide(Entity ent, ref StartCollideEvent args) + { + if(!ent.Comp.BeingTabled) + return; + + if (!HasComp(args.OtherEntity)) + return; + + var modifierOnGlassBreak = 1; + if (TryComp(args.OtherEntity, out var glassTableComponent)) + { + _damageableSystem.TryChangeDamage(args.OtherEntity, glassTableComponent.TableDamage, origin: ent, targetPart: TargetBodyPart.Torso); + _damageableSystem.TryChangeDamage(args.OtherEntity, glassTableComponent.ClimberDamage, origin: ent); + modifierOnGlassBreak = 2; + } + else + { + _damageableSystem.TryChangeDamage(ent, + new DamageSpecifier() + { + DamageDict = new Dictionary { { "Blunt", ent.Comp.TabledDamage } }, + }, + targetPart: TargetBodyPart.Torso); + _damageableSystem.TryChangeDamage(ent, + new DamageSpecifier() + { + DamageDict = new Dictionary { { "Blunt", ent.Comp.TabledDamage } }, + }); + } + + _staminaSystem.TakeStaminaDamage(ent, ent.Comp.TabledStaminaDamage); + _stunSystem.TryKnockdown(ent, TimeSpan.FromSeconds(3 * modifierOnGlassBreak), false); + var postTabledComponent = EnsureComp(ent); + postTabledComponent.PostTabledShovableTime = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3)); + ent.Comp.BeingTabled = false; + + //_audioSystem.PlayPvs("/Audio/Effects/thudswoosh.ogg", uid); + } +} diff --git a/Content.Shared/_White/Grab/GrabThrownComponent.cs b/Content.Shared/_White/Grab/GrabThrownComponent.cs new file mode 100644 index 00000000000..d9e3ff5bb00 --- /dev/null +++ b/Content.Shared/_White/Grab/GrabThrownComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; +using Content.Shared.Damage; + +namespace Content.Shared._White.Grab; + +[RegisterComponent, NetworkedComponent] +public sealed partial class GrabThrownComponent : Component +{ + public DamageSpecifier? DamageOnCollide; + + public List IgnoreEntity = new(); +} diff --git a/Content.Shared/_White/Grab/GrabThrownSystem.cs b/Content.Shared/_White/Grab/GrabThrownSystem.cs new file mode 100644 index 00000000000..e46c8cb9284 --- /dev/null +++ b/Content.Shared/_White/Grab/GrabThrownSystem.cs @@ -0,0 +1,101 @@ +using Content.Shared.Damage.Systems; +using Content.Shared.Damage; +using Content.Shared.Effects; +using Content.Shared.Throwing; +using Robust.Shared.Network; +using Robust.Shared.Physics.Events; +using Robust.Shared.Player; +using System.Numerics; +using Content.Shared.Standing; +using Robust.Shared.Physics.Components; + +namespace Content.Shared._White.Grab; + +public sealed class GrabThrownSystem : EntitySystem +{ + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedColorFlashEffectSystem _color = default!; + [Dependency] private readonly StaminaSystem _stamina = default!; + [Dependency] private readonly ThrowingSystem _throwing = default!; + [Dependency] private readonly INetManager _netMan = default!; + [Dependency] private readonly SharedLayingDownSystem _layingDown = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandleCollide); + SubscribeLocalEvent(OnStopThrow); + } + + private void HandleCollide(Entity ent, ref StartCollideEvent args) + { + if (_netMan.IsClient) // To avoid effect spam + return; + + if (!HasComp(ent)) + { + RemComp(ent); + return; + } + + if (ent.Comp.IgnoreEntity.Contains(args.OtherEntity)) + return; + + if (!HasComp(ent)) + RemComp(ent); + + if(!TryComp(ent, out var physicsComponent)) + return; + + ent.Comp.IgnoreEntity.Add(args.OtherEntity); + + var velocitySquared = args.OurBody.LinearVelocity.LengthSquared(); + var mass = physicsComponent.Mass; + var kineticEnergy = 0.5f * mass * velocitySquared; + + var kineticEnergyDamage = new DamageSpecifier(); + kineticEnergyDamage.DamageDict.Add("Blunt", 1); + var modNumber = Math.Floor(kineticEnergy / 100); + kineticEnergyDamage *= Math.Floor(modNumber / 3); + _damageable.TryChangeDamage(args.OtherEntity, kineticEnergyDamage); + _stamina.TakeStaminaDamage(ent, (float) Math.Floor(modNumber / 2)); + + _layingDown.TryLieDown(args.OtherEntity, behavior: DropHeldItemsBehavior.AlwaysDrop); + + _color.RaiseEffect(Color.Red, new List() { ent }, Filter.Pvs(ent, entityManager: EntityManager)); + } + + private void OnStopThrow(EntityUid uid, GrabThrownComponent comp, StopThrowEvent args) + { + if (comp.DamageOnCollide != null) + _damageable.TryChangeDamage(uid, comp.DamageOnCollide); + + if (HasComp(uid)) + RemComp(uid); + } + + /// + /// Throwing entity to the direction and ensures GrabThrownComponent with params + /// + /// Entity to throw + /// Entity that throws + /// Direction + /// How fast you fly when thrown + /// Stamina damage on collide + /// Damage to entity on collide + public void Throw( + EntityUid uid, + EntityUid thrower, + Vector2 vector, + float grabThrownSpeed, + DamageSpecifier? damageToUid = null) + { + var comp = EnsureComp(uid); + comp.IgnoreEntity.Add(thrower); + comp.DamageOnCollide = damageToUid; + + _layingDown.TryLieDown(uid, behavior: DropHeldItemsBehavior.AlwaysDrop); + _throwing.TryThrow(uid, vector, grabThrownSpeed, animated: false); + } +} diff --git a/Resources/Locale/en-US/_Goobstation/martial-arts/martial-arts.ftl b/Resources/Locale/en-US/_Goobstation/martial-arts/martial-arts.ftl new file mode 100644 index 00000000000..19e740a9faa --- /dev/null +++ b/Resources/Locale/en-US/_Goobstation/martial-arts/martial-arts.ftl @@ -0,0 +1,44 @@ +cqc-fail-used = {CAPITALIZE(THE($manual))} is already used. +cqc-fail-notself = You can't teach anyone with {CAPITALIZE(THE($manual))}. + +cqc-fail-knowanother = You already know another martial art. +cqc-fail-already = You already know everything about a Martial art. +cqc-success-unblocked = Your CQC skills are not bound to kitchen anymore. +cqc-success-learned = You have learned CQC. + +cqc-manual-used = [color=white]{CAPITALIZE(THE($manual))} looks to be already used.[/color] + +ent-CQCManual = CQC manual + .desc = Looks like a usual book, but contains some secrets inside. + +carp-scroll-waiting = The journey of a thousand miles begins with one step, and the path of wisdom is traveled slowly, one lesson at a time. +carp-scroll-advance = You have taken one step closer to becoming a master of the Way of the Sleeping Carp. +carp-scroll-complete = You are now a master of the Way of the Sleeping Carp. + +carp-saying-huah = HUAH! +carv-vaying-hya = HYA! +carp-saying-choo = CHOO! +carp-saying-wuo = WUO! +carp-saying-kya = KYA! +carp-saying-huh = HUH! +carp-saying-hiyoh = HIYOH! +carp-saying-strike = CARP STRIKE! +carp-saying-bite = CARP BITE! + +carp-saying-banzai = BANZAIII! +carp-saying-kiya = KIYAAAA! +carp-saying-omae = OMAE WA MOU SHINDEIRU! +carp-saying-see = YOU CAN'T SEE ME! +carp-saying-time = MY TIME IS NOW!! +carp-saying-cowabunga = COWABUNGA! + +krav-maga-ready = You ready a {$action} + +martial-arts-action-sender = You hit {$name} with {$move} +martial-arts-action-receiver = {$name} hit you with {$move} + +uplink-cqc-name = CQC Manual +uplink-cqc-desc = A manual that teaches a single user tactical Close-Quarters Combat before self-destructing. Does not restrict weapon usage. Your disarms will inflict some stamina damage while making people temporarily slur their words, your grabs will immobilize for a second making it easier to reinforce them, your harm attacks will deal moderate stamina damage, and you will get a set of combos that allow you to efficiently knockdown, disarm or stun your opponent. You can find more information on CQC and its combos here. + +uplink-sleeping-carp-name = Martial Arts Scroll +uplink-sleeping-carp-desc = This scroll contains the secrets of an ancient martial arts technique. You will master unarmed combat, deflecting ranged weapon fire. Learning this art means you will also refuse to use dishonorable ranged weaponry. Unable to be understood by vampire and changeling agents. diff --git a/Resources/Locale/en-US/_White/grab/grab.ftl b/Resources/Locale/en-US/_White/grab/grab.ftl new file mode 100644 index 00000000000..1a7fc592ab8 --- /dev/null +++ b/Resources/Locale/en-US/_White/grab/grab.ftl @@ -0,0 +1,21 @@ +popup-grab-soft-target = {CAPITALIZE($puller)} grabbed you softly. +popup-grab-hard-target = {CAPITALIZE($puller)} grabbed you hardly. +popup-grab-suffocate-target = {CAPITALIZE($puller)} started to choke you! +popup-grab-no-target = {CAPITALIZE($puller)} stopped grabbing you. +popup-grab-soft-self = You grabbed {CAPITALIZE($target)} softly. +popup-grab-hard-self = You grabbed {CAPITALIZE($target)} hardly. +popup-grab-suffocate-self = You started to choke {CAPITALIZE($target)}. +popup-grab-no-self = You stopped grabbing {CAPITALIZE($target)}. +popup-grab-soft-others = {CAPITALIZE($puller)} grabbed {CAPITALIZE($target)} softly. +popup-grab-hard-others = {CAPITALIZE($puller)} grabbed {CAPITALIZE($target)} hardly. +popup-grab-suffocate-others = {CAPITALIZE($puller)} started to choke {CAPITALIZE($target)}! +popup-grab-no-others = {CAPITALIZE($puller)} stopped grabbing {CAPITALIZE($target)}. +popup-grab-release-fail-self = You are trying to escape. +popup-grab-release-success-self = You escaped from grab! +popup-grab-release-success-puller = {CAPITALIZE($target)} escaped! +popup-grab-retake-fail = {CAPITALIZE($puller)} is not letting you to pull {CAPITALIZE($pulled)}! +popup-grab-retake-fail-puller = {CAPITALIZE($puller)} is trying to release {CAPITALIZE($pulled)}! +popup-grab-retake-success = You released {CAPITALIZE($pulled)} from {CAPITALIZE($puller)}'s grab! +popup-grab-retake-success-puller = {CAPITALIZE($puller)} released {CAPITALIZE($pulled)} from your grab! +popup-grabbed-cant-speak = You can't breathe! +popup-grab-need-hand = You need a free hand! diff --git a/Resources/Maps/_Mono/Shuttles/World/ramdronesmall.yml b/Resources/Maps/_Mono/Shuttles/World/ramdronesmall.yml index 5e21e9121f8..215f4d25adc 100644 --- a/Resources/Maps/_Mono/Shuttles/World/ramdronesmall.yml +++ b/Resources/Maps/_Mono/Shuttles/World/ramdronesmall.yml @@ -501,13 +501,13 @@ entities: - type: Transform pos: 2.5,0.5 parent: 1 -- proto: WallReinforcedChitin - entities: - - uid: 3 - components: - - type: Transform - pos: 0.5,0.5 - parent: 1 +#- proto: WallReinforcedChitin +# entities: +# - uid: 3 +# components: +# - type: Transform +# pos: 0.5,0.5 +# parent: 1 - uid: 4 components: - type: Transform diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 63183bbfc78..9ad42974d64 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -398,19 +398,42 @@ clickEvent: !type:RetakeVowAlertEvent description: alerts-vow-broken-desc +# Goobstation start +# Changed pulling to accomodate grabbing severity - type: alert id: Pulled - icons: [ /Textures/Interface/Alerts/Pull/pulled.png ] - clickEvent: !type:StopBeingPulledAlertEvent + icons: + - sprite: /Textures/_White/Interface/Alerts/pull.rsi + state: pulled + - sprite: /Textures/_White/Interface/Alerts/pull.rsi + state: grabbed-soft + - sprite: /Textures/_White/Interface/Alerts/pull.rsi + state: grabbed-hard + - sprite: /Textures/_White/Interface/Alerts/pull.rsi + state: grabbed-choke + clickEvent: !type:StopBeingPulledAlertEvent { } name: alerts-pulled-name description: alerts-pulled-desc + minSeverity: 0 + maxSeverity: 3 - type: alert id: Pulling - icons: [ /Textures/Interface/Alerts/Pull/pulling.png ] - clickEvent: !type:StopPullingAlertEvent + icons: + - sprite: /Textures/_White/Interface/Alerts/pull.rsi + state: pulling + - sprite: /Textures/_White/Interface/Alerts/pull.rsi + state: grab-soft + - sprite: /Textures/_White/Interface/Alerts/pull.rsi + state: grab-hard + - sprite: /Textures/_White/Interface/Alerts/pull.rsi + state: grab-choke + clickEvent: !type:StopPullingAlertEvent { } name: alerts-pulling-name description: alerts-pulling-desc + minSeverity: 0 + maxSeverity: 3 +# Goobstation end - type: alert id: Bleed diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml index debbab95b55..223e02842bd 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml @@ -24,6 +24,8 @@ - !type:AddComponentSpecial components: - type: ProfessionalChef #Nyano - End Summary. + - type: GrantCqc # Goobstation - Martial Arts + isBlocked: true - type: startingGear id: ChefGear diff --git a/Resources/Prototypes/_Goobstation/Catalog/uplink_catalog.yml b/Resources/Prototypes/_Goobstation/Catalog/uplink_catalog.yml index 8ac01829501..31343b874a1 100644 --- a/Resources/Prototypes/_Goobstation/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/_Goobstation/Catalog/uplink_catalog.yml @@ -61,17 +61,17 @@ tags: - NukeOpsUplink -#- type: listing -# id: UplinkMysteriousScroll -# name: uplink-sleeping-carp-name -# description: uplink-sleeping-carp-desc -# productEntity: SleepingCarpScroll -# cost: -# Telecrystal: 70 -# categories: -# - UplinkWeaponry -# conditions: -# - !type:StoreWhitelistCondition -# blacklist: -# tags: -# - NukeOpsUplink +- type: listing + id: UplinkMysteriousScroll + name: uplink-sleeping-carp-name + description: uplink-sleeping-carp-desc + productEntity: SleepingCarpScroll + cost: + Telecrystal: 70 + categories: + - UplinkWeaponry + conditions: + - !type:StoreWhitelistCondition + blacklist: + tags: + - NukeOpsUplink diff --git a/Resources/Prototypes/_Goobstation/Entities/Clothing/Belt/belts.yml b/Resources/Prototypes/_Goobstation/Entities/Clothing/Belt/belts.yml new file mode 100644 index 00000000000..773e9863e9d --- /dev/null +++ b/Resources/Prototypes/_Goobstation/Entities/Clothing/Belt/belts.yml @@ -0,0 +1,33 @@ +- type: entity + parent: ClothingBeltSecurity + id: ClothingBeltCorporateJudo + name: Corporate Judo Belt + description: Teaches the wearer NT Corporate Judo. + components: + - type: Sprite + sprite: _Goobstation/Clothing/Belt/judobelt.rsi + - type: Clothing + sprite: _Goobstation/Clothing/Belt/judobelt.rsi + - type: GrantCorporateJudo + - type: Storage + whitelist: + tags: + - CigPack + - Taser + - SecBeltEquip + - Radio + - Sidearm + - MagazinePistol + - MagazineMagnum + - CombatKnife + - Truncheon + components: + - FlashOnTrigger + - SmokeOnTrigger + - Flash + - Handcuff + - BallisticAmmoProvider + - CartridgeAmmo + - DoorRemote + - Whistle + - BalloonPopper diff --git a/Resources/Prototypes/_Goobstation/Entities/Objects/Weapons/Melee/kravmagagloves.yml b/Resources/Prototypes/_Goobstation/Entities/Objects/Weapons/Melee/kravmagagloves.yml new file mode 100644 index 00000000000..bebe1b0aa90 --- /dev/null +++ b/Resources/Prototypes/_Goobstation/Entities/Objects/Weapons/Melee/kravmagagloves.yml @@ -0,0 +1,16 @@ +- type: entity + parent: ClothingHandsBase + id: ClothingHandsGlovesKravMaga + name: Krav Maga gloves + description: These gloves can teach you to perform Krav Maga using nanochips for as long as you're wearing them. + components: + - type: Sprite + sprite: _Goobstation/Clothing/Hands/Gloves/kravgloves.rsi + - type: Item + - type: Clothing + - type: Fiber + fiberMaterial: fibers-leather + fiberColor: fibers-red + - type: ClothingGrantComponent + component: + - type: KravMaga diff --git a/Resources/Prototypes/_Goobstation/Entities/Specific/syndicate.yml b/Resources/Prototypes/_Goobstation/Entities/Specific/syndicate.yml new file mode 100644 index 00000000000..5789bb1aa25 --- /dev/null +++ b/Resources/Prototypes/_Goobstation/Entities/Specific/syndicate.yml @@ -0,0 +1,32 @@ +- type: entity + parent: [BaseItem, BaseSyndicateContraband] + id: CQCManual + name: old manual + description: A small, black manual. There are drawn instructions of tactical hand-to-hand combat. + components: + - type: Sprite + sprite: _Goobstation/Objects/Misc/cqc_manual.rsi + layers: + - state: icon + - type: Item + size: Small + - type: StaticPrice + price: 3000 + - type: GrantCqc + +- type: entity + parent: BaseItem + id: SleepingCarpScroll + name: mysterious scroll + description: A scroll filled with strange markings. It seems to be drawings of some sort of martial art. + components: + - type: Sprite + sprite: _Goobstation/Wizard/Objects/scroll.rsi + layers: + - state: scroll2 + - type: Item + sprite: _Goobstation/Wizard/Objects/scroll.rsi + size: Small + - type: StaticPrice + price: 3000 + - type: GrantSleepingCarp diff --git a/Resources/Prototypes/_Goobstation/MartialArts/carp.yml b/Resources/Prototypes/_Goobstation/MartialArts/carp.yml new file mode 100644 index 00000000000..bc8775ebc74 --- /dev/null +++ b/Resources/Prototypes/_Goobstation/MartialArts/carp.yml @@ -0,0 +1,64 @@ +- type: martialArt + id: SleepingCarp + martialArtsForm: SleepingCarp + roundstartCombos: SleepingCarpMoves + randomDamageModifier: True + baseDamageModifier: 5 + randomSayings: + - carp-saying-huah + - carv-vaying-hya + - carp-saying-choo + - carp-saying-wuo + - carp-saying-kya + - carp-saying-huh + - carp-saying-hiyoh + - carp-saying-strike + - carp-saying-bite + randomSayingsDowned: + - carp-saying-banzai + - carp-saying-kiya + - carp-saying-omae + - carp-saying-see + - carp-saying-time + - carp-saying-cowabunga + +- type: comboList + id: SleepingCarpMoves + combos: + - SleepingCarpGnashingTeeth + - SleepingCarpKneeHaul + - SleepingCarpCrashingWaves + +- type: combo + id: SleepingCarpGnashingTeeth + name: Gnashing Teeth + martialArtsForm: SleepingCarp + attacks: + - Harm + - Harm + event: !type:SleepingCarpGnashingTeethPerformedEvent + damageType: Slash + extraDamage: 20 + +- type: combo + id: SleepingCarpKneeHaul + name: Knee Haul + martialArtsForm: SleepingCarp + attacks: + - Harm + - Grab + event: !type:SleepingCarpKneeHaulPerformedEvent + extraDamage: 10 + paralyzeTime: 6 + staminaDamage: 60 + +- type: combo + id: SleepingCarpCrashingWaves + name: Crashing Waves + martialArtsForm: SleepingCarp + attacks: + - Harm + - Disarm + event: !type:SleepingCarpCrashingWavesPerformedEvent + staminaDamage: 25 + diff --git a/Resources/Prototypes/_Goobstation/MartialArts/cqc.yml b/Resources/Prototypes/_Goobstation/MartialArts/cqc.yml new file mode 100644 index 00000000000..c78ecdd5c5d --- /dev/null +++ b/Resources/Prototypes/_Goobstation/MartialArts/cqc.yml @@ -0,0 +1,69 @@ +- type: martialArt + id: CloseQuartersCombat + martialArtsForm: CloseQuartersCombat + roundstartCombos: CQCMoves + +- type: comboList + id: CQCMoves + combos: + - CQCConsecutive + - CQCPressure + - CQCRestrain + - CQCKick + - CQCSlam + +- type: combo + id: CQCSlam + name: Slam + martialArtsForm: CloseQuartersCombat + attacks: + - Grab + - Harm + event: !type:CqcSlamPerformedEvent { } + extraDamage: 10 # + paralyzeTime: 12 # in seconds + + +- type: combo + id: CQCKick + name: Kick + martialArtsForm: CloseQuartersCombat + attacks: + - Harm + - Harm + event: !type:CqcKickPerformedEvent + extraDamage: 10 + staminaDamage: 25 + +- type: combo + id: CQCRestrain + name: Restrain + martialArtsForm: CloseQuartersCombat + attacks: + - Grab + - Grab + event: !type:CqcRestrainPerformedEvent + paralyzeTime: 10 + staminaDamage: 30 + +- type: combo + id: CQCPressure + name: Pressure + martialArtsForm: CloseQuartersCombat + attacks: + - Disarm + - Grab + event: !type:CqcPressurePerformedEvent + staminaDamage: 65 + +- type: combo + id: CQCConsecutive + name: Consecutive + martialArtsForm: CloseQuartersCombat + attacks: + - Disarm + - Disarm + - Harm + event: !type:CqcConsecutivePerformedEvent + extraDamage: 10 + staminaDamage: 70 diff --git a/Resources/Prototypes/_Goobstation/MartialArts/judo.yml b/Resources/Prototypes/_Goobstation/MartialArts/judo.yml new file mode 100644 index 00000000000..7e1b980e7ac --- /dev/null +++ b/Resources/Prototypes/_Goobstation/MartialArts/judo.yml @@ -0,0 +1,45 @@ +- type: martialArt + id: CorporateJudo + martialArtsForm: CorporateJudo + roundstartCombos: CorporateJudoMoves + baseDamageModifier: 5 + +- type: comboList + id: CorporateJudoMoves + combos: + - JudoThrow + - JudoEyepoke + - JudoArmbar + +- type: combo + id: JudoThrow + name: Judo Throw + martialArtsForm: CorporateJudo + attacks: + - Grab + - Disarm + event: !type:JudoThrowPerformedEvent + staminaDamage: 30 + paralyzeTime: 3 + +- type: combo + id: JudoArmbar + name: Armbar + martialArtsForm: CorporateJudo + attacks: + - Disarm + - Disarm + - Grab + event: !type:JudoArmbarPerformedEvent + staminaDamage: 70 + paralyzeTime: 7 + +- type: combo + id: JudoEyepoke + name: Eyepoke + martialArtsForm: CorporateJudo + attacks: + - Disarm + - Harm + event: !type:JudoEyePokePerformedEvent + extraDamage: 5 diff --git a/Resources/Prototypes/_Goobstation/MartialArts/kravmaga.yml b/Resources/Prototypes/_Goobstation/MartialArts/kravmaga.yml new file mode 100644 index 00000000000..9998ce727a0 --- /dev/null +++ b/Resources/Prototypes/_Goobstation/MartialArts/kravmaga.yml @@ -0,0 +1,50 @@ +- type: entity + id: ActionLegSweep + name: Leg Sweep + description: Sweeps the legs out from under a target to knock them down for a few seconds. + categories: [ HideSpawnMenu ] + components: + - type: InstantAction + icon: + sprite: _Goobstation/Actions/kravmaga.rsi + state: legsweep + useDelay: 3 + event: !type:KravMagaActionEvent { } + - type: KravMagaAction + configuration: LegSweep + name: Leg Sweep + +- type: entity + id: ActionNeckChop + name: Neck Chop + description: A hard swing with the side of your hand deals some damage and disables the target's ability to speak for twenty seconds + categories: [ HideSpawnMenu ] + components: + - type: InstantAction + icon: + sprite: _Goobstation/Actions/kravmaga.rsi + state: neckchop + useDelay: 3 + event: !type:KravMagaActionEvent { } + - type: KravMagaAction + configuration: NeckChop + name: Neck Chop + effectTime: 20 + +- type: entity + id: ActionLungPunch + name: Lung Punch + description: Deals 30 stamina damage and prevents breathing for ten seconds. Four of those will incapacitate a target less-than-lethally, but beware of the loss of breath you cause. + categories: [ HideSpawnMenu ] + components: + - type: InstantAction + icon: + sprite: _Goobstation/Actions/kravmaga.rsi + state: lungpunch + useDelay: 3 + event: !type:KravMagaActionEvent { } + - type: KravMagaAction + configuration: LungPunch + name: Lung Punch + staminaDamage: 40 + effectTime: 10 diff --git a/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/legsweep.png b/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/legsweep.png new file mode 100644 index 00000000000..33c3f611c91 Binary files /dev/null and b/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/legsweep.png differ diff --git a/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/lungpunch.png b/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/lungpunch.png new file mode 100644 index 00000000000..047aed43095 Binary files /dev/null and b/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/lungpunch.png differ diff --git a/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/meta.json b/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/meta.json new file mode 100644 index 00000000000..31cbad53ff3 --- /dev/null +++ b/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/meta.json @@ -0,0 +1,20 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from Paradise at https://github.com/ParadiseSS13/Paradise/blob/a5726efe0c300662cb6f3a4a12d9151aa3eabe78/icons/mob/actions/actions.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "legsweep" + }, + { + "name": "lungpunch" + }, + { + "name": "neckchop" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/neckchop.png b/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/neckchop.png new file mode 100644 index 00000000000..8ef2373bbec Binary files /dev/null and b/Resources/Textures/_Goobstation/Actions/kravmaga.rsi/neckchop.png differ diff --git a/Resources/Textures/_Goobstation/Clothing/Belt/judobelt.rsi/equipped-BELT.png b/Resources/Textures/_Goobstation/Clothing/Belt/judobelt.rsi/equipped-BELT.png new file mode 100644 index 00000000000..09935e7c09f Binary files /dev/null and b/Resources/Textures/_Goobstation/Clothing/Belt/judobelt.rsi/equipped-BELT.png differ diff --git a/Resources/Textures/_Goobstation/Clothing/Belt/judobelt.rsi/icon.png b/Resources/Textures/_Goobstation/Clothing/Belt/judobelt.rsi/icon.png new file mode 100644 index 00000000000..29754499861 Binary files /dev/null and b/Resources/Textures/_Goobstation/Clothing/Belt/judobelt.rsi/icon.png differ diff --git a/Resources/Textures/_Goobstation/Clothing/Belt/judobelt.rsi/meta.json b/Resources/Textures/_Goobstation/Clothing/Belt/judobelt.rsi/meta.json new file mode 100644 index 00000000000..86066df5282 --- /dev/null +++ b/Resources/Textures/_Goobstation/Clothing/Belt/judobelt.rsi/meta.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from Paradise at https://github.com/ParadiseSS13/Paradise/blob/7da09f5e7230369ec48d9e5d31bdddb3f95a6d8f/icons/mob/clothing/belt.dmi, https://github.com/ParadiseSS13/Paradise/blob/7da09f5e7230369ec48d9e5d31bdddb3f95a6d8f/icons/obj/clothing/belts.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "equipped-BELT", + "directions": 4 + }, + { + "name": "icon" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/_Goobstation/Clothing/Hands/Gloves/kravgloves.rsi/equipped-HAND.png b/Resources/Textures/_Goobstation/Clothing/Hands/Gloves/kravgloves.rsi/equipped-HAND.png new file mode 100644 index 00000000000..6d4e42c4cd8 Binary files /dev/null and b/Resources/Textures/_Goobstation/Clothing/Hands/Gloves/kravgloves.rsi/equipped-HAND.png differ diff --git a/Resources/Textures/_Goobstation/Clothing/Hands/Gloves/kravgloves.rsi/icon.png b/Resources/Textures/_Goobstation/Clothing/Hands/Gloves/kravgloves.rsi/icon.png new file mode 100644 index 00000000000..78a7b892d11 Binary files /dev/null and b/Resources/Textures/_Goobstation/Clothing/Hands/Gloves/kravgloves.rsi/icon.png differ diff --git a/Resources/Textures/_Goobstation/Clothing/Hands/Gloves/kravgloves.rsi/meta.json b/Resources/Textures/_Goobstation/Clothing/Hands/Gloves/kravgloves.rsi/meta.json new file mode 100644 index 00000000000..28854c9bcda --- /dev/null +++ b/Resources/Textures/_Goobstation/Clothing/Hands/Gloves/kravgloves.rsi/meta.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from Paradise at https://github.com/ParadiseSS13/Paradise/blob/bd91a1b962fe9bd38e346e9fafd4ebb10784fcb3/icons/mob/clothing/hands.dmi and https://github.com/ParadiseSS13/Paradise/blob/bd91a1b962fe9bd38e346e9fafd4ebb10784fcb3/icons/obj/clothing/gloves.dmi and https://github.com/ParadiseSS13/Paradise/blob/HEAD/icons/mob/clothing/hands.dmi ", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-HAND", + "directions": 4 + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/_Goobstation/Objects/Misc/cqc_manual.rsi/icon-alt.png b/Resources/Textures/_Goobstation/Objects/Misc/cqc_manual.rsi/icon-alt.png new file mode 100644 index 00000000000..f01ed13e7ef Binary files /dev/null and b/Resources/Textures/_Goobstation/Objects/Misc/cqc_manual.rsi/icon-alt.png differ diff --git a/Resources/Textures/_Goobstation/Objects/Misc/cqc_manual.rsi/icon.png b/Resources/Textures/_Goobstation/Objects/Misc/cqc_manual.rsi/icon.png new file mode 100644 index 00000000000..059c565fc5b Binary files /dev/null and b/Resources/Textures/_Goobstation/Objects/Misc/cqc_manual.rsi/icon.png differ diff --git a/Resources/Textures/_Goobstation/Objects/Misc/cqc_manual.rsi/meta.json b/Resources/Textures/_Goobstation/Objects/Misc/cqc_manual.rsi/meta.json new file mode 100644 index 00000000000..0371deee2a5 --- /dev/null +++ b/Resources/Textures/_Goobstation/Objects/Misc/cqc_manual.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Sprite taken at: https://github.com/tgstation/tgstation", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "icon-alt" + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/equipped-BELT.png b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/equipped-BELT.png new file mode 100644 index 00000000000..9dc2b59ac87 Binary files /dev/null and b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/equipped-BELT.png differ diff --git a/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/inhand-left.png b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/inhand-left.png new file mode 100644 index 00000000000..ed1902af960 Binary files /dev/null and b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/inhand-left.png differ diff --git a/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/inhand-right.png b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/inhand-right.png new file mode 100644 index 00000000000..a1db789ce4f Binary files /dev/null and b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/inhand-right.png differ diff --git a/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/meta.json b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/meta.json new file mode 100644 index 00000000000..098f421f0dd --- /dev/null +++ b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/meta.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/80ab61c8c7741e1d95e5f168357a9e6e61b38f2c", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "scroll" + }, + { + "name": "scroll2" + }, + { + "name": "equipped-BELT", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/scroll.png b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/scroll.png new file mode 100644 index 00000000000..e7780ff2caf Binary files /dev/null and b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/scroll.png differ diff --git a/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/scroll2.png b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/scroll2.png new file mode 100644 index 00000000000..2f00a234018 Binary files /dev/null and b/Resources/Textures/_Goobstation/Wizard/Objects/scroll.rsi/scroll2.png differ diff --git a/Resources/Textures/_White/Interface/Alerts/pull.rsi/grab-choke.png b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grab-choke.png new file mode 100644 index 00000000000..085c7c389c8 Binary files /dev/null and b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grab-choke.png differ diff --git a/Resources/Textures/_White/Interface/Alerts/pull.rsi/grab-hard.png b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grab-hard.png new file mode 100644 index 00000000000..1e703705845 Binary files /dev/null and b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grab-hard.png differ diff --git a/Resources/Textures/_White/Interface/Alerts/pull.rsi/grab-soft.png b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grab-soft.png new file mode 100644 index 00000000000..03877d2c1f6 Binary files /dev/null and b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grab-soft.png differ diff --git a/Resources/Textures/_White/Interface/Alerts/pull.rsi/grabbed-choke.png b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grabbed-choke.png new file mode 100644 index 00000000000..2b0932f5af0 Binary files /dev/null and b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grabbed-choke.png differ diff --git a/Resources/Textures/_White/Interface/Alerts/pull.rsi/grabbed-hard.png b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grabbed-hard.png new file mode 100644 index 00000000000..4d7d6a21443 Binary files /dev/null and b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grabbed-hard.png differ diff --git a/Resources/Textures/_White/Interface/Alerts/pull.rsi/grabbed-soft.png b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grabbed-soft.png new file mode 100644 index 00000000000..c0b7e8f7ae6 Binary files /dev/null and b/Resources/Textures/_White/Interface/Alerts/pull.rsi/grabbed-soft.png differ diff --git a/Resources/Textures/_White/Interface/Alerts/pull.rsi/meta.json b/Resources/Textures/_White/Interface/Alerts/pull.rsi/meta.json new file mode 100644 index 00000000000..95570cc9d4a --- /dev/null +++ b/Resources/Textures/_White/Interface/Alerts/pull.rsi/meta.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Original taken from https://github.com/space-wizards/space-station-14, grab icons edited by _kote", + "states": [ + { + "name": "pulled", + "directions": 1 + }, + { + "name": "pulling", + "directions": 1 + }, + { + "name": "grab-soft", + "directions": 1 + }, + { + "name": "grab-hard", + "directions": 1 + }, + { + "name": "grabbed-soft", + "directions": 1 + }, + { + "name": "grabbed-hard", + "directions": 1 + }, + { + "name": "grab-choke", + "directions": 1, + "delays": [ [ 0.5, 0.5 ] ] + }, + { + "name": "grabbed-choke", + "directions": 1, + "delays": [ [ 0.5, 0.5 ] ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/_White/Interface/Alerts/pull.rsi/pulled.png b/Resources/Textures/_White/Interface/Alerts/pull.rsi/pulled.png new file mode 100644 index 00000000000..9c148fe4595 Binary files /dev/null and b/Resources/Textures/_White/Interface/Alerts/pull.rsi/pulled.png differ diff --git a/Resources/Textures/_White/Interface/Alerts/pull.rsi/pulling.png b/Resources/Textures/_White/Interface/Alerts/pull.rsi/pulling.png new file mode 100644 index 00000000000..d57f12cd63a Binary files /dev/null and b/Resources/Textures/_White/Interface/Alerts/pull.rsi/pulling.png differ