diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 5b7614780b..6b279cb12c 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -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); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 9ef3def541..ee72a91fb7 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -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); diff --git a/Content.Client/Stunnable/StunSystem.cs b/Content.Client/Stunnable/StunSystem.cs index cb59eda482..6947b80664 100644 --- a/Content.Client/Stunnable/StunSystem.cs +++ b/Content.Client/Stunnable/StunSystem.cs @@ -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(OnComponentInit); SubscribeLocalEvent(OnAppearanceChanged); + + CommandBinds.Builder + .BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true), typeof(SharedInteractionSystem)) + .Register(); + } + + private bool OnUseSecondary(in PointerInputCmdHandler.PointerInputCmdArgs args) + { + if (args.Session?.AttachedEntity is not {Valid: true} uid) + return false; + + if (args.EntityUid != uid || !HasComp(uid) || !_combat.IsInCombatMode(uid)) + return false; + + RaisePredictiveEvent(new ForceStandUpEvent()); + return true; } /// diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index b764c7f68d..1bc4b65999 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -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(args.Target); + }, + Impact = LogImpact.Extreme, + Message = string.Join(": ", crawlerName, Loc.GetString("admin-smite-crawler-description")) + }; + args.Verbs.Add(crawler); } } diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs index 1176ce54c7..63ee75618c 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.cs @@ -338,6 +338,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem // Ensure we actually have the component EnsureComp(entity); + EnsureComp(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(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 diff --git a/Content.Server/Stunnable/Components/StunOnCollideComponent.cs b/Content.Server/Stunnable/Components/StunOnCollideComponent.cs index 1ce1cbea57..363fb78b75 100644 --- a/Content.Server/Stunnable/Components/StunOnCollideComponent.cs +++ b/Content.Server/Stunnable/Components/StunOnCollideComponent.cs @@ -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; + /// + /// How long we are stunned for + /// + [DataField] + public TimeSpan StunAmount; - [DataField("knockdownAmount")] - public int KnockdownAmount; + /// + /// How long we are knocked down for + /// + [DataField] + public TimeSpan KnockdownAmount; - [DataField("slowdownAmount")] - public int SlowdownAmount; + /// + /// How long we are slowed down for + /// + [DataField] + public TimeSpan SlowdownAmount; - [DataField("walkSpeedMultiplier")] - public float WalkSpeedMultiplier = 1f; + /// + /// Multiplier for a mob's walking speed + /// + [DataField] + public float WalkSpeedModifier = 1f; - [DataField("runSpeedMultiplier")] - public float RunSpeedMultiplier = 1f; + /// + /// Multiplier for a mob's sprinting speed + /// + [DataField] + public float SprintSpeedModifier = 1f; + + /// + /// Refresh Stun or Slowdown on hit + /// + [DataField] + public bool Refresh = true; + + /// + /// Should the entity try and stand automatically after being knocked down? + /// + [DataField] + public bool AutoStand = true; /// /// Fixture we track for the collision. diff --git a/Content.Server/Stunnable/StunSystem.cs b/Content.Server/Stunnable/StunSystem.cs new file mode 100644 index 0000000000..5637ad54a7 --- /dev/null +++ b/Content.Server/Stunnable/StunSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared.Stunnable; + +namespace Content.Server.Stunnable; + +public sealed class StunSystem : SharedStunSystem; + diff --git a/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs b/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs index ae10957bd8..b998270829 100644 --- a/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs +++ b/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs @@ -22,17 +22,14 @@ namespace Content.Server.Stunnable private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target) { + if (!TryComp(target, out var status)) + return; - if (TryComp(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 index 8f793a0a13..0000000000 --- a/Content.Server/Stunnable/Systems/StunSystem.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Content.Shared.Stunnable; - -namespace Content.Server.Stunnable -{ - public sealed class StunSystem : SharedStunSystem - {} -} diff --git a/Content.Shared/Bed/Sleep/SleepingSystem.cs b/Content.Shared/Bed/Sleep/SleepingSystem.cs index cdff4b7fd7..3534fb88d5 100644 --- a/Content.Shared/Bed/Sleep/SleepingSystem.cs +++ b/Content.Shared/Bed/Sleep/SleepingSystem.cs @@ -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(ent); EnsureComp(ent); diff --git a/Content.Shared/Cuffs/Components/HandcuffComponent.cs b/Content.Shared/Cuffs/Components/HandcuffComponent.cs index 289f587239..9465d605ed 100644 --- a/Content.Shared/Cuffs/Components/HandcuffComponent.cs +++ b/Content.Shared/Cuffs/Components/HandcuffComponent.cs @@ -33,6 +33,24 @@ public sealed partial class HandcuffComponent : Component [DataField, ViewVariables(VVAccess.ReadWrite)] public float StunBonus = 2f; + /// + /// Modifier for the amount of time it takes an entity to stand up if cuffed. + /// + [DataField] + public float StandupMod = 5f; + + /// + /// Modifier to the speed of an entity who is cuffed, does not stack with KnockedMovementMod + /// + [DataField] + public float MovementMod = 1f; + + /// + /// Modifier to the knocked down speed of an entity who is cuffed + /// + [DataField] + public float KnockedMovementMod = 0.4f; + /// /// Will the cuffs break when removed? /// diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index 3b0f6c8a30..931028052c 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -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 _cuffQuery; + public override void Initialize() { base.Initialize(); + _cuffQuery = GetEntityQuery(); + SubscribeLocalEvent(OnHandCountChanged); SubscribeLocalEvent(OnUncuffAttempt); @@ -89,6 +95,9 @@ namespace Content.Shared.Cuffs SubscribeLocalEvent(OnCuffMeleeHit); SubscribeLocalEvent(OnAddCuffDoAfter); SubscribeLocalEvent(OnCuffVirtualItemDeleted); + SubscribeLocalEvent(OnCuffableStandupArgs); + SubscribeLocalEvent(OnCuffableKnockdownRefresh); + SubscribeLocalEvent(OnRefreshMovementSpeedModifiers); } private void CheckInteract(Entity 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 } } + /// + /// Takes longer to stand up when cuffed + /// + private void OnCuffableStandupArgs(Entity ent, ref GetStandUpTimeEvent time) + { + if (!HasComp(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 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 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); + } + /// /// Adds virtual cuff items to the user's hands. /// @@ -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) diff --git a/Content.Shared/Damage/Components/StaminaComponent.cs b/Content.Shared/Damage/Components/StaminaComponent.cs index 3a85f3f8dc..b343b24403 100644 --- a/Content.Shared/Damage/Components/StaminaComponent.cs +++ b/Content.Shared/Damage/Components/StaminaComponent.cs @@ -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; /// - /// How much stamina damage is required to entire stam crit. + /// How much stamina damage is required to enter stam crit. /// [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; + /// + /// This is how much stamina damage a mob takes when it forces itself to stand up before modifiers + /// + [DataField, AutoNetworkedField] + public float ForceStandStamina = 10f; + + /// + /// What sound should play when we successfully stand up + /// + [DataField, AutoNetworkedField] + public SoundSpecifier ForceStandSuccessSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); + /// /// Thresholds that determine an entity's slowdown as a function of stamina damage. /// diff --git a/Content.Shared/Damage/Systems/SharedStaminaSystem.cs b/Content.Shared/Damage/Systems/SharedStaminaSystem.cs index ae4562e690..6687ecd7df 100644 --- a/Content.Shared/Damage/Systems/SharedStaminaSystem.cs +++ b/Content.Shared/Damage/Systems/SharedStaminaSystem.cs @@ -234,7 +234,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem /// /// Tries to take stamina damage without raising the entity over the crit threshold. /// - 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 index 0000000000..af0ed1c1aa --- /dev/null +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.EventListeners.cs @@ -0,0 +1,31 @@ +using Content.Shared.Hands.Components; +using Content.Shared.Stunnable; + +namespace Content.Shared.Hands.EntitySystems; + +/// +/// This is for events that don't affect normal hand functions but do care about hands. +/// +public abstract partial class SharedHandsSystem +{ + private void InitializeEventListeners() + { + SubscribeLocalEvent(OnStandupArgs); + } + + /// + /// Reduces the time it takes to stand up based on the number of hands we have available. + /// + private void OnStandupArgs(Entity ent, ref GetStandUpTimeEvent time) + { + if (!HasComp(ent)) + return; + + var hands = GetEmptyHandCount(ent.Owner); + + if (hands == 0) + return; + + time.DoAfterTime *= (float)ent.Comp.Count / (hands + ent.Comp.Count); + } +} diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs index 4f259a3bde..af5e82f417 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs @@ -36,6 +36,7 @@ public abstract partial class SharedHandsSystem InitializeDrop(); InitializePickup(); InitializeRelay(); + InitializeEventListeners(); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnMapInit); @@ -166,6 +167,26 @@ public abstract partial class SharedHandsSystem return false; } + /// + /// Does this entity have any empty hands, and how many? + /// + public int GetEmptyHandCount(Entity 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; + } + /// /// Attempts to retrieve the item held in the entity's active hand. /// diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index f10c99af2f..cd5f4cff7b 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -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 index 0000000000..33c5389fd3 --- /dev/null +++ b/Content.Shared/Movement/Components/FrictionStatusEffectComponent.cs @@ -0,0 +1,23 @@ +using Content.Shared.Movement.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Movement.Components; + +/// +/// This is used to apply a friction modifier to an entity temporarily +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(MovementModStatusSystem))] +public sealed partial class FrictionStatusEffectComponent : Component +{ + /// + /// Friction modifier applied as a status. + /// + [DataField, AutoNetworkedField] + public float FrictionModifier = 1f; + + /// + /// Acceleration modifier applied as a status. + /// + [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 index 0000000000..a717659fef --- /dev/null +++ b/Content.Shared/Movement/Components/WormComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Movement.Components; + +/// +/// 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. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class WormComponent : Component +{ + /// + /// Modifier for KnockedDown Friction, or in this components case, all friction + /// + [DataField, AutoNetworkedField] + public float FrictionModifier = 1f; + + /// + /// Modifier for KnockedDown Movement, or in this components case, all movement + /// + [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 index 0000000000..4fd6467947 --- /dev/null +++ b/Content.Shared/Movement/Systems/MovementModStatusSystem.cs @@ -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; + +/// +/// This handles the application of movement and friction modifiers to an entity as status effects. +/// +public sealed class MovementModStatusSystem : EntitySystem +{ + public static readonly EntProtoId StatusEffectFriction = "StatusEffectFriction"; + + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + [Dependency] private readonly StatusEffectsSystem _status = default!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnFrictionStatusEffectRemoved); + SubscribeLocalEvent>(OnRefreshFrictionStatus); + SubscribeLocalEvent>(OnRefreshTileFrictionStatus); + } + + private void OnRefreshFrictionStatus(Entity ent, ref StatusEffectRelayedEvent args) + { + var ev = args.Args; + ev.ModifyFriction(ent.Comp.FrictionModifier); + ev.ModifyAcceleration(ent.Comp.AccelerationModifier); + args.Args = ev; + } + + private void OnRefreshTileFrictionStatus(Entity ent, ref StatusEffectRelayedEvent args) + { + var ev = args.Args; + ev.Modifier *= ent.Comp.FrictionModifier; + args.Args = ev; + } + + /// + /// Applies a friction de-buff to the player. + /// + 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); + } + } + + /// + /// Sets the friction status modifiers for a status effect. + /// + /// The status effect entity we're modifying. + /// The friction modifier we're applying. + /// The entity the status effect is attached to that we need to refresh. + private bool TrySetFrictionStatus(Entity status, float friction, EntityUid entity) + { + return TrySetFrictionStatus(status, friction, friction, entity); + } + + /// + /// Sets the friction status modifiers for a status effect. + /// + /// The status effect entity we're modifying. + /// The friction modifier we're applying. + /// The acceleration modifier we're applying + /// The entity the status effect is attached to that we need to refresh. + private bool TrySetFrictionStatus(Entity 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 entity, ref StatusEffectRemovedEvent args) + { + TrySetFrictionStatus(entity!, 1f, args.Target); + } +} diff --git a/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs b/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs index b5bb633491..4584e4401a 100644 --- a/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs +++ b/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs @@ -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(OnModMapInit); + SubscribeLocalEvent(OnDowned); + SubscribeLocalEvent(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 entity, ref DownedEvent args) + { + RefreshFrictionModifiers(entity); + RefreshMovementSpeedModifiers(entity); + } + + private void OnStand(Entity 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 index 0000000000..8dc7f86a08 --- /dev/null +++ b/Content.Shared/Movement/Systems/WormSystem.cs @@ -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; + +/// +/// This handles the worm component +/// +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(OnStandAttempt); + SubscribeLocalEvent(OnKnockedDownRefresh); + SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + EnsureComp(ent, out var knocked); + _alerts.ShowAlert(ent, SharedStunSystem.KnockdownAlert); + _stun.SetAutoStand((ent, knocked)); + } + + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + RemComp(ent); + } + + private void OnStandAttempt(Entity 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 ent, ref KnockedDownRefreshEvent args) + { + args.FrictionModifier *= ent.Comp.FrictionModifier; + args.SpeedModifier *= ent.Comp.SpeedModifier; + } +} diff --git a/Content.Shared/Slippery/SlidingComponent.cs b/Content.Shared/Slippery/SlidingComponent.cs index e48c0f2e9e..200d9570ab 100644 --- a/Content.Shared/Slippery/SlidingComponent.cs +++ b/Content.Shared/Slippery/SlidingComponent.cs @@ -8,15 +8,15 @@ namespace Content.Shared.Slippery; [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] public sealed partial class SlidingComponent : Component { - /// - /// A list of SuperSlippery entities the entity with this component is colliding with. - /// - [DataField, AutoNetworkedField] - public HashSet CollidingEntities = new (); - /// /// The friction modifier that will be applied to any friction calculations. /// [DataField, AutoNetworkedField] public float FrictionModifier; + + /// + /// Hashset of contacting entities. + /// + [DataField] + public HashSet Contacting = new(); } diff --git a/Content.Shared/Slippery/SlidingSystem.cs b/Content.Shared/Slippery/SlidingSystem.cs index dde9bb6ab0..e0084c11ab 100644 --- a/Content.Shared/Slippery/SlidingSystem.cs +++ b/Content.Shared/Slippery/SlidingSystem.cs @@ -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 _slipperyQuery; + public override void Initialize() { base.Initialize(); + _slipperyQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentShutdown); SubscribeLocalEvent(OnStand); SubscribeLocalEvent(OnStartCollide); SubscribeLocalEvent(OnEndCollide); + SubscribeLocalEvent(OnRefreshFrictionModifiers); + SubscribeLocalEvent(OnThrowerImpulse); + SubscribeLocalEvent(ShooterImpulseEvent); + } + + /// + /// When the component is first added, calculate the friction modifier we need. + /// Don't do this more than once to avoid mispredicts. + /// + private void OnComponentInit(Entity entity, ref ComponentInit args) + { + if (CalculateSlidingModifier(entity)) + _speedModifierSystem.RefreshFrictionModifiers(entity); + } + + /// + /// When the component is removed, refresh friction modifiers and set ours to 1 to avoid causing issues. + /// + private void OnComponentShutdown(Entity entity, ref ComponentShutdown args) + { + entity.Comp.FrictionModifier = 1; + _speedModifierSystem.RefreshFrictionModifiers(entity); } /// @@ -25,28 +59,81 @@ public sealed class SlidingSystem : EntitySystem } /// - /// Sets friction to 0 if colliding with a SuperSlippery Entity. + /// Updates friction when we collide with a slippery entity /// - private void OnStartCollide(EntityUid uid, SlidingComponent component, ref StartCollideEvent args) + private void OnStartCollide(Entity entity, ref StartCollideEvent args) { - if (!TryComp(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); } /// - /// Set friction to normal when ending collision with a SuperSlippery entity. + /// Update friction when we stop colliding with a slippery entity /// - private void OnEndCollide(EntityUid uid, SlidingComponent component, ref EndCollideEvent args) + private void OnEndCollide(Entity 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(uid); + if (!CalculateSlidingModifier(entity, args.OtherEntity)) + { + RemComp(entity); + return; + } - Dirty(uid, component); + _speedModifierSystem.RefreshFrictionModifiers(entity); + } + + /// + /// Gets contacting slippery entities and averages their friction modifiers. + /// + private bool CalculateSlidingModifier(Entity 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 entity, ref RefreshFrictionModifiersEvent args) + { + args.ModifyFriction(entity.Comp.FrictionModifier); + args.ModifyAcceleration(entity.Comp.FrictionModifier); + } + + private void OnThrowerImpulse(Entity entity, ref ThrowerImpulseEvent args) + { + args.Push = true; + } + + private void ShooterImpulseEvent(Entity entity, ref ShooterImpulseEvent args) + { + args.Push = true; } } diff --git a/Content.Shared/Slippery/SlipperyComponent.cs b/Content.Shared/Slippery/SlipperyComponent.cs index f6b6dd0b35..7b6cd90800 100644 --- a/Content.Shared/Slippery/SlipperyComponent.cs +++ b/Content.Shared/Slippery/SlipperyComponent.cs @@ -21,6 +21,25 @@ namespace Content.Shared.Slippery [Access(Other = AccessPermissions.ReadWriteExecute)] public SoundSpecifier SlipSound = new SoundPathSpecifier("/Audio/Effects/slip.ogg"); + /// + /// Should this component's friction factor into sliding friction? + /// + [DataField, AutoNetworkedField] + public bool AffectsSliding; + + /// + /// How long should this component apply the FrictionStatusComponent? + /// Note: This does stack with SlidingComponent since they are two separate Components + /// + [DataField, AutoNetworkedField] + public TimeSpan FrictionStatusTime = TimeSpan.FromSeconds(0.5f); + + /// + /// How much stamina damage should this component do on slip? + /// + [DataField, AutoNetworkedField] + public float StaminaDamage = 25f; + /// /// Loads the data needed to determine how slippery something is. /// @@ -34,10 +53,22 @@ namespace Content.Shared.Slippery public sealed partial class SlipperyEffectEntry { /// - /// How many seconds the mob will be paralyzed for. + /// How many seconds the mob will be stunned for. /// [DataField] - public TimeSpan ParalyzeTime = TimeSpan.FromSeconds(1.5); + public TimeSpan StunTime = TimeSpan.FromSeconds(0.5); + + /// + /// How many seconds the mob will be knocked down for. + /// + [DataField] + public TimeSpan KnockdownTime = TimeSpan.FromSeconds(1.5); + + /// + /// Should the slipped entity try to stand up when Knockdown ends? + /// + [DataField] + public bool AutoStand = true; /// /// 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. /// [DataField] - public float SlipFriction; + public float SlipFriction = 0.5f; } } diff --git a/Content.Shared/Slippery/SlipperySystem.cs b/Content.Shared/Slippery/SlipperySystem.cs index 40d12d9ebe..f7e3da8edc 100644 --- a/Content.Shared/Slippery/SlipperySystem.cs +++ b/Content.Shared/Slippery/SlipperySystem.cs @@ -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(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(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(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}"); } } diff --git a/Content.Shared/Standing/StandingStateComponent.cs b/Content.Shared/Standing/StandingStateComponent.cs index 0270872235..c6252d969a 100644 --- a/Content.Shared/Standing/StandingStateComponent.cs +++ b/Content.Shared/Standing/StandingStateComponent.cs @@ -14,6 +14,25 @@ namespace Content.Shared.Standing [DataField, AutoNetworkedField] public bool Standing { get; set; } = true; + /// + /// Time it takes us to stand up + /// + [DataField, AutoNetworkedField] + public TimeSpan StandTime = TimeSpan.FromSeconds(2); + + /// + /// Default Friction modifier for knocked down players. + /// Makes them accelerate and deccelerate slower. + /// + [DataField, AutoNetworkedField] + public float FrictionModifier = 0.4f; + + /// + /// Base modifier to the maximum movement speed of a knocked down mover. + /// + [DataField, AutoNetworkedField] + public float SpeedModifier = 0.3f; + /// /// List of fixtures that had their collision mask changed when the entity was downed. /// Required for re-adding the collision mask. diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs index 86d2b961eb..7177568163 100644 --- a/Content.Shared/Standing/StandingStateSystem.cs +++ b/Content.Shared/Standing/StandingStateSystem.cs @@ -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(OnMobCollide); SubscribeLocalEvent(OnMobTargetCollide); + SubscribeLocalEvent(OnRefreshMovementSpeedModifiers); + SubscribeLocalEvent(OnRefreshFrictionModifiers); + SubscribeLocalEvent(OnTileFriction); } private void OnMobTargetCollide(Entity ent, ref AttemptMobTargetCollideEvent args) @@ -40,6 +44,27 @@ public sealed class StandingStateSystem : EntitySystem } } + private void OnRefreshMovementSpeedModifiers(Entity entity, ref RefreshMovementSpeedModifiersEvent args) + { + if (!entity.Comp.Standing) + args.ModifySpeed(entity.Comp.FrictionModifier); + } + + private void OnRefreshFrictionModifiers(Entity entity, ref RefreshFrictionModifiersEvent args) + { + if (entity.Comp.Standing) + return; + + args.ModifyFriction(entity.Comp.FrictionModifier); + args.ModifyAcceleration(entity.Comp.FrictionModifier); + } + + private void OnTileFriction(Entity 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)) diff --git a/Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs b/Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs index 224a3dc4a4..dddcbc660c 100644 --- a/Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs +++ b/Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs @@ -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(RelayStatusEffectEvent); SubscribeLocalEvent(RelayStatusEffectEvent); + + SubscribeLocalEvent(RefRelayStatusEffectEvent); + SubscribeLocalEvent(RefRelayStatusEffectEvent); } private void RefRelayStatusEffectEvent(EntityUid uid, StatusEffectContainerComponent component, ref T args) where T : struct diff --git a/Content.Shared/Stunnable/KnockedDownComponent.cs b/Content.Shared/Stunnable/KnockedDownComponent.cs index e4f11b8cda..1ee2255c22 100644 --- a/Content.Shared/Stunnable/KnockedDownComponent.cs +++ b/Content.Shared/Stunnable/KnockedDownComponent.cs @@ -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; + /// + /// Game time that we can stand up. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] + public TimeSpan NextUpdate; - [DataField("helpAttemptSound")] - public SoundSpecifier StunAttemptSound = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); + /// + /// Should we try to stand up? + /// + [DataField, AutoNetworkedField] + public bool AutoStand = true; - [ViewVariables, AutoNetworkedField] - public float HelpTimer = 0f; + /// + /// The Standing Up DoAfter. + /// + [DataField, AutoNetworkedField] + public ushort? DoAfterId; + + /// + /// Friction modifier for knocked down players. + /// Makes them accelerate and deccelerate slower. + /// + [DataField, AutoNetworkedField] + public float FrictionModifier = 1f; // Should add a friction modifier to slipping to compensate for this + + /// + /// Modifier to the maximum movement speed of a knocked down mover. + /// + [DataField, AutoNetworkedField] + public float SpeedModifier = 1f; + + /// + /// How long does it take us to get up? + /// + [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 index 0000000000..bd8347ecbf --- /dev/null +++ b/Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs @@ -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; + +/// +/// This contains the knockdown logic for the stun system for organization purposes. +/// +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 KnockdownAlert = "Knockdown"; + + private void InitializeKnockdown() + { + SubscribeLocalEvent(OnRejuvenate); + + // Startup and Shutdown + SubscribeLocalEvent(OnKnockInit); + SubscribeLocalEvent(OnKnockShutdown); + + // Action blockers + SubscribeLocalEvent(OnBuckleAttempt); + SubscribeLocalEvent(OnStandUpAttempt); + + // Updating movement a friction + SubscribeLocalEvent(OnRefreshKnockedSpeed); + SubscribeLocalEvent(OnRefreshFriction); + SubscribeLocalEvent(OnKnockedTileFriction); + SubscribeLocalEvent(OnHandEquipped); + SubscribeLocalEvent(OnHandUnequipped); + + // DoAfter event subscriptions + SubscribeLocalEvent(OnStandDoAfter); + + // Knockdown Extenders + SubscribeLocalEvent(OnDamaged); + + // Handling Alternative Inputs + SubscribeAllEvent(OnForceStandup); + SubscribeLocalEvent(OnKnockedDownAlert); + + CommandBinds.Builder + .Bind(ContentKeyFunctions.ToggleKnockdown, InputCmdHandler.FromDelegate(HandleToggleKnockdown, handle: false)) + .Register(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + + 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 entity, ref RejuvenateEvent args) + { + SetKnockdownTime(entity, GameTiming.CurTime); + + if (entity.Comp.AutoStand) + RemComp(entity); + } + + #region Startup and Shutdown + + private void OnKnockInit(Entity entity, ref ComponentInit args) + { + // Other systems should handle dropping held items... + _standingState.Down(entity, true, false); + RefreshKnockedMovement(entity); + } + + private void OnKnockShutdown(Entity 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 + + /// + /// Sets the autostand property of a on an entity to true or false and dirties it. + /// Defaults to false. + /// + /// Entity we want to edit the data field of. + /// What we want to set the data field to. + public void SetAutoStand(Entity entity, bool autoStand = false) + { + if (!Resolve(entity, ref entity.Comp, false)) + return; + + entity.Comp.AutoStand = autoStand; + DirtyField(entity, entity.Comp, nameof(entity.Comp.AutoStand)); + } + + /// + /// Cancels the DoAfter of an entity with the who is trying to stand. + /// + /// Entity who we are canceling the DoAfter for. + public void CancelKnockdownDoAfter(Entity 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)); + } + + /// + /// Updates the knockdown timer of a knocked down entity with a given inputted time, then dirties the time. + /// + /// Entity who's knockdown time we're updating. + /// The time we're updating with. + /// Whether we're resetting the timer or adding to the current timer. + public void UpdateKnockdownTime(Entity entity, TimeSpan time, bool refresh = true) + { + if (refresh) + RefreshKnockdownTime(entity, time); + else + AddKnockdownTime(entity, time); + } + + /// + /// Sets the next update datafield of an entity's to a specific time. + /// + /// Entity whose timer we're updating + /// The exact time we're setting the next update to. + public void SetKnockdownTime(Entity entity, TimeSpan time) + { + entity.Comp.NextUpdate = time; + DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.NextUpdate)); + } + + /// + /// Refreshes the amount of time an entity is knocked down to the inputted time, if it is greater than + /// the current time left. + /// + /// Entity whose timer we're updating + /// The time we want them to be knocked down for. + public void RefreshKnockdownTime(Entity entity, TimeSpan time) + { + var knockedTime = GameTiming.CurTime + time; + if (entity.Comp.NextUpdate < knockedTime) + SetKnockdownTime(entity, knockedTime); + } + + /// + /// Adds our inputted time to an entity's knocked down timer, or sets it to the given time if their timer has expired. + /// + /// Entity whose timer we're updating + /// The time we want to add to their knocked down timer. + public void AddKnockdownTime(Entity 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)); + } + + /// + /// Checks if an entity is able to stand, returns true if it can, returns false if it cannot. + /// + /// Entity we're checking + /// Returns whether the entity is able to stand + public bool CanStand(Entity 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(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 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; + } + + /// + /// A variant of used when we're actually trying to stand. + /// Main difference is this one affects autostand datafields and also displays popups. + /// + /// Entity we're checking + /// Returns whether the entity is able to stand + public bool TryStand(Entity 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 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 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(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(entity); + + _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(entity):user} has force stood up from knockdown."); + } + + private void OnKnockedDownAlert(Entity 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 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; + } + + /// + /// 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. + /// + private bool IntersectingStandingColliders(Entity 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(); + var xformQuery = GetEntityQuery(); + + 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 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 entity, ref StandAttemptEvent args) + { + if (entity.Comp.LifeStage <= ComponentLifeStage.Running) + args.Cancel(); + } + + private void OnBuckleAttempt(Entity entity, ref BuckleAttemptEvent args) + { + if (args.User == entity && entity.Comp.NextUpdate > GameTiming.CurTime) + args.Cancelled = true; + } + + #endregion + + #region DoAfter + + private void OnStandDoAfter(Entity entity, ref TryStandDoAfterEvent args) + { + entity.Comp.DoAfterId = null; + + if (args.Cancelled || StandingBlocked(entity)) + { + DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.DoAfterId)); + return; + } + + RemComp(entity); + + _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(entity):user} has stood up from knockdown."); + } + + #endregion + + #region Movement and Friction + + private void RefreshKnockedMovement(Entity 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 entity, ref RefreshMovementSpeedModifiersEvent args) + { + args.ModifySpeed(entity.Comp.SpeedModifier); + } + + private void OnKnockedTileFriction(Entity entity, ref TileFrictionEvent args) + { + args.Modifier *= entity.Comp.FrictionModifier; + } + + private void OnRefreshFriction(Entity entity, ref RefreshFrictionModifiersEvent args) + { + args.ModifyFriction(entity.Comp.FrictionModifier); + args.ModifyAcceleration(entity.Comp.FrictionModifier); + } + + private void OnHandEquipped(Entity entity, ref DidEquipHandEvent args) + { + RefreshKnockedMovement(entity); + } + + private void OnHandUnequipped(Entity entity, ref DidUnequipHandEvent args) + { + RefreshKnockedMovement(entity); + } + + #endregion +} diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs index c46dd10ed7..2f75b71f56 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -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!; - /// - /// Friction modifier for knocked down players. - /// Doesn't make them faster but makes them slow down... slower. - /// - public const float KnockDownModifier = 0.2f; - public override void Initialize() { - SubscribeLocalEvent(OnKnockInit); - SubscribeLocalEvent(OnKnockShutdown); - SubscribeLocalEvent(OnStandAttempt); - SubscribeLocalEvent(OnSlowInit); SubscribeLocalEvent(OnSlowRemove); + SubscribeLocalEvent(OnRefreshMovespeed); SubscribeLocalEvent(UpdateCanMove); SubscribeLocalEvent(OnStunShutdown); SubscribeLocalEvent(OnStunOnContactCollide); - // helping people up if they're knocked down - SubscribeLocalEvent(OnInteractHand); - SubscribeLocalEvent(OnRefreshMovespeed); - - SubscribeLocalEvent(OnKnockedTileFriction); - // Attempt event subscriptions. SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnMoveAttempt); @@ -74,7 +61,7 @@ public abstract partial class SharedStunSystem : EntitySystem SubscribeLocalEvent(OnUnequipAttempt); SubscribeLocalEvent(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...) /// /// Stuns the entity, disallowing it from doing many interactions temporarily. /// - 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 /// /// Knocks down the entity, making it fall to the ground. /// - 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(uid)) return false; - if (!_statusEffect.TryAddStatusEffect(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(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); } /// /// Slows down the mob's walking/running speed temporarily /// 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(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,29 +303,14 @@ public abstract partial class SharedStunSystem : EntitySystem UpdateStunModifiers(ent, speedModifier, speedModifier); } - private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, InteractHandEvent args) + #region friction and movement listeners + + private void OnRefreshMovespeed(EntityUid ent, SlowedDownComponent comp, RefreshMovementSpeedModifiersEvent args) { - if (args.Handled || knocked.HelpTimer > 0f) - return; - - // TODO: This should be an event. - if (HasComp(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); - - args.Handled = true; + args.ModifySpeed(comp.WalkSpeedModifier, comp.SprintSpeedModifier); } - private void OnKnockedTileFriction(EntityUid uid, KnockedDownComponent component, ref TileFrictionEvent args) - { - args.Modifier *= KnockDownModifier; - } + #endregion #region Attempt Event Handling @@ -365,15 +343,3 @@ public abstract partial class SharedStunSystem : EntitySystem #endregion } - -/// -/// Raised directed on an entity when it is stunned. -/// -[ByRefEvent] -public record struct StunnedEvent; - -/// -/// Raised directed on an entity when it is knocked down. -/// -[ByRefEvent] -public record struct KnockedDownEvent; diff --git a/Content.Shared/Stunnable/StunOnContactComponent.cs b/Content.Shared/Stunnable/StunOnContactComponent.cs index cc4af53b3b..eae279d58a 100644 --- a/Content.Shared/Stunnable/StunOnContactComponent.cs +++ b/Content.Shared/Stunnable/StunOnContactComponent.cs @@ -18,6 +18,18 @@ public sealed partial class StunOnContactComponent : Component [DataField] public TimeSpan Duration = TimeSpan.FromSeconds(5); + /// + /// Should the stun applied refresh? + /// + [DataField] + public bool Refresh = true; + + /// + /// Should the stunned entity try to stand up when knockdown ends? + /// + [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 index 0000000000..18a7575487 --- /dev/null +++ b/Content.Shared/Stunnable/StunnableEvents.cs @@ -0,0 +1,89 @@ +using Content.Shared.Alert; +using Content.Shared.DoAfter; +using Content.Shared.Popups; +using Robust.Shared.Serialization; + +namespace Content.Shared.Stunnable; + +/// +/// This contains all the events raised by the SharedStunSystem +/// + +/// +/// Raised directed on an entity when it is stunned. +/// +[ByRefEvent] +public record struct StunnedEvent; + +/// +/// Raised directed on an entity before it is knocked down to see if it should be cancelled, and to determine +/// knocked down arguments. +/// +[ByRefEvent] +public record struct KnockDownAttemptEvent(bool AutoStand, bool Drop) +{ + public bool Cancelled; +} + +/// +/// Raised directed on an entity when it is knocked down. +/// +[ByRefEvent] +public record struct KnockedDownEvent(TimeSpan Time); + +/// +/// Raised on an entity that needs to refresh its knockdown modifiers +/// +[ByRefEvent] +public record struct KnockedDownRefreshEvent() +{ + public float SpeedModifier = 1f; + public float FrictionModifier = 1f; +} + +/// +/// Raised directed on an entity when it tries to stand up +/// +/// If the attempt was cancelled, passes a recommended value to change autostand to. +[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; +} + +/// +/// Raises the default DoAfterTime for a stand-up attempt for other components to modify it. +/// +/// +[ByRefEvent] +public record struct GetStandUpTimeEvent(TimeSpan DoAfterTime); + +/// +/// 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. +/// +/// The stamina damage the entity will take when it forces itself to stand. +[ByRefEvent] +public record struct TryForceStandEvent(float Stamina); + +/// +/// Raised when you click on the Knocked Down Alert +/// +public sealed partial class KnockedDownAlertEvent : BaseAlertEvent; + +/// +/// The DoAfterEvent for trying to stand the slow and boring way. +/// +[ByRefEvent] +[Serializable, NetSerializable] +public sealed partial class TryStandDoAfterEvent : SimpleDoAfterEvent; + +/// +/// An event sent by the client to the server to ask it very nicely to perform a forced stand-up. +/// +[Serializable, NetSerializable] +public sealed class ForceStandUpEvent : EntityEventArgs; + diff --git a/Content.Shared/Throwing/ThrowAttemptEvent.cs b/Content.Shared/Throwing/ThrowAttemptEvent.cs index dfb3dca5c7..994206afa5 100644 --- a/Content.Shared/Throwing/ThrowAttemptEvent.cs +++ b/Content.Shared/Throwing/ThrowAttemptEvent.cs @@ -25,4 +25,13 @@ /// Raised when we try to pushback an entity from throwing /// public sealed class ThrowPushbackAttemptEvent : CancellableEntityEventArgs {} + + /// + /// Raised on an entity that is being pushed from a thrown entity + /// + [ByRefEvent] + public record struct ThrowerImpulseEvent() + { + public bool Push; + }; } diff --git a/Content.Shared/Throwing/ThrowingSystem.cs b/Content.Shared/Throwing/ThrowingSystem.cs index 2d01810d8b..ceb9cf8bfb 100644 --- a/Content.Shared/Throwing/ThrowingSystem.cs +++ b/Content.Shared/Throwing/ThrowingSystem.cs @@ -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); } } diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 7e578657eb..624781292a 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -384,11 +384,14 @@ public abstract partial class SharedGunSystem : EntitySystem var shotEv = new GunShotEvent(user, ev.Ammo); RaiseLocalEvent(gunUid, ref shotEv); - if (userImpulse && TryComp(user, out var userPhysics)) - { - if (_gravity.IsWeightless(user, userPhysics)) - CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics); - } + if (!userImpulse || !TryComp(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); +/// +/// 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. +/// +[ByRefEvent] +public record struct ShooterImpulseEvent() +{ + public bool Push; +}; + public enum EffectLayers : byte { Unshaded, diff --git a/Resources/Locale/en-US/administration/smites.ftl b/Resources/Locale/en-US/administration/smites.ftl index adce0bd020..d276d1c135 100644 --- a/Resources/Locale/en-US/administration/smites.ftl +++ b/Resources/Locale/en-US/administration/smites.ftl @@ -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 diff --git a/Resources/Locale/en-US/alerts/alerts.ftl b/Resources/Locale/en-US/alerts/alerts.ftl index 45c22abcbc..9593b01321 100644 --- a/Resources/Locale/en-US/alerts/alerts.ftl +++ b/Resources/Locale/en-US/alerts/alerts.ftl @@ -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. diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 64f9428a13..f2142efaa2 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -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 diff --git a/Resources/Locale/en-US/stunnable/components/stunnable-component.ftl b/Resources/Locale/en-US/stunnable/components/stunnable-component.ftl index 71d275504d..4f822bd334 100644 --- a/Resources/Locale/en-US/stunnable/components/stunnable-component.ftl +++ b/Resources/Locale/en-US/stunnable/components/stunnable-component.ftl @@ -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! diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 6710a15fbc..ba86774f9b 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -146,6 +146,15 @@ 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 diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index c09b5db4f5..a426e7aa1c 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -159,6 +159,8 @@ components: - type: Clickable - type: Slippery + staminaDamage: 0 + frictionStatusTime: 0 # Don't apply friction twice - type: Transform noRot: true anchored: true diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index ea24542e5a..cb3e5be478 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -144,7 +144,6 @@ - type: StatusEffects allowed: - Stun - - KnockedDown - SlowedDown - Flashed - type: TypingIndicator diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index c3fc1ec959..95dcd84ae6 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -36,7 +36,6 @@ - type: StatusEffects allowed: - Stun - - KnockedDown - SlowedDown - Stutter - Electrocution diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index 382de8e085..2251742436 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -16,7 +16,6 @@ - type: StatusEffects allowed: - Stun - - KnockedDown - SlowedDown - Stutter - Electrocution diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index bb095bfffd..eb0ea81617 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -94,7 +94,6 @@ - type: StatusEffects allowed: - Stun - - KnockedDown - SlowedDown - Stutter - Electrocution diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index f2ac678f52..9ea12f2ae3 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -120,7 +120,7 @@ - type: StatusEffects allowed: - Stun - - KnockedDown + - Friction - SlowedDown - Stutter - Electrocution diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml index 22ff617809..0cd960bed1 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml @@ -81,6 +81,26 @@ - 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 diff --git a/Resources/Prototypes/Entities/Objects/Fun/error.yml b/Resources/Prototypes/Entities/Objects/Fun/error.yml index 6f157d4fed..eb1e4d524a 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/error.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/error.yml @@ -17,7 +17,7 @@ Quantity: 5 - type: Slippery slipData: - paralyzeTime: 3 + knockdownTime: 3 launchForwardsMultiplier: 3 - type: StepTrigger intersectRatio: 0.2 diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index 6ed36af773..d0be38d504 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -872,8 +872,10 @@ - type: UseDelay delay: 0.8 - type: Slippery + staminaDamage: 0 slipData: - paralyzeTime: 0 + stunTime: 0 + knockdownTime: 0 launchForwardsMultiplier: 0 slipSound: collection: Parp diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml index dc88de3ed6..01283128e6 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml @@ -127,8 +127,9 @@ - type: SolutionContainerVisuals fillBaseName: syndie- - type: Slippery + staminaDamage: 50 slipData: - paralyzeTime: 3 + knockdownTime: 3 launchForwardsMultiplier: 3 - type: Item heldPrefix: syndie @@ -155,9 +156,6 @@ layers: - state: syndie-soaplet - type: Slippery - slipData: - paralyzeTime: 1.5 # these things are tiny - launchForwardsMultiplier: 1.5 - type: StepTrigger intersectRatio: 0.04 - type: Item @@ -222,8 +220,9 @@ - type: SolutionContainerVisuals fillBaseName: omega- - type: Slippery + staminaDamage: 50 slipData: - paralyzeTime: 5.0 + knockdownTime: 5.0 launchForwardsMultiplier: 3.0 - type: Item heldPrefix: omega diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml index d5f4681a9b..cdd3edd93f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml @@ -547,7 +547,7 @@ 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: @@ -579,6 +579,23 @@ 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] diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml index 7c94dd65cc..7a15d6520e 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml @@ -216,13 +216,46 @@ - 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 diff --git a/Resources/Prototypes/Entities/StatusEffects/misc.yml b/Resources/Prototypes/Entities/StatusEffects/misc.yml index bb2aa9de93..444600023a 100644 --- a/Resources/Prototypes/Entities/StatusEffects/misc.yml +++ b/Resources/Prototypes/Entities/StatusEffects/misc.yml @@ -43,10 +43,18 @@ 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 diff --git a/Resources/Prototypes/Reagents/cleaning.yml b/Resources/Prototypes/Reagents/cleaning.yml index 33c0a0e86f..90bcb23f86 100644 --- a/Resources/Prototypes/Reagents/cleaning.yml +++ b/Resources/Prototypes/Reagents/cleaning.yml @@ -83,7 +83,7 @@ meltingPoint: 18.2 tileReactions: - !type:SpillTileReaction - friction: 0.0 + friction: 0.05 - type: reagent id: SpaceGlue diff --git a/Resources/Prototypes/status_effects.yml b/Resources/Prototypes/status_effects.yml index 7c96b88c22..8ae32928a3 100644 --- a/Resources/Prototypes/status_effects.yml +++ b/Resources/Prototypes/status_effects.yml @@ -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 index 0000000000..3af4eddb50 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 index 0000000000..abae5803ea --- /dev/null +++ b/Resources/Textures/Interface/Alerts/stunnable.rsi/meta.json @@ -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" + } + ] +} diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 5c480fdc6d..1d86e4c5e7 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -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