]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Crawling Part 1: The Knockdownening (#36881)
authorPrincess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
Sat, 19 Jul 2025 23:54:42 +0000 (16:54 -0700)
committerGitHub <noreply@github.com>
Sat, 19 Jul 2025 23:54:42 +0000 (01:54 +0200)
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
60 files changed:
Content.Client/Input/ContentContexts.cs
Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
Content.Client/Stunnable/StunSystem.cs
Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
Content.Server/Fluids/EntitySystems/PuddleSystem.cs
Content.Server/Stunnable/Components/StunOnCollideComponent.cs
Content.Server/Stunnable/StunSystem.cs [new file with mode: 0644]
Content.Server/Stunnable/Systems/StunOnCollideSystem.cs
Content.Server/Stunnable/Systems/StunSystem.cs [deleted file]
Content.Shared/Bed/Sleep/SleepingSystem.cs
Content.Shared/Cuffs/Components/HandcuffComponent.cs
Content.Shared/Cuffs/SharedCuffableSystem.cs
Content.Shared/Damage/Components/StaminaComponent.cs
Content.Shared/Damage/Systems/SharedStaminaSystem.cs
Content.Shared/Hands/EntitySystems/SharedHandsSystem.EventListeners.cs [new file with mode: 0644]
Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs
Content.Shared/Input/ContentKeyFunctions.cs
Content.Shared/Movement/Components/FrictionStatusEffectComponent.cs [new file with mode: 0644]
Content.Shared/Movement/Components/WormComponent.cs [new file with mode: 0644]
Content.Shared/Movement/Systems/MovementModStatusSystem.cs [new file with mode: 0644]
Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs
Content.Shared/Movement/Systems/WormSystem.cs [new file with mode: 0644]
Content.Shared/Slippery/SlidingComponent.cs
Content.Shared/Slippery/SlidingSystem.cs
Content.Shared/Slippery/SlipperyComponent.cs
Content.Shared/Slippery/SlipperySystem.cs
Content.Shared/Standing/StandingStateComponent.cs
Content.Shared/Standing/StandingStateSystem.cs
Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs
Content.Shared/Stunnable/KnockedDownComponent.cs
Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs [new file with mode: 0644]
Content.Shared/Stunnable/SharedStunSystem.cs
Content.Shared/Stunnable/StunOnContactComponent.cs
Content.Shared/Stunnable/StunnableEvents.cs [new file with mode: 0644]
Content.Shared/Throwing/ThrowAttemptEvent.cs
Content.Shared/Throwing/ThrowingSystem.cs
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
Resources/Locale/en-US/administration/smites.ftl
Resources/Locale/en-US/alerts/alerts.ftl
Resources/Locale/en-US/escape-menu/ui/options-menu.ftl
Resources/Locale/en-US/stunnable/components/stunnable-component.ftl
Resources/Prototypes/Alerts/alerts.yml
Resources/Prototypes/Entities/Effects/puddle.yml
Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml
Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml
Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml
Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
Resources/Prototypes/Entities/Mobs/Species/base.yml
Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml
Resources/Prototypes/Entities/Objects/Fun/error.yml
Resources/Prototypes/Entities/Objects/Fun/toys.yml
Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
Resources/Prototypes/Entities/StatusEffects/misc.yml
Resources/Prototypes/Reagents/cleaning.yml
Resources/Prototypes/status_effects.yml
Resources/Textures/Interface/Alerts/stunnable.rsi/knocked-down.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/stunnable.rsi/meta.json [new file with mode: 0644]
Resources/keybinds.yml

index 5b7614780b6ebc23ede4a67f709a7ea85e0f8575..6b279cb12c2dbfab3483e9d63331ea8f7415d1b1 100644 (file)
@@ -55,6 +55,7 @@ namespace Content.Client.Input
             human.AddFunction(EngineKeyFunctions.MoveLeft);
             human.AddFunction(EngineKeyFunctions.MoveRight);
             human.AddFunction(EngineKeyFunctions.Walk);
+            human.AddFunction(ContentKeyFunctions.ToggleKnockdown);
             human.AddFunction(ContentKeyFunctions.SwapHands);
             human.AddFunction(ContentKeyFunctions.SwapHandsReverse);
             human.AddFunction(ContentKeyFunctions.Drop);
index 9ef3def541870702b4808545923353b3448cac23..ee72a91fb7bc1fd700f52fcc8ce1aa80813987d8 100644 (file)
@@ -162,6 +162,7 @@ namespace Content.Client.Options.UI.Tabs
             AddButton(EngineKeyFunctions.Walk);
             AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk);
             InitToggleWalk();
+            AddButton(ContentKeyFunctions.ToggleKnockdown);
 
             AddHeader("ui-options-header-camera");
             AddButton(EngineKeyFunctions.CameraRotateLeft);
index cb59eda4824bfbfadcb3c60021fd726d1ebdbf2d..6947b80664b0025215300e3e9d70bae50243c2f6 100644 (file)
@@ -1,17 +1,19 @@
-using System.Numerics;
-using Content.Shared.Mobs;
+using System.Numerics;
+using Content.Shared.CombatMode;
+using Content.Shared.Interaction;
 using Content.Shared.Stunnable;
 using Robust.Client.Animations;
 using Robust.Client.GameObjects;
 using Robust.Shared.Animations;
+using Robust.Shared.Input;
+using Robust.Shared.Input.Binding;
 using Robust.Shared.Random;
-using Robust.Shared.Timing;
-using Robust.Shared.Utility;
 
 namespace Content.Client.Stunnable;
 
 public sealed class StunSystem : SharedStunSystem
 {
+    [Dependency] private readonly SharedCombatModeSystem _combat = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly SpriteSystem _spriteSystem = default!;
 
@@ -23,6 +25,22 @@ public sealed class StunSystem : SharedStunSystem
 
         SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
         SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged);
+
+        CommandBinds.Builder
+            .BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true), typeof(SharedInteractionSystem))
+            .Register<StunSystem>();
+    }
+
+    private bool OnUseSecondary(in PointerInputCmdHandler.PointerInputCmdArgs args)
+    {
+        if (args.Session?.AttachedEntity is not {Valid: true} uid)
+            return false;
+
+        if (args.EntityUid != uid || !HasComp<KnockedDownComponent>(uid) || !_combat.IsInCombatMode(uid))
+            return false;
+
+        RaisePredictiveEvent(new ForceStandUpEvent());
+        return true;
     }
 
     /// <summary>
index b764c7f68d110f98a01fd9c2bf0d20352c5004f6..1bc4b659998da49dcd17e4bdf1e796b20385e8b3 100644 (file)
@@ -39,6 +39,7 @@ using Content.Shared.Movement.Systems;
 using Content.Shared.Nutrition.Components;
 using Content.Shared.Popups;
 using Content.Shared.Slippery;
+using Content.Shared.Stunnable;
 using Content.Shared.Tabletop.Components;
 using Content.Shared.Tools.Systems;
 using Content.Shared.Verbs;
@@ -48,6 +49,7 @@ using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Systems;
 using Robust.Shared.Player;
 using Robust.Shared.Random;
+using Robust.Shared.Timing;
 using Robust.Shared.Utility;
 using Timer = Robust.Shared.Timing.Timer;
 
@@ -877,7 +879,7 @@ public sealed partial class AdminVerbSystem
                 if (!hadSlipComponent)
                 {
                     slipComponent.SlipData.SuperSlippery = true;
-                    slipComponent.SlipData.ParalyzeTime = TimeSpan.FromSeconds(5);
+                    slipComponent.SlipData.StunTime = TimeSpan.FromSeconds(5);
                     slipComponent.SlipData.LaunchForwardsMultiplier = 20;
                 }
 
@@ -922,5 +924,20 @@ public sealed partial class AdminVerbSystem
             Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
         };
         args.Verbs.Add(omniaccent);
+
+        var crawlerName = Loc.GetString("admin-smite-crawler-name").ToLowerInvariant();
+        Verb crawler = new()
+        {
+            Text = crawlerName,
+            Category = VerbCategory.Smite,
+            Icon = new SpriteSpecifier.Rsi(new("Mobs/Animals/snake.rsi"), "icon"),
+            Act = () =>
+            {
+                EnsureComp<WormComponent>(args.Target);
+            },
+            Impact = LogImpact.Extreme,
+            Message = string.Join(": ", crawlerName, Loc.GetString("admin-smite-crawler-description"))
+        };
+        args.Verbs.Add(crawler);
     }
 }
index 1176ce54c75f57ab3fe217fd3cde6d5256c9822a..63ee75618c8ace683221202b122deba157602670 100644 (file)
@@ -338,6 +338,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         // Ensure we actually have the component
         EnsureComp<TileFrictionModifierComponent>(entity);
 
+        EnsureComp<SlipperyComponent>(entity, out var slipComp);
+
         // This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
         // the sprite threshold for a puddle larger than 5 pixels.
         var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
@@ -356,17 +358,21 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         var launchMult = FixedPoint2.Zero;
         // A cumulative weighted amount of stun times from slippery reagents
         var stunTimer = TimeSpan.Zero;
+        // A cumulative weighted amount of knockdown times from slippery reagents
+        var knockdownTimer = TimeSpan.Zero;
 
         // Check if the puddle is big enough to slip in to avoid doing unnecessary logic
         if (solution.Volume <= smallPuddleThreshold)
         {
             _stepTrigger.SetActive(entity, false, comp);
             _tile.SetModifier(entity, 1f);
+            slipComp.SlipData.SlipFriction = 1f;
+            slipComp.AffectsSliding = false;
+            Dirty(entity, slipComp);
             return;
         }
 
-        if (!TryComp<SlipperyComponent>(entity, out var slipComp))
-            return;
+        slipComp.AffectsSliding = true;
 
         foreach (var (reagent, quantity) in solution.Contents)
         {
@@ -386,7 +392,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
             // Aggregate launch speed based on quantity
             launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
             // Aggregate stun times based on quantity
-            stunTimer += reagentProto.SlipData.ParalyzeTime * (float)quantity;
+            stunTimer += reagentProto.SlipData.StunTime * (float)quantity;
+            knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity;
 
             if (reagentProto.SlipData.SuperSlippery)
                 superSlipperyUnits += quantity;
@@ -404,8 +411,9 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
         // A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount.
         if (slipperyUnits > 0)
         {
-            slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits);
-            slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits;
+            slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
+            slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits);
+            slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits);
         }
 
         // Only make it super slippery if there is enough super slippery units for its own puddle
index 1ce1cbea5752be866fc783118e45f67295659cea..363fb78b750fd6e0a544000fadd867951194a7ae 100644 (file)
@@ -8,21 +8,47 @@ namespace Content.Server.Stunnable.Components
     {
         // TODO: Can probably predict this.
 
-        // See stunsystem for what these do
-        [DataField("stunAmount")]
-        public int StunAmount;
+        /// <summary>
+        /// How long we are stunned for
+        /// </summary>
+        [DataField]
+        public TimeSpan StunAmount;
+
+        /// <summary>
+        /// How long we are knocked down for
+        /// </summary>
+        [DataField]
+        public TimeSpan KnockdownAmount;
+
+        /// <summary>
+        /// How long we are slowed down for
+        /// </summary>
+        [DataField]
+        public TimeSpan SlowdownAmount;
 
-        [DataField("knockdownAmount")]
-        public int KnockdownAmount;
+        /// <summary>
+        /// Multiplier for a mob's walking speed
+        /// </summary>
+        [DataField]
+        public float WalkSpeedModifier = 1f;
 
-        [DataField("slowdownAmount")]
-        public int SlowdownAmount;
+        /// <summary>
+        /// Multiplier for a mob's sprinting speed
+        /// </summary>
+        [DataField]
+        public float SprintSpeedModifier = 1f;
 
-        [DataField("walkSpeedMultiplier")]
-        public float WalkSpeedMultiplier = 1f;
+        /// <summary>
+        /// Refresh Stun or Slowdown on hit
+        /// </summary>
+        [DataField]
+        public bool Refresh = true;
 
-        [DataField("runSpeedMultiplier")]
-        public float RunSpeedMultiplier = 1f;
+        /// <summary>
+        /// Should the entity try and stand automatically after being knocked down?
+        /// </summary>
+        [DataField]
+        public bool AutoStand = true;
 
         /// <summary>
         /// Fixture we track for the collision.
diff --git a/Content.Server/Stunnable/StunSystem.cs b/Content.Server/Stunnable/StunSystem.cs
new file mode 100644 (file)
index 0000000..5637ad5
--- /dev/null
@@ -0,0 +1,6 @@
+using Content.Shared.Stunnable;
+
+namespace Content.Server.Stunnable;
+
+public sealed class StunSystem : SharedStunSystem;
+
index ae10957bd8e34d3ea4028b68326f5851668cd516..b998270829bb80cde89e120ed6d25e031c2aec22 100644 (file)
@@ -22,17 +22,14 @@ namespace Content.Server.Stunnable
 
         private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target)
         {
+            if (!TryComp<StatusEffectsComponent>(target, out var status))
+                return;
 
-            if (TryComp<StatusEffectsComponent>(target, out var status))
-            {
-                _stunSystem.TryStun(target, TimeSpan.FromSeconds(component.StunAmount), true, status);
+            _stunSystem.TryStun(target, component.StunAmount, component.Refresh, status);
 
-                _stunSystem.TryKnockdown(target, TimeSpan.FromSeconds(component.KnockdownAmount), true,
-                    status);
+            _stunSystem.TryKnockdown(target, component.KnockdownAmount, component.Refresh, component.AutoStand);
 
-                _stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(component.SlowdownAmount), true,
-                    component.WalkSpeedMultiplier, component.RunSpeedMultiplier, status);
-            }
+            _stunSystem.TrySlowdown(target, component.SlowdownAmount, component.Refresh, component.WalkSpeedModifier, component.SprintSpeedModifier, status);
         }
         private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args)
         {
diff --git a/Content.Server/Stunnable/Systems/StunSystem.cs b/Content.Server/Stunnable/Systems/StunSystem.cs
deleted file mode 100644 (file)
index 8f793a0..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-using Content.Shared.Stunnable;
-
-namespace Content.Server.Stunnable
-{
-    public sealed class StunSystem : SharedStunSystem
-    {}
-}
index cdff4b7fd702eee5cfa6d5919a3c6119e443b74c..3534fb88d5c6ac30d58ea618741b992c89cf4717 100644 (file)
@@ -108,7 +108,6 @@ public sealed partial class SleepingSystem : EntitySystem
         {
             // Expiring status effects would remove the components needed for sleeping
             _statusEffectOld.TryRemoveStatusEffect(ent.Owner, "Stun");
-            _statusEffectOld.TryRemoveStatusEffect(ent.Owner, "KnockedDown");
 
             EnsureComp<StunnedComponent>(ent);
             EnsureComp<KnockedDownComponent>(ent);
index 289f587239b5634ce79212b8901e14fc9db729b9..9465d605ed8238c68a57ecd1b404dd907057f2e1 100644 (file)
@@ -33,6 +33,24 @@ public sealed partial class HandcuffComponent : Component
     [DataField, ViewVariables(VVAccess.ReadWrite)]
     public float StunBonus = 2f;
 
+    /// <summary>
+    ///     Modifier for the amount of time it takes an entity to stand up if cuffed.
+    /// </summary>
+    [DataField]
+    public float StandupMod = 5f;
+
+    /// <summary>
+    ///     Modifier to the speed of an entity who is cuffed, does not stack with KnockedMovementMod
+    /// </summary>
+    [DataField]
+    public float MovementMod = 1f;
+
+    /// <summary>
+    ///     Modifier to the knocked down speed of an entity who is cuffed
+    /// </summary>
+    [DataField]
+    public float KnockedMovementMod = 0.4f;
+
     /// <summary>
     ///     Will the cuffs break when removed?
     /// </summary>
index 3b0f6c8a30cfe266fe64e2f14abff06908704c88..931028052cfa1ecdc86e5e9fb0735f6889f811be 100644 (file)
@@ -21,6 +21,7 @@ using Content.Shared.Inventory.VirtualItem;
 using Content.Shared.Item;
 using Content.Shared.Movement.Events;
 using Content.Shared.Movement.Pulling.Events;
+using Content.Shared.Movement.Systems;
 using Content.Shared.Popups;
 using Content.Shared.Pulling.Events;
 using Content.Shared.Rejuvenate;
@@ -45,6 +46,7 @@ namespace Content.Shared.Cuffs
         [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
         [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
         [Dependency] private readonly AlertsSystem _alerts = default!;
+        [Dependency] private readonly MovementSpeedModifierSystem _move = default!;
         [Dependency] private readonly SharedAudioSystem _audio = default!;
         [Dependency] private readonly SharedContainerSystem _container = default!;
         [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
@@ -56,10 +58,14 @@ namespace Content.Shared.Cuffs
         [Dependency] private readonly UseDelaySystem _delay = default!;
         [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
 
+        private EntityQuery<HandcuffComponent> _cuffQuery;
+
         public override void Initialize()
         {
             base.Initialize();
 
+            _cuffQuery = GetEntityQuery<HandcuffComponent>();
+
             SubscribeLocalEvent<CuffableComponent, HandCountChangedEvent>(OnHandCountChanged);
             SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
 
@@ -89,6 +95,9 @@ namespace Content.Shared.Cuffs
             SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
             SubscribeLocalEvent<HandcuffComponent, AddCuffDoAfterEvent>(OnAddCuffDoAfter);
             SubscribeLocalEvent<HandcuffComponent, VirtualItemDeletedEvent>(OnCuffVirtualItemDeleted);
+            SubscribeLocalEvent<CuffableComponent, GetStandUpTimeEvent>(OnCuffableStandupArgs);
+            SubscribeLocalEvent<CuffableComponent, KnockedDownRefreshEvent>(OnCuffableKnockdownRefresh);
+            SubscribeLocalEvent<CuffableComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
         }
 
         private void CheckInteract(Entity<CuffableComponent> ent, ref InteractionAttemptEvent args)
@@ -366,6 +375,9 @@ namespace Content.Shared.Cuffs
                     _adminLog.Add(LogType.Action, LogImpact.High,
                         $"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}");
                 }
+
+                if (!MathHelper.CloseTo(component.MovementMod, 1f))
+                    _move.RefreshMovementSpeedModifiers(target);
             }
             else
             {
@@ -420,6 +432,72 @@ namespace Content.Shared.Cuffs
             }
         }
 
+        /// <summary>
+        ///     Takes longer to stand up when cuffed
+        /// </summary>
+        private void OnCuffableStandupArgs(Entity<CuffableComponent> ent, ref GetStandUpTimeEvent time)
+        {
+            if (!HasComp<KnockedDownComponent>(ent) || !IsCuffed(ent))
+                return;
+
+            var cuffs = GetAllCuffs(ent.Comp);
+            var mod = 1f;
+
+            if (cuffs.Count == 0)
+                return;
+
+            foreach (var cuff in cuffs)
+            {
+                if (!_cuffQuery.TryComp(cuff, out var comp))
+                    continue;
+
+                // Get the worst modifier
+                mod = Math.Max(mod, comp.StandupMod);
+            }
+
+            time.DoAfterTime *= mod;
+        }
+
+        private void OnCuffableKnockdownRefresh(Entity<CuffableComponent> ent, ref KnockedDownRefreshEvent args)
+        {
+            var cuffs = GetAllCuffs(ent.Comp);
+            var mod = 1f;
+
+            if (cuffs.Count == 0)
+                return;
+
+            foreach (var cuff in cuffs)
+            {
+                if (!_cuffQuery.TryComp(cuff, out var comp))
+                    continue;
+
+                // Get the worst modifier
+                mod = Math.Min(mod, comp.KnockedMovementMod);
+            }
+
+            args.SpeedModifier *= mod;
+        }
+
+        private void OnRefreshMovementSpeedModifiers(Entity<CuffableComponent> ent, ref RefreshMovementSpeedModifiersEvent args)
+        {
+            var cuffs = GetAllCuffs(ent.Comp);
+            var mod = 1f;
+
+            if (cuffs.Count == 0)
+                return;
+
+            foreach (var cuff in cuffs)
+            {
+                if (!_cuffQuery.TryComp(cuff, out var comp))
+                    continue;
+
+                // Get the worst modifier
+                mod = Math.Min(mod, comp.MovementMod);
+            }
+
+            args.ModifySpeed(mod);
+        }
+
         /// <summary>
         ///     Adds virtual cuff items to the user's hands.
         /// </summary>
@@ -736,6 +814,9 @@ namespace Content.Shared.Cuffs
                 shoved = true;
             }
 
+            if (!MathHelper.CloseTo(cuff.MovementMod, 1f))
+                _move.RefreshMovementSpeedModifiers(target);
+
             if (cuffable.CuffedHandCount == 0)
             {
                 if (user != null)
index 3a85f3f8dc72e84589c4e8ea8b534036403474fe..b343b24403717c92e86a7bd21069765ed6296b9b 100644 (file)
@@ -1,6 +1,7 @@
 using System.Numerics;
 using Content.Shared.Alert;
 using Content.Shared.FixedPoint;
+using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -38,7 +39,7 @@ public sealed partial class StaminaComponent : Component
     public float StaminaDamage;
 
     /// <summary>
-    /// How much stamina damage is required to entire stam crit.
+    /// How much stamina damage is required to enter stam crit.
     /// </summary>
     [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField]
     public float CritThreshold = 100f;
@@ -71,6 +72,18 @@ public sealed partial class StaminaComponent : Component
     [DataField, AutoNetworkedField]
     public float AfterCritDecayMultiplier = 5f;
 
+    /// <summary>
+    /// This is how much stamina damage a mob takes when it forces itself to stand up before modifiers
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float ForceStandStamina = 10f;
+
+    /// <summary>
+    /// What sound should play when we successfully stand up
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier ForceStandSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
+
     /// <summary>
     /// Thresholds that determine an entity's slowdown as a function of stamina damage.
     /// </summary>
index ae4562e690e73e63c6d8f42658085ee07d7c6629..6687ecd7dff50054df3929b6d84a216f4386ec8e 100644 (file)
@@ -234,7 +234,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
     /// <summary>
     /// Tries to take stamina damage without raising the entity over the crit threshold.
     /// </summary>
-    public bool TryTakeStamina(EntityUid uid, float value, StaminaComponent? component = null, EntityUid? source = null, EntityUid? with = null)
+    public bool TryTakeStamina(EntityUid uid, float value, StaminaComponent? component = null, EntityUid? source = null, EntityUid? with = null, bool visual = false)
     {
         // Something that has no Stamina component automatically passes stamina checks
         if (!Resolve(uid, ref component, false))
@@ -242,10 +242,10 @@ public abstract partial class SharedStaminaSystem : EntitySystem
 
         var oldStam = component.StaminaDamage;
 
-        if (oldStam + value > component.CritThreshold || component.Critical)
+        if (oldStam + value >= component.CritThreshold || component.Critical)
             return false;
 
-        TakeStaminaDamage(uid, value, component, source, with, visual: false);
+        TakeStaminaDamage(uid, value, component, source, with, visual: visual);
         return true;
     }
 
diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.EventListeners.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.EventListeners.cs
new file mode 100644 (file)
index 0000000..af0ed1c
--- /dev/null
@@ -0,0 +1,31 @@
+using Content.Shared.Hands.Components;
+using Content.Shared.Stunnable;
+
+namespace Content.Shared.Hands.EntitySystems;
+
+/// <summary>
+/// This is for events that don't affect normal hand functions but do care about hands.
+/// </summary>
+public abstract partial class SharedHandsSystem
+{
+    private void InitializeEventListeners()
+    {
+        SubscribeLocalEvent<HandsComponent, GetStandUpTimeEvent>(OnStandupArgs);
+    }
+
+    /// <summary>
+    /// Reduces the time it takes to stand up based on the number of hands we have available.
+    /// </summary>
+    private void OnStandupArgs(Entity<HandsComponent> ent, ref GetStandUpTimeEvent time)
+    {
+        if (!HasComp<KnockedDownComponent>(ent))
+            return;
+
+        var hands = GetEmptyHandCount(ent.Owner);
+
+        if (hands == 0)
+            return;
+
+        time.DoAfterTime *= (float)ent.Comp.Count / (hands + ent.Comp.Count);
+    }
+}
index 4f259a3bdeb25969421d6b7c9c98e333a56bf144..af5e82f41710dc1cdbf96a48ddad2487c1341045 100644 (file)
@@ -36,6 +36,7 @@ public abstract partial class SharedHandsSystem
         InitializeDrop();
         InitializePickup();
         InitializeRelay();
+        InitializeEventListeners();
 
         SubscribeLocalEvent<HandsComponent, ComponentInit>(OnInit);
         SubscribeLocalEvent<HandsComponent, MapInitEvent>(OnMapInit);
@@ -166,6 +167,26 @@ public abstract partial class SharedHandsSystem
         return false;
     }
 
+    /// <summary>
+    ///     Does this entity have any empty hands, and how many?
+    /// </summary>
+    public int GetEmptyHandCount(Entity<HandsComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp, false) || entity.Comp.Count == 0)
+            return 0;
+
+        var hands = 0;
+
+        foreach (var hand in EnumerateHands(entity))
+        {
+            if (!HandIsEmpty(entity, hand))
+                continue;
+            hands++;
+        }
+
+        return hands;
+    }
+
     /// <summary>
     /// Attempts to retrieve the item held in the entity's active hand.
     /// </summary>
index f10c99af2fc9294666a4d4b0e4230364d1a86839..cd5f4cff7b9f27f6799966b8d1397eae92c40e96 100644 (file)
@@ -5,6 +5,7 @@ namespace Content.Shared.Input
     [KeyFunctions]
     public static class ContentKeyFunctions
     {
+        public static readonly BoundKeyFunction ToggleKnockdown = "ToggleKnockdown";
         public static readonly BoundKeyFunction UseItemInHand = "ActivateItemInHand";
         public static readonly BoundKeyFunction AltUseItemInHand = "AltActivateItemInHand";
         public static readonly BoundKeyFunction ActivateItemInWorld = "ActivateItemInWorld";
diff --git a/Content.Shared/Movement/Components/FrictionStatusEffectComponent.cs b/Content.Shared/Movement/Components/FrictionStatusEffectComponent.cs
new file mode 100644 (file)
index 0000000..33c5389
--- /dev/null
@@ -0,0 +1,23 @@
+using Content.Shared.Movement.Systems;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Movement.Components;
+
+/// <summary>
+/// This is used to apply a friction modifier to an entity temporarily
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(MovementModStatusSystem))]
+public sealed partial class FrictionStatusEffectComponent : Component
+{
+    /// <summary>
+    /// Friction modifier applied as a status.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float FrictionModifier = 1f;
+
+    /// <summary>
+    /// Acceleration modifier applied as a status.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float AccelerationModifier = 1f;
+}
diff --git a/Content.Shared/Movement/Components/WormComponent.cs b/Content.Shared/Movement/Components/WormComponent.cs
new file mode 100644 (file)
index 0000000..a717659
--- /dev/null
@@ -0,0 +1,23 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Movement.Components;
+
+/// <summary>
+///     This component ensures an entity is always in the KnockedDown State and cannot stand. Great for any entities you
+///     don't want to collide with other mobs, don't want eating projectiles and don't want to get knocked down.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class WormComponent : Component
+{
+    /// <summary>
+    /// Modifier for KnockedDown Friction, or in this components case, all friction
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float FrictionModifier = 1f;
+
+    /// <summary>
+    /// Modifier for KnockedDown Movement, or in this components case, all movement
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float SpeedModifier = 1f;
+}
diff --git a/Content.Shared/Movement/Systems/MovementModStatusSystem.cs b/Content.Shared/Movement/Systems/MovementModStatusSystem.cs
new file mode 100644 (file)
index 0000000..4fd6467
--- /dev/null
@@ -0,0 +1,100 @@
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Events;
+using Content.Shared.StatusEffectNew;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Movement.Systems;
+
+/// <summary>
+/// This handles the application of movement and friction modifiers to an entity as status effects.
+/// </summary>
+public sealed class MovementModStatusSystem : EntitySystem
+{
+    public static readonly EntProtoId StatusEffectFriction = "StatusEffectFriction";
+
+    [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
+    [Dependency] private readonly StatusEffectsSystem _status = default!;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<FrictionStatusEffectComponent, StatusEffectRemovedEvent>(OnFrictionStatusEffectRemoved);
+        SubscribeLocalEvent<FrictionStatusEffectComponent, StatusEffectRelayedEvent<RefreshFrictionModifiersEvent>>(OnRefreshFrictionStatus);
+        SubscribeLocalEvent<FrictionStatusEffectComponent, StatusEffectRelayedEvent<TileFrictionEvent>>(OnRefreshTileFrictionStatus);
+    }
+
+    private void OnRefreshFrictionStatus(Entity<FrictionStatusEffectComponent> ent, ref StatusEffectRelayedEvent<RefreshFrictionModifiersEvent> args)
+    {
+        var ev = args.Args;
+        ev.ModifyFriction(ent.Comp.FrictionModifier);
+        ev.ModifyAcceleration(ent.Comp.AccelerationModifier);
+        args.Args = ev;
+    }
+
+    private void OnRefreshTileFrictionStatus(Entity<FrictionStatusEffectComponent> ent, ref StatusEffectRelayedEvent<TileFrictionEvent> args)
+    {
+        var ev = args.Args;
+        ev.Modifier *= ent.Comp.FrictionModifier;
+        args.Args = ev;
+    }
+
+    /// <summary>
+    ///     Applies a friction de-buff to the player.
+    /// </summary>
+    public bool TryFriction(EntityUid uid,
+        TimeSpan time,
+        bool refresh,
+        float friction,
+        float acceleration)
+    {
+        if (time <= TimeSpan.Zero)
+            return false;
+
+        if (refresh)
+        {
+            return _status.TryUpdateStatusEffectDuration(uid, StatusEffectFriction, out var status, time)
+                   && TrySetFrictionStatus(status.Value, friction, acceleration, uid);
+        }
+        else
+        {
+            return _status.TryAddStatusEffectDuration(uid, StatusEffectFriction, out var status, time)
+                   && TrySetFrictionStatus(status.Value, friction, acceleration, uid);
+        }
+    }
+
+    /// <summary>
+    /// Sets the friction status modifiers for a status effect.
+    /// </summary>
+    /// <param name="status">The status effect entity we're modifying.</param>
+    /// <param name="friction">The friction modifier we're applying.</param>
+    /// <param name="entity">The entity the status effect is attached to that we need to refresh.</param>
+    private bool TrySetFrictionStatus(Entity<FrictionStatusEffectComponent?> status, float friction, EntityUid entity)
+    {
+        return TrySetFrictionStatus(status, friction, friction, entity);
+    }
+
+    /// <summary>
+    /// Sets the friction status modifiers for a status effect.
+    /// </summary>
+    /// <param name="status">The status effect entity we're modifying.</param>
+    /// <param name="friction">The friction modifier we're applying.</param>
+    /// <param name="acceleration">The acceleration modifier we're applying</param>
+    /// <param name="entity">The entity the status effect is attached to that we need to refresh.</param>
+    private bool TrySetFrictionStatus(Entity<FrictionStatusEffectComponent?> status, float friction, float acceleration, EntityUid entity)
+    {
+        if (!Resolve(status, ref status.Comp, false))
+            return false;
+
+        status.Comp.FrictionModifier = friction;
+        status.Comp.AccelerationModifier = acceleration;
+        Dirty(status);
+
+        _movementSpeedModifier.RefreshFrictionModifiers(entity);
+        return true;
+    }
+
+    private void OnFrictionStatusEffectRemoved(Entity<FrictionStatusEffectComponent> entity, ref StatusEffectRemovedEvent args)
+    {
+        TrySetFrictionStatus(entity!, 1f, args.Target);
+    }
+}
index b5bb633491b0a7e20ce1d0adadbca881f1ef0faa..4584e4401aba20589d51a5e5e9a811230dccf62e 100644 (file)
@@ -1,8 +1,7 @@
-using System.Text.Json.Serialization.Metadata;
 using Content.Shared.CCVar;
 using Content.Shared.Inventory;
 using Content.Shared.Movement.Components;
-using Content.Shared.Movement.Events;
+using Content.Shared.Standing;
 using Robust.Shared.Configuration;
 using Robust.Shared.Timing;
 
@@ -11,7 +10,7 @@ namespace Content.Shared.Movement.Systems
     public sealed class MovementSpeedModifierSystem : EntitySystem
     {
         [Dependency] private readonly IGameTiming _timing = default!;
-        [Dependency] private   readonly IConfigurationManager _configManager = default!;
+        [Dependency] private readonly IConfigurationManager _configManager = default!;
 
         private float _frictionModifier;
         private float _airDamping;
@@ -21,6 +20,8 @@ namespace Content.Shared.Movement.Systems
         {
             base.Initialize();
             SubscribeLocalEvent<MovementSpeedModifierComponent, MapInitEvent>(OnModMapInit);
+            SubscribeLocalEvent<MovementSpeedModifierComponent, DownedEvent>(OnDowned);
+            SubscribeLocalEvent<MovementSpeedModifierComponent, StoodEvent>(OnStand);
 
             Subs.CVar(_configManager, CCVars.TileFrictionModifier, value => _frictionModifier = value, true);
             Subs.CVar(_configManager, CCVars.AirFriction, value => _airDamping = value, true);
@@ -41,6 +42,18 @@ namespace Content.Shared.Movement.Systems
             Dirty(ent);
         }
 
+        private void OnDowned(Entity<MovementSpeedModifierComponent> entity, ref DownedEvent args)
+        {
+            RefreshFrictionModifiers(entity);
+            RefreshMovementSpeedModifiers(entity);
+        }
+
+        private void OnStand(Entity<MovementSpeedModifierComponent> entity, ref StoodEvent args)
+        {
+            RefreshFrictionModifiers(entity);
+            RefreshMovementSpeedModifiers(entity);
+        }
+
         public void RefreshWeightlessModifiers(EntityUid uid, MovementSpeedModifierComponent? move = null)
         {
             if (!Resolve(uid, ref move, false))
diff --git a/Content.Shared/Movement/Systems/WormSystem.cs b/Content.Shared/Movement/Systems/WormSystem.cs
new file mode 100644 (file)
index 0000000..8dc7f86
--- /dev/null
@@ -0,0 +1,53 @@
+using Content.Shared.Alert;
+using Content.Shared.Movement.Components;
+using Content.Shared.Popups;
+using Content.Shared.Rejuvenate;
+using Content.Shared.Stunnable;
+
+namespace Content.Shared.Movement.Systems;
+
+/// <summary>
+/// This handles the worm component
+/// </summary>
+public sealed class WormSystem : EntitySystem
+{
+    [Dependency] private readonly AlertsSystem _alerts = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedStunSystem _stun = default!;
+
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<WormComponent, StandUpAttemptEvent>(OnStandAttempt);
+        SubscribeLocalEvent<WormComponent, KnockedDownRefreshEvent>(OnKnockedDownRefresh);
+        SubscribeLocalEvent<WormComponent, RejuvenateEvent>(OnRejuvenate);
+        SubscribeLocalEvent<WormComponent, MapInitEvent>(OnMapInit);
+    }
+
+    private void OnMapInit(Entity<WormComponent> ent, ref MapInitEvent args)
+    {
+        EnsureComp<KnockedDownComponent>(ent, out var knocked);
+        _alerts.ShowAlert(ent, SharedStunSystem.KnockdownAlert);
+        _stun.SetAutoStand((ent, knocked));
+    }
+
+    private void OnRejuvenate(Entity<WormComponent> ent, ref RejuvenateEvent args)
+    {
+        RemComp<WormComponent>(ent);
+    }
+
+    private void OnStandAttempt(Entity<WormComponent> ent, ref StandUpAttemptEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        args.Cancelled = true;
+        args.Message = (Loc.GetString("worm-component-stand-attempt"), PopupType.SmallCaution);
+        args.Autostand = false;
+    }
+
+    private void OnKnockedDownRefresh(Entity<WormComponent> ent, ref KnockedDownRefreshEvent args)
+    {
+        args.FrictionModifier *= ent.Comp.FrictionModifier;
+        args.SpeedModifier *= ent.Comp.SpeedModifier;
+    }
+}
index e48c0f2e9ee271a84513e03312800acb90ca58f2..200d9570ab7e92beba9d4d5e894de87cc7782069 100644 (file)
@@ -9,14 +9,14 @@ namespace Content.Shared.Slippery;
 public sealed partial class SlidingComponent : Component
 {
     /// <summary>
-    ///     A list of SuperSlippery entities the entity with this component is colliding with.
+    ///     The friction modifier that will be applied to any friction calculations.
     /// </summary>
     [DataField, AutoNetworkedField]
-    public HashSet<EntityUid> CollidingEntities = new ();
+    public float FrictionModifier;
 
     /// <summary>
-    ///     The friction modifier that will be applied to any friction calculations.
+    /// Hashset of contacting entities.
     /// </summary>
-    [DataField, AutoNetworkedField]
-    public float FrictionModifier;
+    [DataField]
+    public HashSet<EntityUid> Contacting = new();
 }
index dde9bb6ab0c84fe2866f78dfdaa4cc7feeb1f1c4..e0084c11aba0ddcff4afe09e7b94e12aed92cff0 100644 (file)
@@ -1,19 +1,53 @@
-using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
 using Content.Shared.Standing;
-using Content.Shared.Stunnable;
+using Content.Shared.Throwing;
+using Content.Shared.Weapons.Ranged.Systems;
+using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Events;
+using Robust.Shared.Physics.Systems;
 
 namespace Content.Shared.Slippery;
 
 public sealed class SlidingSystem : EntitySystem
 {
+    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly MovementSpeedModifierSystem _speedModifierSystem = default!;
+
+    private EntityQuery<SlipperyComponent> _slipperyQuery;
+
     public override void Initialize()
     {
         base.Initialize();
 
+        _slipperyQuery = GetEntityQuery<SlipperyComponent>();
+
+        SubscribeLocalEvent<SlidingComponent, ComponentInit>(OnComponentInit);
+        SubscribeLocalEvent<SlidingComponent, ComponentShutdown>(OnComponentShutdown);
         SubscribeLocalEvent<SlidingComponent, StoodEvent>(OnStand);
         SubscribeLocalEvent<SlidingComponent, StartCollideEvent>(OnStartCollide);
         SubscribeLocalEvent<SlidingComponent, EndCollideEvent>(OnEndCollide);
+        SubscribeLocalEvent<SlidingComponent, RefreshFrictionModifiersEvent>(OnRefreshFrictionModifiers);
+        SubscribeLocalEvent<SlidingComponent, ThrowerImpulseEvent>(OnThrowerImpulse);
+        SubscribeLocalEvent<SlidingComponent, ShooterImpulseEvent>(ShooterImpulseEvent);
+    }
+
+    /// <summary>
+    ///     When the component is first added, calculate the friction modifier we need.
+    ///     Don't do this more than once to avoid mispredicts.
+    /// </summary>
+    private void OnComponentInit(Entity<SlidingComponent> entity, ref ComponentInit args)
+    {
+        if (CalculateSlidingModifier(entity))
+            _speedModifierSystem.RefreshFrictionModifiers(entity);
+    }
+
+    /// <summary>
+    ///     When the component is removed, refresh friction modifiers and set ours to 1 to avoid causing issues.
+    /// </summary>
+    private void OnComponentShutdown(Entity<SlidingComponent> entity, ref ComponentShutdown args)
+    {
+        entity.Comp.FrictionModifier = 1;
+        _speedModifierSystem.RefreshFrictionModifiers(entity);
     }
 
     /// <summary>
@@ -25,28 +59,81 @@ public sealed class SlidingSystem : EntitySystem
     }
 
     /// <summary>
-    ///     Sets friction to 0 if colliding with a SuperSlippery Entity.
+    ///     Updates friction when we collide with a slippery entity
     /// </summary>
-    private void OnStartCollide(EntityUid uid, SlidingComponent component, ref StartCollideEvent args)
+    private void OnStartCollide(Entity<SlidingComponent> entity, ref StartCollideEvent args)
     {
-        if (!TryComp<SlipperyComponent>(args.OtherEntity, out var slippery) || !slippery.SlipData.SuperSlippery)
+        if (!_slipperyQuery.TryComp(args.OtherEntity, out var slippery) || !slippery.AffectsSliding)
             return;
 
-        component.CollidingEntities.Add(args.OtherEntity);
-        Dirty(uid, component);
+        CalculateSlidingModifier(entity);
+        _speedModifierSystem.RefreshFrictionModifiers(entity);
     }
 
     /// <summary>
-    ///     Set friction to normal when ending collision with a SuperSlippery entity.
+    ///     Update friction when we stop colliding with a slippery entity
     /// </summary>
-    private void OnEndCollide(EntityUid uid, SlidingComponent component, ref EndCollideEvent args)
+    private void OnEndCollide(Entity<SlidingComponent> entity, ref EndCollideEvent args)
     {
-        if (!component.CollidingEntities.Remove(args.OtherEntity))
+        if (!_slipperyQuery.TryComp(args.OtherEntity, out var slippery) || !slippery.AffectsSliding)
             return;
 
-        if (component.CollidingEntities.Count == 0)
-            RemComp<SlidingComponent>(uid);
+        if (!CalculateSlidingModifier(entity, args.OtherEntity))
+        {
+            RemComp<SlidingComponent>(entity);
+            return;
+        }
 
-        Dirty(uid, component);
+        _speedModifierSystem.RefreshFrictionModifiers(entity);
+    }
+
+    /// <summary>
+    ///     Gets contacting slippery entities and averages their friction modifiers.
+    /// </summary>
+    private bool CalculateSlidingModifier(Entity<SlidingComponent, PhysicsComponent?> entity, EntityUid? ignore = null)
+    {
+        if (!Resolve(entity, ref entity.Comp2, false))
+            return false;
+
+        var friction = 0.0f;
+        var count = 0;
+        entity.Comp1.Contacting.Clear();
+
+        _physics.GetContactingEntities((entity, entity.Comp2), entity.Comp1.Contacting);
+
+        foreach (var ent in entity.Comp1.Contacting)
+        {
+            if (ent == ignore || !_slipperyQuery.TryComp(ent, out var slippery) || !slippery.AffectsSliding)
+                continue;
+
+            friction += slippery.SlipData.SlipFriction;
+
+            count++;
+        }
+
+        if (count > 0)
+        {
+            entity.Comp1.FrictionModifier = friction / count;
+            Dirty(entity.Owner, entity.Comp1);
+            return true;
+        }
+
+        return false;
+    }
+
+    private void OnRefreshFrictionModifiers(Entity<SlidingComponent> entity, ref RefreshFrictionModifiersEvent args)
+    {
+        args.ModifyFriction(entity.Comp.FrictionModifier);
+        args.ModifyAcceleration(entity.Comp.FrictionModifier);
+    }
+
+    private void OnThrowerImpulse(Entity<SlidingComponent> entity, ref ThrowerImpulseEvent args)
+    {
+        args.Push = true;
+    }
+
+    private void ShooterImpulseEvent(Entity<SlidingComponent> entity, ref ShooterImpulseEvent args)
+    {
+        args.Push = true;
     }
 }
index f6b6dd0b35ab6a0d784bc7e7388175e97f12d432..7b6cd90800201fff3b6790a76913e46b847ad8fb 100644 (file)
@@ -21,6 +21,25 @@ namespace Content.Shared.Slippery
         [Access(Other = AccessPermissions.ReadWriteExecute)]
         public SoundSpecifier SlipSound = new SoundPathSpecifier("/Audio/Effects/slip.ogg");
 
+        /// <summary>
+        /// Should this component's friction factor into sliding friction?
+        /// </summary>
+        [DataField, AutoNetworkedField]
+        public bool AffectsSliding;
+
+        /// <summary>
+        /// How long should this component apply the FrictionStatusComponent?
+        /// Note: This does stack with SlidingComponent since they are two separate Components
+        /// </summary>
+        [DataField, AutoNetworkedField]
+        public TimeSpan FrictionStatusTime = TimeSpan.FromSeconds(0.5f);
+
+        /// <summary>
+        /// How much stamina damage should this component do on slip?
+        /// </summary>
+        [DataField, AutoNetworkedField]
+        public float StaminaDamage = 25f;
+
         /// <summary>
         /// Loads the data needed to determine how slippery something is.
         /// </summary>
@@ -34,10 +53,22 @@ namespace Content.Shared.Slippery
     public sealed partial class SlipperyEffectEntry
     {
         /// <summary>
-        /// How many seconds the mob will be paralyzed for.
+        /// How many seconds the mob will be stunned for.
+        /// </summary>
+        [DataField]
+        public TimeSpan StunTime = TimeSpan.FromSeconds(0.5);
+
+        /// <summary>
+        /// How many seconds the mob will be knocked down for.
+        /// </summary>
+        [DataField]
+        public TimeSpan KnockdownTime = TimeSpan.FromSeconds(1.5);
+
+        /// <summary>
+        /// Should the slipped entity try to stand up when Knockdown ends?
         /// </summary>
         [DataField]
-        public TimeSpan ParalyzeTime = TimeSpan.FromSeconds(1.5);
+        public bool AutoStand = true;
 
         /// <summary>
         /// The entity's speed will be multiplied by this to slip it forwards.
@@ -63,6 +94,6 @@ namespace Content.Shared.Slippery
         /// This is used to store the friction modifier that is used on a sliding entity.
         /// </summary>
         [DataField]
-        public float SlipFriction;
+        public float SlipFriction = 0.5f;
     }
 }
index 40d12d9ebeeb01d1af6192a7f4d33e4a5a9a1813..f7e3da8edcf740f6705b7585d802d28c71ae9506 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Administration.Logs;
+using Content.Shared.Damage.Systems;
 using Content.Shared.Database;
 using Content.Shared.Inventory;
 using Robust.Shared.Network;
@@ -23,8 +24,10 @@ namespace Content.Shared.Slippery;
 public sealed class SlipperySystem : EntitySystem
 {
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly MovementModStatusSystem _movementMod = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedStunSystem _stun = default!;
+    [Dependency] private readonly SharedStaminaSystem _stamina = default!;
     [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
     [Dependency] private readonly SharedContainerSystem _container = default!;
     [Dependency] private readonly SharedPhysicsSystem _physics = default!;
@@ -115,27 +118,21 @@ public sealed class SlipperySystem : EntitySystem
         {
             _physics.SetLinearVelocity(other, physics.LinearVelocity * component.SlipData.LaunchForwardsMultiplier, body: physics);
 
-            if (component.SlipData.SuperSlippery && requiresContact)
-            {
-                var sliding = EnsureComp<SlidingComponent>(other);
-                sliding.CollidingEntities.Add(uid);
-                // Why the fuck does this assertion stack overflow every once in a while
-                DebugTools.Assert(_physics.GetContactingEntities(other, physics).Contains(uid));
-            }
+            if (component.AffectsSliding && requiresContact)
+                EnsureComp<SlidingComponent>(other);
         }
 
-        var playSound = !_statusEffects.HasStatusEffect(other, "KnockedDown");
-
-        _stun.TryParalyze(other, component.SlipData.ParalyzeTime, true);
-
-        // Preventing from playing the slip sound when you are already knocked down.
-        if (playSound)
+        // Preventing from playing the slip sound and stunning when you are already knocked down.
+        if (!HasComp<KnockedDownComponent>(other))
         {
+            _stun.TryStun(other, component.SlipData.StunTime, true);
+            _stamina.TakeStaminaDamage(other, component.StaminaDamage); // Note that this can stamCrit
+            _movementMod.TryFriction(other, component.FrictionStatusTime, true, component.SlipData.SlipFriction, component.SlipData.SlipFriction);
             _audio.PlayPredicted(component.SlipSound, other, other);
         }
+        _stun.TryKnockdown(other, component.SlipData.KnockdownTime, true, true);
 
-        _adminLogger.Add(LogType.Slip, LogImpact.Low,
-            $"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(uid):entity}");
+        _adminLogger.Add(LogType.Slip, LogImpact.Low, $"{ToPrettyString(other):mob} slipped on collision with {ToPrettyString(uid):entity}");
     }
 }
 
index 027087223554346d07b52bcabe19e329d6191d44..c6252d969af0f5356c8e2e28eb65d1636e61f1f1 100644 (file)
@@ -14,6 +14,25 @@ namespace Content.Shared.Standing
         [DataField, AutoNetworkedField]
         public bool Standing { get; set; } = true;
 
+        /// <summary>
+        /// Time it takes us to stand up
+        /// </summary>
+        [DataField, AutoNetworkedField]
+        public TimeSpan StandTime = TimeSpan.FromSeconds(2);
+
+        /// <summary>
+        /// Default Friction modifier for knocked down players.
+        /// Makes them accelerate and deccelerate slower.
+        /// </summary>
+        [DataField, AutoNetworkedField]
+        public float FrictionModifier = 0.4f;
+
+        /// <summary>
+        /// Base modifier to the maximum movement speed of a knocked down mover.
+        /// </summary>
+        [DataField, AutoNetworkedField]
+        public float SpeedModifier = 0.3f;
+
         /// <summary>
         ///     List of fixtures that had their collision mask changed when the entity was downed.
         ///     Required for re-adding the collision mask.
index 86d2b961eb5fa9200c211b569e40086c83110df0..717756816394d25948f562fec801777e8fb68f3f 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Hands.Components;
+using Content.Shared.Movement.Events;
 using Content.Shared.Movement.Systems;
 using Content.Shared.Physics;
 using Content.Shared.Rotation;
@@ -15,13 +16,16 @@ public sealed class StandingStateSystem : EntitySystem
     [Dependency] private readonly SharedPhysicsSystem _physics = default!;
 
     // If StandingCollisionLayer value is ever changed to more than one layer, the logic needs to be edited.
-    private const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable;
+    public const int StandingCollisionLayer = (int) CollisionGroup.MidImpassable;
 
     public override void Initialize()
     {
         base.Initialize();
         SubscribeLocalEvent<StandingStateComponent, AttemptMobCollideEvent>(OnMobCollide);
         SubscribeLocalEvent<StandingStateComponent, AttemptMobTargetCollideEvent>(OnMobTargetCollide);
+        SubscribeLocalEvent<StandingStateComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeedModifiers);
+        SubscribeLocalEvent<StandingStateComponent, RefreshFrictionModifiersEvent>(OnRefreshFrictionModifiers);
+        SubscribeLocalEvent<StandingStateComponent, TileFrictionEvent>(OnTileFriction);
     }
 
     private void OnMobTargetCollide(Entity<StandingStateComponent> ent, ref AttemptMobTargetCollideEvent args)
@@ -40,6 +44,27 @@ public sealed class StandingStateSystem : EntitySystem
         }
     }
 
+    private void OnRefreshMovementSpeedModifiers(Entity<StandingStateComponent> entity, ref RefreshMovementSpeedModifiersEvent args)
+    {
+        if (!entity.Comp.Standing)
+            args.ModifySpeed(entity.Comp.FrictionModifier);
+    }
+
+    private void OnRefreshFrictionModifiers(Entity<StandingStateComponent> entity, ref RefreshFrictionModifiersEvent args)
+    {
+        if (entity.Comp.Standing)
+            return;
+
+        args.ModifyFriction(entity.Comp.FrictionModifier);
+        args.ModifyAcceleration(entity.Comp.FrictionModifier);
+    }
+
+    private void OnTileFriction(Entity<StandingStateComponent> entity, ref TileFrictionEvent args)
+    {
+        if (!entity.Comp.Standing)
+            args.Modifier *= entity.Comp.FrictionModifier;
+    }
+
     public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null)
     {
         if (!Resolve(uid, ref standingState, false))
index 224a3dc4a4db5d2219636be08389bc4da0f4548d..dddcbc660c03090ac4f6683003fbe4791ec1c1b4 100644 (file)
@@ -1,3 +1,5 @@
+using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
 using Content.Shared.StatusEffectNew.Components;
 using Robust.Shared.Player;
 
@@ -9,6 +11,9 @@ public sealed partial class StatusEffectsSystem
     {
         SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerAttachedEvent>(RelayStatusEffectEvent);
         SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerDetachedEvent>(RelayStatusEffectEvent);
+
+        SubscribeLocalEvent<StatusEffectContainerComponent, RefreshFrictionModifiersEvent>(RefRelayStatusEffectEvent);
+        SubscribeLocalEvent<StatusEffectContainerComponent, TileFrictionEvent>(RefRelayStatusEffectEvent);
     }
 
     private void RefRelayStatusEffectEvent<T>(EntityUid uid, StatusEffectContainerComponent component, ref T args) where T : struct
index e4f11b8cdaa675010bf6a6089f8b5f9f4b0307f3..1ee2255c2212cc56c7dd952d222164769a4a26db 100644 (file)
@@ -1,18 +1,46 @@
-using Robust.Shared.Audio;
+using Content.Shared.DoAfter;
 using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Stunnable;
 
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStunSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas:true), AutoGenerateComponentPause, Access(typeof(SharedStunSystem))]
 public sealed partial class KnockedDownComponent : Component
 {
-    [DataField("helpInterval"), AutoNetworkedField]
-    public float HelpInterval = 1f;
+    /// <summary>
+    /// Game time that we can stand up.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+    public TimeSpan NextUpdate;
 
-    [DataField("helpAttemptSound")]
-    public SoundSpecifier StunAttemptSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg");
+    /// <summary>
+    /// Should we try to stand up?
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool AutoStand = true;
 
-    [ViewVariables, AutoNetworkedField]
-    public float HelpTimer = 0f;
+    /// <summary>
+    /// The Standing Up DoAfter.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public ushort? DoAfterId;
+
+    /// <summary>
+    /// Friction modifier for knocked down players.
+    /// Makes them accelerate and deccelerate slower.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float FrictionModifier = 1f; // Should add a friction modifier to slipping to compensate for this
+
+    /// <summary>
+    /// Modifier to the maximum movement speed of a knocked down mover.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float SpeedModifier = 1f;
+
+    /// <summary>
+    /// How long does it take us to get up?
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public TimeSpan GetUpDoAfter = TimeSpan.FromSeconds(1);
 }
diff --git a/Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs b/Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs
new file mode 100644 (file)
index 0000000..bd8347e
--- /dev/null
@@ -0,0 +1,538 @@
+using Content.Shared.Alert;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Components;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
+using Content.Shared.Hands;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Input;
+using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Rejuvenate;
+using Content.Shared.Standing;
+using Robust.Shared.Audio;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Stunnable;
+
+/// <summary>
+/// This contains the knockdown logic for the stun system for organization purposes.
+/// </summary>
+public abstract partial class SharedStunSystem
+{
+    // TODO: Both of these constants need to be moved to a component somewhere, and need to be tweaked for balance...
+    // We don't always have standing state available when these are called so it can't go there
+    // Maybe I can pass the values to KnockedDownComponent from Standing state on Component init?
+    // Default knockdown timer
+    public static readonly TimeSpan DefaultKnockedDuration = TimeSpan.FromSeconds(0.5f);
+    // Minimum damage taken to refresh our knockdown timer to the default duration
+    public static readonly float KnockdownDamageThreshold = 5f;
+
+    [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly StandingStateSystem _standingState = default!;
+
+    public static readonly ProtoId<AlertPrototype> KnockdownAlert = "Knockdown";
+
+    private void InitializeKnockdown()
+    {
+        SubscribeLocalEvent<KnockedDownComponent, RejuvenateEvent>(OnRejuvenate);
+
+        // Startup and Shutdown
+        SubscribeLocalEvent<KnockedDownComponent, ComponentInit>(OnKnockInit);
+        SubscribeLocalEvent<KnockedDownComponent, ComponentShutdown>(OnKnockShutdown);
+
+        // Action blockers
+        SubscribeLocalEvent<KnockedDownComponent, BuckleAttemptEvent>(OnBuckleAttempt);
+        SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandUpAttempt);
+
+        // Updating movement a friction
+        SubscribeLocalEvent<KnockedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshKnockedSpeed);
+        SubscribeLocalEvent<KnockedDownComponent, RefreshFrictionModifiersEvent>(OnRefreshFriction);
+        SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction);
+        SubscribeLocalEvent<KnockedDownComponent, DidEquipHandEvent>(OnHandEquipped);
+        SubscribeLocalEvent<KnockedDownComponent, DidUnequipHandEvent>(OnHandUnequipped);
+
+        // DoAfter event subscriptions
+        SubscribeLocalEvent<KnockedDownComponent, TryStandDoAfterEvent>(OnStandDoAfter);
+
+        // Knockdown Extenders
+        SubscribeLocalEvent<KnockedDownComponent, DamageChangedEvent>(OnDamaged);
+
+        // Handling Alternative Inputs
+        SubscribeAllEvent<ForceStandUpEvent>(OnForceStandup);
+        SubscribeLocalEvent<KnockedDownComponent, KnockedDownAlertEvent>(OnKnockedDownAlert);
+
+        CommandBinds.Builder
+            .Bind(ContentKeyFunctions.ToggleKnockdown, InputCmdHandler.FromDelegate(HandleToggleKnockdown, handle: false))
+            .Register<SharedStunSystem>();
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var query = EntityQueryEnumerator<KnockedDownComponent>();
+
+        while (query.MoveNext(out var uid, out var knockedDown))
+        {
+            if (!knockedDown.AutoStand || knockedDown.DoAfterId.HasValue || knockedDown.NextUpdate > GameTiming.CurTime)
+                continue;
+
+            TryStanding(uid, out knockedDown.DoAfterId);
+            DirtyField(uid, knockedDown, nameof(KnockedDownComponent.DoAfterId));
+        }
+    }
+
+    private void OnRejuvenate(Entity<KnockedDownComponent> entity, ref RejuvenateEvent args)
+    {
+        SetKnockdownTime(entity, GameTiming.CurTime);
+
+        if (entity.Comp.AutoStand)
+            RemComp<KnockedDownComponent>(entity);
+    }
+
+    #region Startup and Shutdown
+
+    private void OnKnockInit(Entity<KnockedDownComponent> entity, ref ComponentInit args)
+    {
+        // Other systems should handle dropping held items...
+        _standingState.Down(entity, true, false);
+        RefreshKnockedMovement(entity);
+    }
+
+    private void OnKnockShutdown(Entity<KnockedDownComponent> entity, ref ComponentShutdown args)
+    {
+        // This is jank but if we don't do this it'll still use the knockedDownComponent modifiers for friction because it hasn't been deleted quite yet.
+        entity.Comp.FrictionModifier = 1f;
+        entity.Comp.SpeedModifier = 1f;
+
+        _standingState.Stand(entity);
+        Alerts.ClearAlert(entity, KnockdownAlert);
+    }
+
+    #endregion
+
+    #region API
+
+    /// <summary>
+    /// Sets the autostand property of a <see cref="KnockedDownComponent"/> on an entity to true or false and dirties it.
+    /// Defaults to false.
+    /// </summary>
+    /// <param name="entity">Entity we want to edit the data field of.</param>
+    /// <param name="autoStand">What we want to set the data field to.</param>
+    public void SetAutoStand(Entity<KnockedDownComponent?> entity, bool autoStand = false)
+    {
+        if (!Resolve(entity, ref entity.Comp, false))
+            return;
+
+        entity.Comp.AutoStand = autoStand;
+        DirtyField(entity, entity.Comp, nameof(entity.Comp.AutoStand));
+    }
+
+    /// <summary>
+    /// Cancels the DoAfter of an entity with the <see cref="KnockedDownComponent"/> who is trying to stand.
+    /// </summary>
+    /// <param name="entity">Entity who we are canceling the DoAfter for.</param>
+    public void CancelKnockdownDoAfter(Entity<KnockedDownComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp, false))
+            return;
+
+        if (entity.Comp.DoAfterId == null)
+            return;
+
+        DoAfter.Cancel(entity.Owner, entity.Comp.DoAfterId.Value);
+        entity.Comp.DoAfterId = null;
+        DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.DoAfterId));
+    }
+
+    /// <summary>
+    /// Updates the knockdown timer of a knocked down entity with a given inputted time, then dirties the time.
+    /// </summary>
+    /// <param name="entity">Entity who's knockdown time we're updating.</param>
+    /// <param name="time">The time we're updating with.</param>
+    /// <param name="refresh">Whether we're resetting the timer or adding to the current timer.</param>
+    public void UpdateKnockdownTime(Entity<KnockedDownComponent> entity, TimeSpan time, bool refresh = true)
+    {
+        if (refresh)
+            RefreshKnockdownTime(entity, time);
+        else
+            AddKnockdownTime(entity, time);
+    }
+
+    /// <summary>
+    /// Sets the next update datafield of an entity's <see cref="KnockedDownComponent"/> to a specific time.
+    /// </summary>
+    /// <param name="entity">Entity whose timer we're updating</param>
+    /// <param name="time">The exact time we're setting the next update to.</param>
+    public void SetKnockdownTime(Entity<KnockedDownComponent> entity, TimeSpan time)
+    {
+        entity.Comp.NextUpdate = time;
+        DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.NextUpdate));
+    }
+
+    /// <summary>
+    /// Refreshes the amount of time an entity is knocked down to the inputted time, if it is greater than
+    /// the current time left.
+    /// </summary>
+    /// <param name="entity">Entity whose timer we're updating</param>
+    /// <param name="time">The time we want them to be knocked down for.</param>
+    public void RefreshKnockdownTime(Entity<KnockedDownComponent> entity, TimeSpan time)
+    {
+        var knockedTime = GameTiming.CurTime + time;
+        if (entity.Comp.NextUpdate < knockedTime)
+            SetKnockdownTime(entity, knockedTime);
+    }
+
+    /// <summary>
+    /// Adds our inputted time to an entity's knocked down timer, or sets it to the given time if their timer has expired.
+    /// </summary>
+    /// <param name="entity">Entity whose timer we're updating</param>
+    /// <param name="time">The time we want to add to their knocked down timer.</param>
+    public void AddKnockdownTime(Entity<KnockedDownComponent> entity, TimeSpan time)
+    {
+        if (entity.Comp.NextUpdate < GameTiming.CurTime)
+        {
+            SetKnockdownTime(entity, GameTiming.CurTime + time);
+            return;
+        }
+
+        entity.Comp.NextUpdate += time;
+        DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.NextUpdate));
+    }
+
+    /// <summary>
+    /// Checks if an entity is able to stand, returns true if it can, returns false if it cannot.
+    /// </summary>
+    /// <param name="entity">Entity we're checking</param>
+    /// <returns>Returns whether the entity is able to stand</returns>
+    public bool CanStand(Entity<KnockedDownComponent> entity)
+    {
+        if (entity.Comp.NextUpdate > GameTiming.CurTime)
+            return false;
+
+        if (!Blocker.CanMove(entity))
+            return false;
+
+        var ev = new StandUpAttemptEvent();
+        RaiseLocalEvent(entity, ref ev);
+
+        return !ev.Cancelled;
+    }
+
+    #endregion
+
+    #region Knockdown Logic
+
+    private void HandleToggleKnockdown(ICommonSession? session)
+    {
+        if (session is not { } playerSession)
+            return;
+
+        if (playerSession.AttachedEntity is not { Valid: true } playerEnt || !Exists(playerEnt))
+            return;
+
+        if (!TryComp<KnockedDownComponent>(playerEnt, out var component))
+        {
+            TryKnockdown(playerEnt, DefaultKnockedDuration, true, false, false); // TODO: Unhardcode these numbers
+            return;
+        }
+
+        var stand = !component.DoAfterId.HasValue;
+        SetAutoStand(playerEnt, stand);
+
+        if (stand && TryStanding(playerEnt, out component.DoAfterId))
+            DirtyField(playerEnt, component, nameof(KnockedDownComponent.DoAfterId));
+        else
+            CancelKnockdownDoAfter((playerEnt, component));
+    }
+
+    public bool TryStanding(Entity<KnockedDownComponent?, StandingStateComponent?> entity, out ushort? id)
+    {
+        id = null;
+        // If we aren't knocked down or can't be knocked down, then we did technically succeed in standing up
+        if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2, false))
+            return true;
+
+        id = entity.Comp1.DoAfterId;
+
+        if (!TryStand((entity.Owner, entity.Comp1)))
+            return false;
+
+        var ev = new GetStandUpTimeEvent(entity.Comp2.StandTime);
+        RaiseLocalEvent(entity, ref ev);
+
+        var doAfterArgs = new DoAfterArgs(EntityManager, entity, ev.DoAfterTime, new TryStandDoAfterEvent(), entity, entity)
+        {
+            BreakOnDamage = true,
+            DamageThreshold = 5,
+            CancelDuplicate = true,
+            RequireCanInteract = false,
+            BreakOnHandChange = true
+        };
+
+        // If we try standing don't try standing again
+        if (!DoAfter.TryStartDoAfter(doAfterArgs, out var doAfterId))
+            return false;
+
+        id = doAfterId.Value.Index;
+        return true;
+    }
+
+    /// <summary>
+    /// A variant of <see cref="CanStand"/> used when we're actually trying to stand.
+    /// Main difference is this one affects autostand datafields and also displays popups.
+    /// </summary>
+    /// <param name="entity">Entity we're checking</param>
+    /// <returns>Returns whether the entity is able to stand</returns>
+    public bool TryStand(Entity<KnockedDownComponent> entity)
+    {
+        if (entity.Comp.NextUpdate > GameTiming.CurTime)
+            return false;
+
+        if (!Blocker.CanMove(entity))
+            return false;
+
+        var ev = new StandUpAttemptEvent(entity.Comp.AutoStand);
+        RaiseLocalEvent(entity, ref ev);
+
+        if (ev.Autostand != entity.Comp.AutoStand)
+            SetAutoStand(entity!, ev.Autostand);
+
+        if (ev.Message != null)
+        {
+            _popup.PopupClient(ev.Message.Value.Item1, entity, entity, ev.Message.Value.Item2);
+        }
+
+        return !ev.Cancelled;
+    }
+
+    private bool StandingBlocked(Entity<KnockedDownComponent> entity)
+    {
+        if (!TryStand(entity))
+            return true;
+
+        if (!IntersectingStandingColliders(entity.Owner))
+            return false;
+
+        _popup.PopupClient(Loc.GetString("knockdown-component-stand-no-room"), entity, entity, PopupType.SmallCaution);
+        SetAutoStand(entity.Owner);
+        return true;
+
+    }
+
+    private void OnForceStandup(ForceStandUpEvent msg, EntitySessionEventArgs args)
+    {
+        if (args.SenderSession.AttachedEntity is not {} user)
+            return;
+
+        ForceStandUp(user);
+    }
+
+    public void ForceStandUp(Entity<KnockedDownComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp, false))
+            return;
+
+        // That way if we fail to stand, the game will try to stand for us when we are able to
+        SetAutoStand(entity, true);
+
+        if (!HasComp<StandingStateComponent>(entity) || StandingBlocked((entity, entity.Comp)))
+            return;
+
+        if (!_hands.TryGetEmptyHand(entity.Owner, out _))
+            return;
+
+        if (!TryForceStand(entity.Owner))
+            return;
+
+        // If we have a DoAfter, cancel it
+        CancelKnockdownDoAfter(entity);
+        // Remove Component
+        RemComp<KnockedDownComponent>(entity);
+
+        _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(entity):user} has force stood up from knockdown.");
+    }
+
+    private void OnKnockedDownAlert(Entity<KnockedDownComponent> entity, ref KnockedDownAlertEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        // If we're already trying to stand, or we fail to stand try forcing it
+        if (!TryStanding(entity.Owner, out entity.Comp.DoAfterId))
+            ForceStandUp(entity!);
+
+        DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.DoAfterId));
+        args.Handled = true;
+    }
+
+    private bool TryForceStand(Entity<StaminaComponent?> entity)
+    {
+        // Can't force stand if no Stamina.
+        if (!Resolve(entity, ref entity.Comp, false))
+            return false;
+
+        var ev = new TryForceStandEvent(entity.Comp.ForceStandStamina);
+        RaiseLocalEvent(entity, ref ev);
+
+        if (!Stamina.TryTakeStamina(entity, ev.Stamina, entity.Comp, visual: true))
+        {
+            _popup.PopupClient(Loc.GetString("knockdown-component-pushup-failure"), entity, entity, PopupType.MediumCaution);
+            return false;
+        }
+
+        _popup.PopupClient(Loc.GetString("knockdown-component-pushup-success"), entity, entity);
+        _audio.PlayPredicted(entity.Comp.ForceStandSuccessSound, entity.Owner, entity.Owner, AudioParams.Default.WithVariation(0.025f).WithVolume(5f));
+
+        return true;
+    }
+
+    /// <summary>
+    ///     Checks if standing would cause us to collide with something and potentially get stuck.
+    ///     Returns true if we will collide with something, and false if we will not.
+    /// </summary>
+    private bool IntersectingStandingColliders(Entity<TransformComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp))
+            return false;
+
+        var intersecting = _physics.GetEntitiesIntersectingBody(entity, StandingStateSystem.StandingCollisionLayer, false);
+
+        if (intersecting.Count == 0)
+            return false;
+
+        var fixtureQuery = GetEntityQuery<FixturesComponent>();
+        var xformQuery = GetEntityQuery<TransformComponent>();
+
+        var ourAABB = _entityLookup.GetAABBNoContainer(entity, entity.Comp.LocalPosition, entity.Comp.LocalRotation);
+
+        foreach (var ent in intersecting)
+        {
+            if (!fixtureQuery.TryGetComponent(ent, out var fixtures))
+                continue;
+
+            if (!xformQuery.TryComp(ent, out var xformComp))
+                continue;
+
+            var xform = new Transform(xformComp.LocalPosition, xformComp.LocalRotation);
+
+            foreach (var fixture in fixtures.Fixtures.Values)
+            {
+                if (!fixture.Hard || (fixture.CollisionMask & StandingStateSystem.StandingCollisionLayer) != StandingStateSystem.StandingCollisionLayer)
+                    continue;
+
+                for (var i = 0; i < fixture.Shape.ChildCount; i++)
+                {
+                    var intersection = fixture.Shape.ComputeAABB(xform, i).IntersectPercentage(ourAABB);
+                    if (intersection > 0.1f)
+                        return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    #endregion
+
+    #region Knockdown Extenders
+
+    private void OnDamaged(Entity<KnockedDownComponent> entity, ref DamageChangedEvent args)
+    {
+        // We only want to extend our knockdown timer if it would've prevented us from standing up
+        if (!args.InterruptsDoAfters || !args.DamageIncreased || args.DamageDelta == null || GameTiming.ApplyingState)
+            return;
+
+        if (args.DamageDelta.GetTotal() >= KnockdownDamageThreshold) // TODO: Unhardcode this
+            SetKnockdownTime(entity, GameTiming.CurTime + DefaultKnockedDuration);
+    }
+
+    #endregion
+
+    #region Action Blockers
+
+    private void OnStandUpAttempt(Entity<KnockedDownComponent> entity, ref StandAttemptEvent args)
+    {
+        if (entity.Comp.LifeStage <= ComponentLifeStage.Running)
+            args.Cancel();
+    }
+
+    private void OnBuckleAttempt(Entity<KnockedDownComponent> entity, ref BuckleAttemptEvent args)
+    {
+        if (args.User == entity && entity.Comp.NextUpdate > GameTiming.CurTime)
+            args.Cancelled = true;
+    }
+
+    #endregion
+
+    #region DoAfter
+
+    private void OnStandDoAfter(Entity<KnockedDownComponent> entity, ref TryStandDoAfterEvent args)
+    {
+        entity.Comp.DoAfterId = null;
+
+        if (args.Cancelled || StandingBlocked(entity))
+        {
+            DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.DoAfterId));
+            return;
+        }
+
+        RemComp<KnockedDownComponent>(entity);
+
+        _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(entity):user} has stood up from knockdown.");
+    }
+
+    #endregion
+
+    #region Movement and Friction
+
+    private void RefreshKnockedMovement(Entity<KnockedDownComponent> ent)
+    {
+        var ev = new KnockedDownRefreshEvent();
+        RaiseLocalEvent(ent, ref ev);
+
+        ent.Comp.SpeedModifier = ev.SpeedModifier;
+        ent.Comp.FrictionModifier = ev.FrictionModifier;
+
+        _movementSpeedModifier.RefreshMovementSpeedModifiers(ent);
+        _movementSpeedModifier.RefreshFrictionModifiers(ent);
+    }
+
+    private void OnRefreshKnockedSpeed(Entity<KnockedDownComponent> entity, ref RefreshMovementSpeedModifiersEvent args)
+    {
+        args.ModifySpeed(entity.Comp.SpeedModifier);
+    }
+
+    private void OnKnockedTileFriction(Entity<KnockedDownComponent> entity, ref TileFrictionEvent args)
+    {
+        args.Modifier *= entity.Comp.FrictionModifier;
+    }
+
+    private void OnRefreshFriction(Entity<KnockedDownComponent> entity, ref RefreshFrictionModifiersEvent args)
+    {
+        args.ModifyFriction(entity.Comp.FrictionModifier);
+        args.ModifyAcceleration(entity.Comp.FrictionModifier);
+    }
+
+    private void OnHandEquipped(Entity<KnockedDownComponent> entity, ref DidEquipHandEvent args)
+    {
+        RefreshKnockedMovement(entity);
+    }
+
+    private void OnHandUnequipped(Entity<KnockedDownComponent> entity, ref DidUnequipHandEvent args)
+    {
+        RefreshKnockedMovement(entity);
+    }
+
+    #endregion
+}
index c46dd10ed7849a55180e4426406bd124ff103758..2f75b71f56116d8534a35d47cd06cfec3c91c90f 100644 (file)
@@ -1,12 +1,13 @@
 using Content.Shared.ActionBlocker;
 using Content.Shared.Administration.Logs;
-using Content.Shared.Interaction;
+using Content.Shared.Alert;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Item;
-using Content.Shared.Bed.Sleep;
 using Content.Shared.Damage.Components;
+using Content.Shared.Damage.Systems;
 using Content.Shared.Database;
+using Content.Shared.DoAfter;
 using Content.Shared.Hands;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
@@ -17,9 +18,7 @@ using Content.Shared.StatusEffect;
 using Content.Shared.Throwing;
 using Content.Shared.Whitelist;
 using Robust.Shared.Audio.Systems;
-using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Events;
-using Robust.Shared.Physics.Systems;
 using Robust.Shared.Timing;
 
 namespace Content.Shared.Stunnable;
@@ -27,40 +26,28 @@ namespace Content.Shared.Stunnable;
 public abstract partial class SharedStunSystem : EntitySystem
 {
     [Dependency] protected readonly ActionBlockerSystem Blocker = default!;
+    [Dependency] protected readonly AlertsSystem Alerts = default!;
+    [Dependency] protected readonly IGameTiming GameTiming = default!;
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
     [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
-    [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
-    [Dependency] private readonly StandingStateSystem _standingState = default!;
+    [Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;
+    [Dependency] protected readonly SharedStaminaSystem Stamina = default!;
     [Dependency] private readonly StatusEffectsSystem _statusEffect = default!;
 
-    /// <summary>
-    /// Friction modifier for knocked down players.
-    /// Doesn't make them faster but makes them slow down... slower.
-    /// </summary>
-    public const float KnockDownModifier = 0.2f;
-
     public override void Initialize()
     {
-        SubscribeLocalEvent<KnockedDownComponent, ComponentInit>(OnKnockInit);
-        SubscribeLocalEvent<KnockedDownComponent, ComponentShutdown>(OnKnockShutdown);
-        SubscribeLocalEvent<KnockedDownComponent, StandAttemptEvent>(OnStandAttempt);
-
         SubscribeLocalEvent<SlowedDownComponent, ComponentInit>(OnSlowInit);
         SubscribeLocalEvent<SlowedDownComponent, ComponentShutdown>(OnSlowRemove);
+        SubscribeLocalEvent<SlowedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
 
         SubscribeLocalEvent<StunnedComponent, ComponentStartup>(UpdateCanMove);
         SubscribeLocalEvent<StunnedComponent, ComponentShutdown>(OnStunShutdown);
 
         SubscribeLocalEvent<StunOnContactComponent, StartCollideEvent>(OnStunOnContactCollide);
 
-        // helping people up if they're knocked down
-        SubscribeLocalEvent<KnockedDownComponent, InteractHandEvent>(OnInteractHand);
-        SubscribeLocalEvent<SlowedDownComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
-
-        SubscribeLocalEvent<KnockedDownComponent, TileFrictionEvent>(OnKnockedTileFriction);
-
         // Attempt event subscriptions.
         SubscribeLocalEvent<StunnedComponent, ChangeDirectionAttemptEvent>(OnAttempt);
         SubscribeLocalEvent<StunnedComponent, UpdateCanMoveEvent>(OnMoveAttempt);
@@ -74,7 +61,7 @@ public abstract partial class SharedStunSystem : EntitySystem
         SubscribeLocalEvent<StunnedComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
         SubscribeLocalEvent<MobStateComponent, MobStateChangedEvent>(OnMobStateChanged);
 
-        // Stun Appearance Data
+        InitializeKnockdown();
         InitializeAppearance();
     }
 
@@ -136,23 +123,7 @@ public abstract partial class SharedStunSystem : EntitySystem
             return;
 
         TryStun(args.OtherEntity, ent.Comp.Duration, true, status);
-        TryKnockdown(args.OtherEntity, ent.Comp.Duration, true, status);
-    }
-
-    private void OnKnockInit(EntityUid uid, KnockedDownComponent component, ComponentInit args)
-    {
-        _standingState.Down(uid);
-    }
-
-    private void OnKnockShutdown(EntityUid uid, KnockedDownComponent component, ComponentShutdown args)
-    {
-        _standingState.Stand(uid);
-    }
-
-    private void OnStandAttempt(EntityUid uid, KnockedDownComponent component, StandAttemptEvent args)
-    {
-        if (component.LifeStage <= ComponentLifeStage.Running)
-            args.Cancel();
+        TryKnockdown(args.OtherEntity, ent.Comp.Duration, ent.Comp.Refresh, ent.Comp.AutoStand);
     }
 
     private void OnSlowInit(EntityUid uid, SlowedDownComponent component, ComponentInit args)
@@ -167,18 +138,12 @@ public abstract partial class SharedStunSystem : EntitySystem
         _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
     }
 
-    private void OnRefreshMovespeed(EntityUid uid, SlowedDownComponent component, RefreshMovementSpeedModifiersEvent args)
-    {
-        args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
-    }
-
     // TODO STUN: Make events for different things. (Getting modifiers, attempt events, informative events...)
 
     /// <summary>
     ///     Stuns the entity, disallowing it from doing many interactions temporarily.
     /// </summary>
-    public bool TryStun(EntityUid uid, TimeSpan time, bool refresh,
-        StatusEffectsComponent? status = null)
+    public bool TryStun(EntityUid uid, TimeSpan time, bool refresh, StatusEffectsComponent? status = null)
     {
         if (time <= TimeSpan.Zero)
             return false;
@@ -199,20 +164,48 @@ public abstract partial class SharedStunSystem : EntitySystem
     /// <summary>
     ///     Knocks down the entity, making it fall to the ground.
     /// </summary>
-    public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh,
-        StatusEffectsComponent? status = null)
+    public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh, bool autoStand = true, bool drop = true)
     {
         if (time <= TimeSpan.Zero)
             return false;
 
-        if (!Resolve(uid, ref status, false))
+        // Can't fall down if you can't actually be downed.
+        if (!HasComp<StandingStateComponent>(uid))
             return false;
 
-        if (!_statusEffect.TryAddStatusEffect<KnockedDownComponent>(uid, "KnockedDown", time, refresh))
+        var evAttempt = new KnockDownAttemptEvent(autoStand, drop);
+        RaiseLocalEvent(uid, ref evAttempt);
+
+        if (evAttempt.Cancelled)
             return false;
 
-        var ev = new KnockedDownEvent();
-        RaiseLocalEvent(uid, ref ev);
+        // Initialize our component with the relevant data we need if we don't have it
+        if (EnsureComp<KnockedDownComponent>(uid, out var component))
+        {
+            RefreshKnockedMovement((uid, component));
+            CancelKnockdownDoAfter((uid, component));
+        }
+        else
+        {
+            // Only drop items the first time we want to fall...
+            if (drop)
+            {
+                var ev = new DropHandItemsEvent();
+                RaiseLocalEvent(uid, ref ev);
+            }
+
+            // Only update Autostand value if it's our first time being knocked down...
+            SetAutoStand((uid, component), evAttempt.AutoStand);
+        }
+
+        var knockedEv = new KnockedDownEvent(time);
+        RaiseLocalEvent(uid, ref knockedEv);
+
+        UpdateKnockdownTime((uid, component), knockedEv.Time, refresh);
+
+        Alerts.ShowAlert(uid, KnockdownAlert, null, (GameTiming.CurTime, component.NextUpdate));
+
+        _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} knocked down for {time.Seconds} seconds");
 
         return true;
     }
@@ -226,14 +219,14 @@ public abstract partial class SharedStunSystem : EntitySystem
         if (!Resolve(uid, ref status, false))
             return false;
 
-        return TryKnockdown(uid, time, refresh, status) && TryStun(uid, time, refresh, status);
+        return TryKnockdown(uid, time, refresh) && TryStun(uid, time, refresh, status);
     }
 
     /// <summary>
     ///     Slows down the mob's walking/running speed temporarily
     /// </summary>
     public bool TrySlowdown(EntityUid uid, TimeSpan time, bool refresh,
-        float walkSpeedMultiplier = 1f, float runSpeedMultiplier = 1f,
+        float walkSpeedMod = 1f, float sprintSpeedMod = 1f,
         StatusEffectsComponent? status = null)
     {
         if (!Resolve(uid, ref status, false))
@@ -246,11 +239,11 @@ public abstract partial class SharedStunSystem : EntitySystem
         {
             var slowed = Comp<SlowedDownComponent>(uid);
             // Doesn't make much sense to have the "TrySlowdown" method speed up entities now does it?
-            walkSpeedMultiplier = Math.Clamp(walkSpeedMultiplier, 0f, 1f);
-            runSpeedMultiplier = Math.Clamp(runSpeedMultiplier, 0f, 1f);
+            walkSpeedMod = Math.Clamp(walkSpeedMod, 0f, 1f);
+            sprintSpeedMod = Math.Clamp(sprintSpeedMod, 0f, 1f);
 
-            slowed.WalkSpeedModifier *= walkSpeedMultiplier;
-            slowed.SprintSpeedModifier *= runSpeedMultiplier;
+            slowed.WalkSpeedModifier *= walkSpeedMod;
+            slowed.SprintSpeedModifier *= sprintSpeedMod;
 
             _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
             return true;
@@ -310,30 +303,15 @@ public abstract partial class SharedStunSystem : EntitySystem
         UpdateStunModifiers(ent, speedModifier, speedModifier);
     }
 
-    private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, InteractHandEvent args)
-    {
-        if (args.Handled || knocked.HelpTimer > 0f)
-            return;
-
-        // TODO: This should be an event.
-        if (HasComp<SleepingComponent>(uid))
-            return;
-
-        // Set it to half the help interval so helping is actually useful...
-        knocked.HelpTimer = knocked.HelpInterval / 2f;
-
-        _statusEffect.TryRemoveTime(uid, "KnockedDown", TimeSpan.FromSeconds(knocked.HelpInterval));
-        _audio.PlayPredicted(knocked.StunAttemptSound, uid, args.User);
-        Dirty(uid, knocked);
+    #region friction and movement listeners
 
-        args.Handled = true;
-    }
-
-    private void OnKnockedTileFriction(EntityUid uid, KnockedDownComponent component, ref TileFrictionEvent args)
+    private void OnRefreshMovespeed(EntityUid ent, SlowedDownComponent comp, RefreshMovementSpeedModifiersEvent args)
     {
-        args.Modifier *= KnockDownModifier;
+        args.ModifySpeed(comp.WalkSpeedModifier, comp.SprintSpeedModifier);
     }
 
+    #endregion
+
     #region Attempt Event Handling
 
     private void OnMoveAttempt(EntityUid uid, StunnedComponent stunned, UpdateCanMoveEvent args)
@@ -365,15 +343,3 @@ public abstract partial class SharedStunSystem : EntitySystem
 
     #endregion
 }
-
-/// <summary>
-///     Raised directed on an entity when it is stunned.
-/// </summary>
-[ByRefEvent]
-public record struct StunnedEvent;
-
-/// <summary>
-///     Raised directed on an entity when it is knocked down.
-/// </summary>
-[ByRefEvent]
-public record struct KnockedDownEvent;
index cc4af53b3b3e9f44b7a49eeb5961083bd9846cbe..eae279d58abdd64469387be415a25cb5b430e12b 100644 (file)
@@ -18,6 +18,18 @@ public sealed partial class StunOnContactComponent : Component
     [DataField]
     public TimeSpan Duration = TimeSpan.FromSeconds(5);
 
+    /// <summary>
+    /// Should the stun applied refresh?
+    /// </summary>
+    [DataField]
+    public bool Refresh = true;
+
+    /// <summary>
+    /// Should the stunned entity try to stand up when knockdown ends?
+    /// </summary>
+    [DataField]
+    public bool AutoStand = true;
+
     [DataField]
     public EntityWhitelist Blacklist = new();
 }
diff --git a/Content.Shared/Stunnable/StunnableEvents.cs b/Content.Shared/Stunnable/StunnableEvents.cs
new file mode 100644 (file)
index 0000000..18a7575
--- /dev/null
@@ -0,0 +1,89 @@
+using Content.Shared.Alert;
+using Content.Shared.DoAfter;
+using Content.Shared.Popups;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Stunnable;
+
+/// <summary>
+/// This contains all the events raised by the SharedStunSystem
+/// </summary>
+
+/// <summary>
+///     Raised directed on an entity when it is stunned.
+/// </summary>
+[ByRefEvent]
+public record struct StunnedEvent;
+
+/// <summary>
+///     Raised directed on an entity before it is knocked down to see if it should be cancelled, and to determine
+///     knocked down arguments.
+/// </summary>
+[ByRefEvent]
+public record struct KnockDownAttemptEvent(bool AutoStand, bool Drop)
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+///     Raised directed on an entity when it is knocked down.
+/// </summary>
+[ByRefEvent]
+public record struct KnockedDownEvent(TimeSpan Time);
+
+/// <summary>
+///     Raised on an entity that needs to refresh its knockdown modifiers
+/// </summary>
+[ByRefEvent]
+public record struct KnockedDownRefreshEvent()
+{
+    public float SpeedModifier = 1f;
+    public float FrictionModifier = 1f;
+}
+
+/// <summary>
+///     Raised directed on an entity when it tries to stand up
+/// </summary>
+/// <param name="Autostand">If the attempt was cancelled, passes a recommended value to change autostand to.</param>
+[ByRefEvent]
+public record struct StandUpAttemptEvent(bool Autostand)
+{
+    public bool Cancelled = false;
+
+    // Popup data to display to the entity if we so desire...
+    public (string, PopupType)? Message = null;
+}
+
+/// <summary>
+/// Raises the default DoAfterTime for a stand-up attempt for other components to modify it.
+/// </summary>
+/// <param name="DoAfterTime"></param>
+[ByRefEvent]
+public record struct GetStandUpTimeEvent(TimeSpan DoAfterTime);
+
+/// <summary>
+/// Raised when an entity is forcing itself to stand, allows for the stamina damage it is taking to be modified.
+/// This is raised before the stamina damage is taken so it can still fail if the entity does not have enough stamina.
+/// </summary>
+/// <param name="Stamina">The stamina damage the entity will take when it forces itself to stand.</param>
+[ByRefEvent]
+public record struct TryForceStandEvent(float Stamina);
+
+/// <summary>
+///     Raised when you click on the Knocked Down Alert
+/// </summary>
+public sealed partial class KnockedDownAlertEvent : BaseAlertEvent;
+
+/// <summary>
+/// The DoAfterEvent for trying to stand the slow and boring way.
+/// </summary>
+[ByRefEvent]
+[Serializable, NetSerializable]
+public sealed partial class TryStandDoAfterEvent : SimpleDoAfterEvent;
+
+/// <summary>
+/// An event sent by the client to the server to ask it very nicely to perform a forced stand-up.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class ForceStandUpEvent : EntityEventArgs;
+
index dfb3dca5c7d5489c036890f8d9d58d553b4da296..994206afa5c53e3e4d45afa6dde18cbe3ca32250 100644 (file)
     /// Raised when we try to pushback an entity from throwing
     /// </summary>
     public sealed class ThrowPushbackAttemptEvent : CancellableEntityEventArgs {}
+
+    /// <summary>
+    /// Raised on an entity that is being pushed from a thrown entity
+    /// </summary>
+    [ByRefEvent]
+    public record struct ThrowerImpulseEvent()
+    {
+        public bool Push;
+    };
 }
index 2d01810d8bf48707bbcaa4a45a37eac281ac2da0..ceb9cf8bfb2b5405524cb1e584cad79c7182de56 100644 (file)
@@ -222,17 +222,21 @@ public sealed class ThrowingSystem : EntitySystem
             _recoil.KickCamera(user.Value, -direction * 0.04f);
 
         // Give thrower an impulse in the other direction
-        if (pushbackRatio != 0.0f &&
-            physics.Mass > 0f &&
-            TryComp(user.Value, out PhysicsComponent? userPhysics) &&
-            _gravity.IsWeightless(user.Value, userPhysics))
-        {
-            var msg = new ThrowPushbackAttemptEvent();
-            RaiseLocalEvent(uid, msg);
-            const float massLimit = 5f;
+        if (pushbackRatio == 0.0f ||
+            physics.Mass == 0f ||
+            !TryComp(user.Value, out PhysicsComponent? userPhysics))
+            return;
+        var msg = new ThrowPushbackAttemptEvent();
+        RaiseLocalEvent(uid, msg);
 
-            if (!msg.Cancelled)
-                _physics.ApplyLinearImpulse(user.Value, -impulseVector / physics.Mass * pushbackRatio * MathF.Min(massLimit, physics.Mass), body: userPhysics);
-        }
+        if (msg.Cancelled)
+            return;
+
+        var pushEv = new ThrowerImpulseEvent();
+        RaiseLocalEvent(user.Value, ref pushEv);
+        const float massLimit = 5f;
+
+        if (pushEv.Push || _gravity.IsWeightless(user.Value))
+            _physics.ApplyLinearImpulse(user.Value, -impulseVector / physics.Mass * pushbackRatio * MathF.Min(massLimit, physics.Mass), body: userPhysics);
     }
 }
index 7e578657ebd2c8d6844e84057a1da584a14f4e65..624781292abd4937ab56ec29c141ccfec7ec74e0 100644 (file)
@@ -384,11 +384,14 @@ public abstract partial class SharedGunSystem : EntitySystem
         var shotEv = new GunShotEvent(user, ev.Ammo);
         RaiseLocalEvent(gunUid, ref shotEv);
 
-        if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics))
-        {
-            if (_gravity.IsWeightless(user, userPhysics))
-                CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
-        }
+        if (!userImpulse || !TryComp<PhysicsComponent>(user, out var userPhysics))
+            return;
+
+        var shooterEv = new ShooterImpulseEvent();
+        RaiseLocalEvent(user, ref shooterEv);
+
+        if (shooterEv.Push || _gravity.IsWeightless(user, userPhysics))
+            CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
     }
 
     public void Shoot(
@@ -629,6 +632,16 @@ public record struct AttemptShootEvent(EntityUid User, string? Message, bool Can
 [ByRefEvent]
 public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo);
 
+/// <summary>
+/// Raised on an entity after firing a gun to see if any components or systems would allow this entity to be pushed
+/// by the gun they're firing. If true, GunSystem will create an impulse on our entity.
+/// </summary>
+[ByRefEvent]
+public record struct ShooterImpulseEvent()
+{
+    public bool Push;
+};
+
 public enum EffectLayers : byte
 {
     Unshaded,
index adce0bd020fab230233d48fdab2398efcf2d3155..d276d1c13503831df065cf19e8a49509f7b1adfb 100644 (file)
@@ -57,6 +57,7 @@ admin-smite-ghostkick-name = Ghost Kick
 admin-smite-nyanify-name = Cat Ears
 admin-smite-kill-sign-name = Kill Sign
 admin-smite-omni-accent-name = Omni-Accent
+admin-smite-crawler-name = Crawler
 
 ## Smite descriptions
 
@@ -101,6 +102,7 @@ admin-smite-super-bonk-lite-description= Slams them on every single table on the
 admin-smite-terminate-description = Creates a Terminator ghost role with the sole objective of killing them.
 admin-smite-super-slip-description = Slips them really, really hard.
 admin-smite-omni-accent-description = Makes the target speak with almost every accent available.
+admin-smite-crawler-description = Makes the target fall down and be unable to stand up. Remove their hands too for added effect!
 
 ## Tricks descriptions
 
index 45c22abcbce85d6aa62f6ccd3ed6253f7e6b1f42..9593b013216fd4b09e2281b6989e6615b0ec11b5 100644 (file)
@@ -33,6 +33,9 @@ alerts-walking-desc = You are walking, moving at a slow pace.
 alerts-stunned-name = [color=yellow]Stunned[/color]
 alerts-stunned-desc = You're [color=yellow]stunned[/color]! Something is impairing your ability to move or interact with objects.
 
+alerts-knockdown-name = [color=yellow]Knocked Down[/color]
+alerts-knockdown-desc = You're [color=yellow]Knocked Down[/color]! Something has slipped or pushed you over, encumbering your movement.
+
 alerts-handcuffed-name = [color=yellow]Handcuffed[/color]
 alerts-handcuffed-desc = You're [color=yellow]handcuffed[/color] and can't use your hands. If anyone drags you, you won't be able to resist.
 
index 64f9428a139f0df9bcabb0cf75a0ad324d4764c7..f2142efaa2a4904a373c54fa96c3e621127b4d01 100644 (file)
@@ -136,6 +136,7 @@ ui-options-function-move-left = Move Left
 ui-options-function-move-down = Move Down
 ui-options-function-move-right = Move Right
 ui-options-function-walk = Walk
+ui-options-function-toggle-knockdown = Toggle Crawling
 
 ui-options-function-camera-rotate-left = Rotate left
 ui-options-function-camera-rotate-right = Rotate right
index 71d275504d38e036c4f60e2e038c0a2072d3b417..4f822bd33454781a484824247fb51ad08ad3f200 100644 (file)
@@ -1,2 +1,6 @@
 stunnable-component-disarm-success-others = {CAPITALIZE(THE($source))} pushes {THE($target)}!
 stunnable-component-disarm-success = You push {THE($target)}!
+knockdown-component-pushup-failure = You're too exhausted to push yourself up!
+knockdown-component-pushup-success = With a burst of energy you push yourself up!
+knockdown-component-stand-no-room = You try to push yourself to stand up but there's not enough room!
+worm-component-stand-attempt = You try to stand up but you cannot!
index 6710a15fbcff9ad17b92759a583c2dfb3d5f3734..ba86774f9b92df7707cd8ca4abc9e5b63208c035 100644 (file)
   name: alerts-stunned-name
   description: alerts-stunned-desc
 
+- type: alert
+  id: Knockdown
+  clickEvent: !type:KnockedDownAlertEvent
+  icons:
+  - sprite: /Textures/Interface/Alerts/stunnable.rsi
+    state: knocked-down
+  name: alerts-knockdown-name
+  description: alerts-knockdown-desc
+
 - type: alert
   id: Handcuffed
   clickEvent: !type:RemoveCuffsAlertEvent
index c09b5db4f50bd625551f3275555f61bb9fff3514..a426e7aa1c9012499bbf5946963f77aaea4f2614 100644 (file)
   components:
   - type: Clickable
   - type: Slippery
+    staminaDamage: 0
+    frictionStatusTime: 0 # Don't apply friction twice
   - type: Transform
     noRot: true
     anchored: true
index ea24542e5af285724cc42557a017483993620fa6..cb3e5be478a243593247f894d49cc7dbd0e23adb 100644 (file)
   - type: StatusEffects
     allowed:
     - Stun
-    - KnockedDown
     - SlowedDown
     - Flashed
   - type: TypingIndicator
index c3fc1ec9596e1423800501bda08330ca9de4b92c..95dcd84ae65a2b3b4766e216f20abbdfa6ce393e 100644 (file)
@@ -36,7 +36,6 @@
   - type: StatusEffects
     allowed:
     - Stun
-    - KnockedDown
     - SlowedDown
     - Stutter
     - Electrocution
index 382de8e085518a6f85efcb3d13250ad8e1edf3cf..225174243605d5e83b57f6ef3e9cd34c9349ae3e 100644 (file)
@@ -16,7 +16,6 @@
   - type: StatusEffects
     allowed:
     - Stun
-    - KnockedDown
     - SlowedDown
     - Stutter
     - Electrocution
index bb095bfffd696f715f0f9a546e7a2e186948d8ca..eb0ea8161758901a595155f14ad4291b3105e4f7 100644 (file)
@@ -94,7 +94,6 @@
   - type: StatusEffects
     allowed:
     - Stun
-    - KnockedDown
     - SlowedDown
     - Stutter
     - Electrocution
index f2ac678f5218b621562f4a455e084972ef26f44d..9ea12f2ae39a61cac52e30577c4b781e1cd90409 100644 (file)
   - type: StatusEffects
     allowed:
     - Stun
-    - KnockedDown
+    - Friction
     - SlowedDown
     - Stutter
     - Electrocution
index 22ff617809831f9d9c6294e2ca9973e0df5fcb3c..0cd960bed12b5fc9468533378edb0f3497d609f8 100644 (file)
   - type: TrashOnSolutionEmpty
     solution: drink
 
+- type: entity
+  parent: BluespaceBeaker
+  id: BottomlessLube
+  name: bottomless lube beaker
+  suffix: DEBUG
+  description: This anomalous beaker infinitely produces space lube and as such is to be closely guarded such that it doesn't fall in the wrong hands.
+  components:
+  - type: SolutionContainerManager
+    solutions:
+      beaker:
+        maxVol: 1000
+        reagents:
+        - ReagentId: SpaceLube
+          Quantity: 1000
+  - type: SolutionRegeneration
+    solution: beaker
+    generated:
+      reagents:
+      - ReagentId: SpaceLube
+        Quantity: 200
 
 # Mopwata
 - type: weightedRandomFillSolution
index 6f157d4fed99909e55c4b0d049500b8f1d468007..eb1e4d524a752f3b58610b2168402e9f5bb4394f 100644 (file)
@@ -17,7 +17,7 @@
           Quantity: 5
   - type: Slippery
     slipData:
-      paralyzeTime: 3
+      knockdownTime: 3
       launchForwardsMultiplier: 3
   - type: StepTrigger
     intersectRatio: 0.2
index 6ed36af7733a0f2fb796a68279043ede36cb54f8..d0be38d5045ee8cafc2c011ec0d33ae98bb70e44 100644 (file)
   - type: UseDelay
     delay: 0.8
   - type: Slippery
+    staminaDamage: 0
     slipData:
-      paralyzeTime: 0
+      stunTime: 0
+      knockdownTime: 0
       launchForwardsMultiplier: 0
     slipSound:
       collection: Parp
index dc88de3ed66c16df001001fa4d282e7157e1f2b6..01283128e6d626422eb9b684061b3ea66528af88 100644 (file)
   - type: SolutionContainerVisuals
     fillBaseName: syndie-
   - type: Slippery
+    staminaDamage: 50
     slipData:
-      paralyzeTime: 3
+      knockdownTime: 3
       launchForwardsMultiplier: 3
   - type: Item
     heldPrefix: syndie
     layers:
     - state: syndie-soaplet
   - type: Slippery
-    slipData:
-      paralyzeTime: 1.5 # these things are tiny
-      launchForwardsMultiplier: 1.5
   - type: StepTrigger
     intersectRatio: 0.04
   - type: Item
   - type: SolutionContainerVisuals
     fillBaseName: omega-
   - type: Slippery
+    staminaDamage: 50
     slipData:
-      paralyzeTime: 5.0
+      knockdownTime: 5.0
       launchForwardsMultiplier: 3.0
   - type: Item
     heldPrefix: omega
index d5f4681a9bcaec42bb2529626330d3eba4034e40..cdd3edd93f9e3b66bb72ea90e78284ca5b0ffde1 100644 (file)
   name: taser
   parent: [BaseWeaponBatterySmall, BaseSecurityContraband]
   id: WeaponTaser
-  description: A low-capacity, energy-based stun gun used by security teams to subdue targets at range.
+  description: A low-capacity, energy-based stun gun used by security teams to subdue targets at close range.
   components:
   - type: Tag
     tags:
     zeroVisible: true
   - type: Appearance
 
+- type: entity
+  name: elite taser
+  parent: [ BaseCentcommContraband, WeaponTaser ]
+  id: WeaponTaserSuper
+  suffix: ADMEME
+  description: A low-capacity, energy-based stun gun used by elite security teams to disable even the toughest of targets.
+  components:
+  - type: Gun
+    fireRate: 0.5
+    soundGunshot:
+      path: /Audio/Effects/tesla_collapse.ogg # The wrath of god...
+      params:
+        volume: -6
+  - type: ProjectileBatteryAmmoProvider
+    proto: BulletTaserSuper
+    fireCost: 200
+
 - type: entity
   name: antique laser pistol
   parent: [BaseWeaponBatterySmall, BaseGrandTheftContraband]
index 7c94dd65ccc8d320224d6a690a344c38586348c9..7a15d6520e6adc5a9b693cd0227b1e3dcf6a01be 100644 (file)
   - type: Projectile
     damage:
       types:
-        Heat: 5
+        Shock: 1
     soundHit:
       path: "/Audio/Weapons/Guns/Hits/taser_hit.ogg"
     forceSound: true
+  - type: TimedDespawn
+    lifetime: 0.170 # Very short range
+  - type: StunOnCollide
+    stunAmount: 0
+    knockdownAmount: 2.5 # Enough to subdue and follow up with a stun batong
+    slowdownAmount: 2.5
+    walkSpeedModifier: 0.5
+    sprintSpeedModifier: 0.5
+
+- type: entity
+  parent: BulletTaser
+  id: BulletTaserSuper
+  categories: [ HideSpawnMenu ]
+  name: taser bolt
+  description: If you can see this, you've probably been stun-meta'd
+  components:
+  - type: Sprite
+    noRot: true
+    sprite: Structures/Power/Generation/Tesla/energy_miniball.rsi
+    color: "#ffff33"
+    layers:
+    - state: tesla_projectile
+      shader: unshaded
+  - type: PointLight
+    enabled: true
+    color: "#ffff33"
+    radius: 2.0
+    energy: 7.01
+  - type: TimedDespawn
+    lifetime: 1.0 # Not so short range
   - type: StunOnCollide
     stunAmount: 5
-    knockdownAmount: 5
+    knockdownAmount: 10
+    slowdownAmount: 10
+    walkSpeedModifier: 0.5
+    sprintSpeedModifier: 0.5
 
 - type: entity
   name : disabler bolt
index bb2aa9de93b3aeef59a71e09d65e29a81e6516b3..444600023a865396487a6f170242c718754bbfd1 100644 (file)
   components:
   - type: DrowsinessStatusEffect
 
+# Makes you more slippery, or perhaps less slippery.
+- type: entity
+  parent: MobStatusEffectBase
+  id: StatusEffectFriction
+  name: friction
+  components:
+  - type: FrictionStatusEffect
+
 # Adds drugs overlay
 - type: entity
   parent: MobStatusEffectBase
   id: StatusEffectSeeingRainbow
   name: hallucinations
   components:
-  - type: SeeingRainbowsStatusEffect
\ No newline at end of file
+  - type: SeeingRainbowsStatusEffect
index 33c0a0e86f61af07460b79c4f0a43c29e2e40b83..90bcb23f86cefd3f3c2bc84929ed091aa3ec50c6 100644 (file)
@@ -83,7 +83,7 @@
   meltingPoint: 18.2
   tileReactions:
   - !type:SpillTileReaction
-  friction: 0.0
+  friction: 0.05
 
 - type: reagent
   id: SpaceGlue
index 7c96b88c22d19d48acf81e7da5b3c771c2287dfb..8ae32928a39e0a8ac539147716b85a6581daf32f 100644 (file)
@@ -8,10 +8,6 @@
   id: Stun
   alert: Stun
 
-- type: statusEffect
-  id: KnockedDown
-  alert: Stun
-
 - type: statusEffect
   id: SlowedDown
 
diff --git a/Resources/Textures/Interface/Alerts/stunnable.rsi/knocked-down.png b/Resources/Textures/Interface/Alerts/stunnable.rsi/knocked-down.png
new file mode 100644 (file)
index 0000000..3af4edd
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/stunnable.rsi/knocked-down.png differ
diff --git a/Resources/Textures/Interface/Alerts/stunnable.rsi/meta.json b/Resources/Textures/Interface/Alerts/stunnable.rsi/meta.json
new file mode 100644 (file)
index 0000000..abae580
--- /dev/null
@@ -0,0 +1,14 @@
+{
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Made by Princess Chesseballs, Pronana on Github https://github.com/Pronana",
+    "size": {
+        "x": 32,
+        "y": 32
+    },
+    "states": [
+        {
+            "name": "knocked-down"
+        }
+    ]
+}
index 5c480fdc6d55d35c1f0a6c1ce894f3d7d7a18965..1d86e4c5e7dc43609ba57c5beb7bc23f970b196e 100644 (file)
@@ -190,7 +190,7 @@ binds:
   mod1: Alt
 - function: OpenCharacterMenu
   type: State
-  key: C
+  key: U
 - function: OpenEmotesMenu
   type: State
   key: Y
@@ -525,6 +525,9 @@ binds:
 - function: Arcade3
   type: State
   key: Z
+- function: ToggleKnockdown
+  type: State
+  key: C
 - function: OpenAbilitiesMenu
   type: State
   key: K