diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs index e78a17e180..b5adeb04db 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.Impact.cs @@ -249,7 +249,7 @@ public sealed partial class ShuttleSystem if (direction.LengthSquared() > minsq) { - _stuns.TryUpdateKnockdownDuration(uid, knockdownTime); + _stuns.TryCrawling(uid, knockdownTime); _throwing.TryThrow(uid, direction, physics, Transform(uid), _projQuery, direction.Length(), playSound: false); } else diff --git a/Content.Server/Stunnable/Components/StunOnCollideComponent.cs b/Content.Server/Stunnable/Components/StunOnCollideComponent.cs index 363fb78b75..cbf6b17af8 100644 --- a/Content.Server/Stunnable/Components/StunOnCollideComponent.cs +++ b/Content.Server/Stunnable/Components/StunOnCollideComponent.cs @@ -1,58 +1,66 @@ -namespace Content.Server.Stunnable.Components +using Content.Server.Stunnable.Systems; + +namespace Content.Server.Stunnable.Components; + +/// +/// Adds stun when it collides with an entity +/// +[RegisterComponent, Access(typeof(StunOnCollideSystem))] +public sealed partial class StunOnCollideComponent : Component { + // TODO: Can probably predict this. + /// - /// Adds stun when it collides with an entity + /// How long we are stunned for /// - [RegisterComponent, Access(typeof(StunOnCollideSystem))] - public sealed partial class StunOnCollideComponent : Component - { - // TODO: Can probably predict this. + [DataField] + public TimeSpan StunAmount; - /// - /// How long we are stunned for - /// - [DataField] - public TimeSpan StunAmount; + /// + /// How long we are knocked down for + /// + [DataField] + public TimeSpan KnockdownAmount; - /// - /// How long we are knocked down for - /// - [DataField] - public TimeSpan KnockdownAmount; + /// + /// How long we are slowed down for + /// + [DataField] + public TimeSpan SlowdownAmount; - /// - /// How long we are slowed down for - /// - [DataField] - public TimeSpan SlowdownAmount; + /// + /// Multiplier for a mob's walking speed + /// + [DataField] + public float WalkSpeedModifier = 1f; - /// - /// Multiplier for a mob's walking speed - /// - [DataField] - public float WalkSpeedModifier = 1f; + /// + /// Multiplier for a mob's sprinting speed + /// + [DataField] + public float SprintSpeedModifier = 1f; - /// - /// Multiplier for a mob's sprinting speed - /// - [DataField] - public float SprintSpeedModifier = 1f; + /// + /// Refresh Stun or Slowdown on hit + /// + [DataField] + public bool Refresh = true; - /// - /// 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; - /// - /// Should the entity try and stand automatically after being knocked down? - /// - [DataField] - public bool AutoStand = true; + /// + /// Should the entity drop their items upon first being knocked down? + /// + [DataField] + public bool Drop = true; - /// - /// Fixture we track for the collision. - /// - [DataField("fixture")] public string FixtureID = "projectile"; - } + /// + /// Fixture we track for the collision. + /// + [DataField("fixture")] public string FixtureID = "projectile"; } + diff --git a/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs b/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs index 18c386d4ac..2257812da1 100644 --- a/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs +++ b/Content.Server/Stunnable/Systems/StunOnCollideSystem.cs @@ -4,47 +4,60 @@ using JetBrains.Annotations; using Content.Shared.Throwing; using Robust.Shared.Physics.Events; -namespace Content.Server.Stunnable +namespace Content.Server.Stunnable.Systems; + +[UsedImplicitly] +internal sealed class StunOnCollideSystem : EntitySystem { - [UsedImplicitly] - internal sealed class StunOnCollideSystem : EntitySystem + [Dependency] private readonly StunSystem _stunSystem = default!; + [Dependency] private readonly MovementModStatusSystem _movementMod = default!; + + public override void Initialize() { - [Dependency] private readonly StunSystem _stunSystem = default!; - [Dependency] private readonly MovementModStatusSystem _movementMod = default!; + base.Initialize(); - public override void Initialize() + SubscribeLocalEvent(HandleCollide); + SubscribeLocalEvent(HandleThrow); + } + + private void TryDoCollideStun(Entity ent, EntityUid target) + { + _stunSystem.TryKnockdown(target, ent.Comp.KnockdownAmount, ent.Comp.Refresh, ent.Comp.AutoStand, ent.Comp.Drop); + + if (ent.Comp.Refresh) { - base.Initialize(); - SubscribeLocalEvent(HandleCollide); - SubscribeLocalEvent(HandleThrow); - } - - private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target) - { - _stunSystem.TryUpdateStunDuration(target, component.StunAmount); - - _stunSystem.TryKnockdown(target, component.KnockdownAmount, component.Refresh, component.AutoStand, force: true); - + _stunSystem.TryUpdateStunDuration(target, ent.Comp.StunAmount); _movementMod.TryUpdateMovementSpeedModDuration( target, MovementModStatusSystem.TaserSlowdown, - component.SlowdownAmount, - component.WalkSpeedModifier, - component.SprintSpeedModifier + ent.Comp.SlowdownAmount, + ent.Comp.WalkSpeedModifier, + ent.Comp.SprintSpeedModifier ); } - - private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args) + else { - if (args.OurFixtureId != component.FixtureID) - return; - - TryDoCollideStun(uid, component, args.OtherEntity); - } - - private void HandleThrow(EntityUid uid, StunOnCollideComponent component, ThrowDoHitEvent args) - { - TryDoCollideStun(uid, component, args.Target); + _stunSystem.TryAddStunDuration(target, ent.Comp.StunAmount); + _movementMod.TryAddMovementSpeedModDuration( + target, + MovementModStatusSystem.TaserSlowdown, + ent.Comp.SlowdownAmount, + ent.Comp.WalkSpeedModifier, + ent.Comp.SprintSpeedModifier + ); } } + + private void HandleCollide(Entity ent, ref StartCollideEvent args) + { + if (args.OurFixtureId != ent.Comp.FixtureID) + return; + + TryDoCollideStun(ent, args.OtherEntity); + } + + private void HandleThrow(Entity ent, ref ThrowDoHitEvent args) + { + TryDoCollideStun(ent, args.Target); + } } diff --git a/Content.Shared/Slippery/SlipperySystem.cs b/Content.Shared/Slippery/SlipperySystem.cs index 6a0e96888a..51bbd2bea0 100644 --- a/Content.Shared/Slippery/SlipperySystem.cs +++ b/Content.Shared/Slippery/SlipperySystem.cs @@ -149,7 +149,8 @@ public sealed class SlipperySystem : EntitySystem _audio.PlayPredicted(component.SlipSound, other, other); } - _stun.TryKnockdown(other, component.SlipData.KnockdownTime, true, force: true); + // Slippery is so tied to knockdown that we really just need to force it here. + _stun.TryKnockdown(other, component.SlipData.KnockdownTime, force: true); _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 c6252d969a..8ed00ce23e 100644 --- a/Content.Shared/Standing/StandingStateComponent.cs +++ b/Content.Shared/Standing/StandingStateComponent.cs @@ -15,23 +15,10 @@ namespace Content.Shared.Standing public bool Standing { get; set; } = true; /// - /// Time it takes us to stand up + /// Friction modifier applied to an entity in the downed state. /// [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; + public float DownFrictionMod = 0.4f; /// /// List of fixtures that had their collision mask changed when the entity was downed. diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs index 7177568163..f965e0ae7c 100644 --- a/Content.Shared/Standing/StandingStateSystem.cs +++ b/Content.Shared/Standing/StandingStateSystem.cs @@ -23,7 +23,6 @@ public sealed class StandingStateSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(OnMobCollide); SubscribeLocalEvent(OnMobTargetCollide); - SubscribeLocalEvent(OnRefreshMovementSpeedModifiers); SubscribeLocalEvent(OnRefreshFrictionModifiers); SubscribeLocalEvent(OnTileFriction); } @@ -44,25 +43,19 @@ 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); + args.ModifyFriction(entity.Comp.DownFrictionMod); + args.ModifyAcceleration(entity.Comp.DownFrictionMod); } private void OnTileFriction(Entity entity, ref TileFrictionEvent args) { if (!entity.Comp.Standing) - args.Modifier *= entity.Comp.FrictionModifier; + args.Modifier *= entity.Comp.DownFrictionMod; } public bool IsDown(EntityUid uid, StandingStateComponent? standingState = null) diff --git a/Content.Shared/Stunnable/CrawlerComponent.cs b/Content.Shared/Stunnable/CrawlerComponent.cs new file mode 100644 index 0000000000..d969117344 --- /dev/null +++ b/Content.Shared/Stunnable/CrawlerComponent.cs @@ -0,0 +1,40 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Stunnable; + +/// +/// This is used to denote that an entity can crawl. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStunSystem))] +public sealed partial class CrawlerComponent : Component +{ + /// + /// Default time we will be knocked down for. + /// + [DataField, AutoNetworkedField] + public TimeSpan DefaultKnockedDuration { get; set; } = TimeSpan.FromSeconds(0.5); + + /// + /// Minimum damage taken to extend our knockdown timer by the default time. + /// + [DataField, AutoNetworkedField] + public float KnockdownDamageThreshold = 5f; + + /// + /// Time it takes us to stand up + /// + [DataField, AutoNetworkedField] + public TimeSpan StandTime = TimeSpan.FromSeconds(2); + + /// + /// Base modifier to the maximum movement speed of a knocked down mover. + /// + [DataField, AutoNetworkedField] + public float SpeedModifier = 0.4f; + + /// + /// Friction modifier applied to an entity in the downed state. + /// + [DataField, AutoNetworkedField] + public float FrictionModifier = 1f; +} diff --git a/Content.Shared/Stunnable/KnockdownStatusEffectComponent.cs b/Content.Shared/Stunnable/KnockdownStatusEffectComponent.cs index 79b2fb695b..b4805511b2 100644 --- a/Content.Shared/Stunnable/KnockdownStatusEffectComponent.cs +++ b/Content.Shared/Stunnable/KnockdownStatusEffectComponent.cs @@ -6,4 +6,22 @@ namespace Content.Shared.Stunnable; /// Knockdown as a status effect. /// [RegisterComponent, NetworkedComponent, Access(typeof(SharedStunSystem))] -public sealed partial class KnockdownStatusEffectComponent : Component; +public sealed partial class KnockdownStatusEffectComponent : Component +{ + /// + /// Should this knockdown only affect crawlers? + /// + /// + /// If your status effect doesn't come paired with + /// Or if your status effect doesn't whitelist itself to only those with + /// Then you need to set this to true. + /// + [DataField] + public bool Crawl; + + /// + /// Should we drop items when we fall? + /// + [DataField] + public bool Drop = true; +} diff --git a/Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs b/Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs index 26e8e47f03..7917f10bd5 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.Knockdown.cs @@ -25,13 +25,7 @@ namespace Content.Shared.Stunnable; /// 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; + private EntityQuery _crawlerQuery; [Dependency] private readonly EntityLookupSystem _entityLookup = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; @@ -43,6 +37,8 @@ public abstract partial class SharedStunSystem private void InitializeKnockdown() { + _crawlerQuery = GetEntityQuery(); + SubscribeLocalEvent(OnRejuvenate); // Startup and Shutdown @@ -61,8 +57,9 @@ public abstract partial class SharedStunSystem // DoAfter event subscriptions SubscribeLocalEvent(OnStandDoAfter); - // Knockdown Extenders - SubscribeLocalEvent(OnDamaged); + // Crawling + SubscribeLocalEvent(OnKnockdownRefresh); + SubscribeLocalEvent(OnDamaged); // Handling Alternative Inputs SubscribeAllEvent(OnForceStandup); @@ -81,6 +78,7 @@ public abstract partial class SharedStunSystem while (query.MoveNext(out var uid, out var knockedDown)) { + // If it's null then we don't want to stand up if (!knockedDown.AutoStand || knockedDown.DoAfterId.HasValue || knockedDown.NextUpdate > GameTiming.CurTime) continue; @@ -157,7 +155,7 @@ public abstract partial class SharedStunSystem /// 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) + public void UpdateKnockdownTime(Entity entity, TimeSpan time, bool refresh = true) { if (refresh) RefreshKnockdownTime(entity, time); @@ -174,6 +172,7 @@ public abstract partial class SharedStunSystem { entity.Comp.NextUpdate = time; DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.NextUpdate)); + Alerts.ShowAlert(entity, KnockdownAlert, null, (GameTiming.CurTime, entity.Comp.NextUpdate)); } /// @@ -182,11 +181,14 @@ public abstract partial class SharedStunSystem /// /// Entity whose timer we're updating /// The time we want them to be knocked down for. - public void RefreshKnockdownTime(Entity entity, TimeSpan time) + public void RefreshKnockdownTime(Entity entity, TimeSpan time) { + if (!Resolve(entity, ref entity.Comp, false)) + return; + var knockedTime = GameTiming.CurTime + time; if (entity.Comp.NextUpdate < knockedTime) - SetKnockdownTime(entity, knockedTime); + SetKnockdownTime((entity, entity.Comp), knockedTime); } /// @@ -194,35 +196,20 @@ public abstract partial class SharedStunSystem /// /// Entity whose timer we're updating /// The time we want to add to their knocked down timer. - public void AddKnockdownTime(Entity entity, TimeSpan time) + public void AddKnockdownTime(Entity entity, TimeSpan time) { + if (!Resolve(entity, ref entity.Comp, false)) + return; + if (entity.Comp.NextUpdate < GameTiming.CurTime) { - SetKnockdownTime(entity, GameTiming.CurTime + time); + SetKnockdownTime((entity, entity.Comp), 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; + Alerts.ShowAlert(entity, KnockdownAlert, null, (GameTiming.CurTime, entity.Comp.NextUpdate)); } #endregion @@ -237,29 +224,55 @@ public abstract partial class SharedStunSystem if (playerSession.AttachedEntity is not { Valid: true } playerEnt || !Exists(playerEnt)) return; - if (!TryComp(playerEnt, out var component)) + ToggleKnockdown(playerEnt); + } + + /// + /// Handles an entity trying to make itself fall down. + /// + /// Entity who is trying to fall down + private void ToggleKnockdown(Entity entity) + { + // We resolve here instead of using TryCrawling to be extra sure someone without crawler can't stand up early. + if (!Resolve(entity, ref entity.Comp1, false)) + return; + + if (!Resolve(entity, ref entity.Comp2, false)) { - TryKnockdown(playerEnt, DefaultKnockedDuration, true, false, false); // TODO: Unhardcode these numbers + TryKnockdown(entity.Owner, entity.Comp1.DefaultKnockedDuration, true, false, false); return; } - var stand = !component.DoAfterId.HasValue; - SetAutoStand(playerEnt, stand); + var stand = !entity.Comp2.DoAfterId.HasValue; + SetAutoStand((entity, entity.Comp2), stand); - if (!stand || !TryStanding(playerEnt)) - CancelKnockdownDoAfter((playerEnt, component)); + if (!stand || !TryStanding((entity, entity.Comp2))) + CancelKnockdownDoAfter((entity, entity.Comp2)); } - public bool TryStanding(Entity entity) + public bool TryStanding(Entity entity) { // 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)) + if (!Resolve(entity, ref entity.Comp, false)) return true; - if (!TryStand((entity.Owner, entity.Comp1))) + if (!KnockdownOver((entity, entity.Comp))) return false; - var ev = new GetStandUpTimeEvent(entity.Comp2.StandTime); + if (!_crawlerQuery.TryComp(entity, out var crawler)) + { + // If we can't crawl then just have us sit back up... + // In case you're wondering, the KnockdownOverCheck, returns if we're able to move, so if next update is null. + // An entity that can't crawl will stand up the next time they can move, which should prevent moving while knocked down. + RemComp(entity); + _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(entity):user} has stood up from knockdown."); + return true; + } + + if (!TryStand((entity, entity.Comp))) + return false; + + var ev = new GetStandUpTimeEvent(crawler.StandTime); RaiseLocalEvent(entity, ref ev); var doAfterArgs = new DoAfterArgs(EntityManager, entity, ev.DoAfterTime, new TryStandDoAfterEvent(), entity, entity) @@ -275,11 +288,19 @@ public abstract partial class SharedStunSystem if (!DoAfter.TryStartDoAfter(doAfterArgs, out var doAfterId)) return false; - entity.Comp1.DoAfterId = doAfterId.Value.Index; - DirtyField(entity, entity.Comp1, nameof(KnockedDownComponent.DoAfterId)); + entity.Comp.DoAfterId = doAfterId.Value.Index; + DirtyField(entity, entity.Comp, nameof(KnockedDownComponent.DoAfterId)); return true; } + public bool KnockdownOver(Entity entity) + { + if (entity.Comp.NextUpdate > GameTiming.CurTime) + return false; + + return Blocker.CanMove(entity); + } + /// /// A variant of used when we're actually trying to stand. /// Main difference is this one affects autostand datafields and also displays popups. @@ -288,10 +309,7 @@ public abstract partial class SharedStunSystem /// 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)) + if (!KnockdownOver(entity)) return false; var ev = new StandUpAttemptEvent(entity.Comp.AutoStand); @@ -308,6 +326,22 @@ public abstract partial class SharedStunSystem return !ev.Cancelled; } + /// + /// 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 (!KnockdownOver(entity)) + return false; + + var ev = new StandUpAttemptEvent(); + RaiseLocalEvent(entity, ref ev); + + return !ev.Cancelled; + } + private bool StandingBlocked(Entity entity) { if (!TryStand(entity)) @@ -338,7 +372,7 @@ public abstract partial class SharedStunSystem // 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))) + if (StandingBlocked((entity, entity.Comp))) return; if (!_hands.TryGetEmptyHand(entity.Owner, out _)) @@ -436,16 +470,22 @@ public abstract partial class SharedStunSystem #endregion - #region Knockdown Extenders + #region Crawling - private void OnDamaged(Entity entity, ref DamageChangedEvent args) + 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); + if (args.DamageDelta.GetTotal() >= entity.Comp.KnockdownDamageThreshold) + RefreshKnockdownTime(entity.Owner, entity.Comp.DefaultKnockedDuration); + } + + private void OnKnockdownRefresh(Entity entity, ref KnockedDownRefreshEvent args) + { + args.FrictionModifier *= entity.Comp.FrictionModifier; + args.SpeedModifier *= entity.Comp.SpeedModifier; } #endregion diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs index 199afcabf2..dcf9ee4f60 100644 --- a/Content.Shared/Stunnable/SharedStunSystem.cs +++ b/Content.Shared/Stunnable/SharedStunSystem.cs @@ -26,7 +26,6 @@ namespace Content.Shared.Stunnable; public abstract partial class SharedStunSystem : EntitySystem { public static readonly EntProtoId StunId = "StatusEffectStunned"; - public static readonly EntProtoId KnockdownId = "StatusEffectKnockdown"; [Dependency] protected readonly IGameTiming GameTiming = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; @@ -61,10 +60,11 @@ public abstract partial class SharedStunSystem : EntitySystem SubscribeLocalEvent(OnMobStateChanged); // New Status Effect subscriptions - SubscribeLocalEvent(OnStunEffectApplied); + SubscribeLocalEvent(OnStunStatusApplied); SubscribeLocalEvent(OnStunStatusRemoved); SubscribeLocalEvent>(OnStunEndAttempt); + SubscribeLocalEvent(OnKnockdownStatusApplied); SubscribeLocalEvent>(OnStandUpAttempt); // Stun Appearance Data @@ -123,7 +123,7 @@ public abstract partial class SharedStunSystem : EntitySystem return; TryUpdateStunDuration(args.OtherEntity, ent.Comp.Duration); - TryKnockdown(args.OtherEntity, ent.Comp.Duration, true, force: true); + TryKnockdown(args.OtherEntity, ent.Comp.Duration, force: true); } // TODO STUN: Make events for different things. (Getting modifiers, attempt events, informative events...) @@ -156,29 +156,54 @@ public abstract partial class SharedStunSystem : EntitySystem _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} stunned for {timeForLogs} seconds"); } - public bool TryAddKnockdownDuration(EntityUid uid, TimeSpan duration) + /// + /// Tries to knock an entity to the ground, but will fail if they aren't able to crawl. + /// Useful if you don't want to paralyze an entity that can't crawl, but still want to knockdown + /// entities that can. + /// + /// Entity we're trying to knockdown. + /// Time of the knockdown. + /// Do we refresh their timer, or add to it if one exists? + /// Whether we should automatically stand when knockdown ends. + /// Should we drop what we're holding? + /// Should we force crawling? Even if something tried to block it? + /// Returns true if the entity is able to crawl, and was able to be knocked down. + public bool TryCrawling(Entity entity, + TimeSpan? time, + bool refresh = true, + bool autoStand = true, + bool drop = true, + bool force = false) { - if (!_status.TryAddStatusEffectDuration(uid, KnockdownId, duration)) + if (!Resolve(entity, ref entity.Comp, false)) return false; - TryKnockdown(uid, duration, true, force: true); - - return true; - + return TryKnockdown(entity, time, refresh, autoStand, drop, force); } - public bool TryUpdateKnockdownDuration(EntityUid uid, TimeSpan? duration) + /// + /// An overload of TryCrawling which uses the default crawling time from the CrawlerComponent as its timespan. + public bool TryCrawling(Entity entity, + bool refresh = true, + bool autoStand = true, + bool drop = true, + bool force = false) { - if (!_status.TryUpdateStatusEffectDuration(uid, KnockdownId, duration)) + if (!Resolve(entity, ref entity.Comp, false)) return false; - return TryKnockdown(uid, duration, true, force: true); + return TryKnockdown(entity, entity.Comp.DefaultKnockedDuration, refresh, autoStand, drop, force); } /// - /// Knocks down the entity, making it fall to the ground. + /// Checks if we can knock down an entity to the ground... /// - public bool TryKnockdown(Entity entity, TimeSpan? time, bool refresh, bool autoStand = true, bool drop = true, bool force = false) + /// The entity we're trying to knock down + /// The time of the knockdown + /// Whether we want to automatically stand when knockdown ends. + /// Whether we should drop items. + /// Should we force the status effect? + public bool CanKnockdown(Entity entity, ref TimeSpan? time, ref bool autoStand, ref bool drop, bool force = false) { if (time <= TimeSpan.Zero) return false; @@ -187,30 +212,53 @@ public abstract partial class SharedStunSystem : EntitySystem if (!Resolve(entity, ref entity.Comp, false)) return false; - if (!force) - { - var evAttempt = new KnockDownAttemptEvent(autoStand, drop); - RaiseLocalEvent(entity, ref evAttempt); + var evAttempt = new KnockDownAttemptEvent(autoStand, drop, time); + RaiseLocalEvent(entity, ref evAttempt); - if (evAttempt.Cancelled) - return false; + autoStand = evAttempt.AutoStand; + drop = evAttempt.Drop; - autoStand = evAttempt.AutoStand; - drop = evAttempt.Drop; - } + return force || !evAttempt.Cancelled; + } - Knockdown(entity!, time, refresh, autoStand, drop); + /// + /// Knocks down the entity, making it fall to the ground. + /// + /// The entity we're trying to knock down + /// The time of the knockdown + /// Whether we should refresh a running timer or add to it, if one exists. + /// Whether we want to automatically stand when knockdown ends. + /// Whether we should drop items. + /// Should we force the status effect? + public bool TryKnockdown(Entity entity, TimeSpan? time, bool refresh = true, bool autoStand = true, bool drop = true, bool force = false) + { + if (!CanKnockdown(entity.Owner, ref time, ref autoStand, ref drop, force)) + return false; + // If the entity can't crawl they also need to be stunned, and therefore we should be using paralysis status effect. + // Also time shouldn't be null if we're and trying to add time but, we check just in case anyways. + if (!Resolve(entity, ref entity.Comp, false)) + return refresh || time == null ? TryUpdateParalyzeDuration(entity, time) : TryAddParalyzeDuration(entity, time.Value); + + Knockdown(entity, time, refresh, autoStand, drop); return true; } - private void Knockdown(Entity entity, TimeSpan? time, bool refresh, bool autoStand, bool drop) + private void Crawl(Entity entity, TimeSpan? time, bool refresh, bool autoStand, bool drop) + { + if (!Resolve(entity, ref entity.Comp, false)) + return; + + Knockdown(entity, time, refresh, autoStand, drop); + } + + private void Knockdown(EntityUid uid, TimeSpan? time, bool refresh, bool autoStand, bool drop) { // Initialize our component with the relevant data we need if we don't have it - if (EnsureComp(entity, out var component)) + if (EnsureComp(uid, out var component)) { - RefreshKnockedMovement((entity, component)); - CancelKnockdownDoAfter((entity, component)); + RefreshKnockedMovement((uid, component)); + CancelKnockdownDoAfter((uid, component)); } else { @@ -218,41 +266,50 @@ public abstract partial class SharedStunSystem : EntitySystem if (drop) { var ev = new DropHandItemsEvent(); - RaiseLocalEvent(entity, ref ev); + RaiseLocalEvent(uid, ref ev); } // Only update Autostand value if it's our first time being knocked down... - SetAutoStand((entity, component), autoStand); + SetAutoStand((uid, component), autoStand); } - var knockedEv = new KnockedDownEvent(time); - RaiseLocalEvent(entity, ref knockedEv); + var knockedEv = new KnockedDownEvent(); + RaiseLocalEvent(uid, ref knockedEv); if (time != null) { - UpdateKnockdownTime((entity, component), time.Value, refresh); - _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(entity):user} knocked down for {time.Value.Seconds} seconds"); + UpdateKnockdownTime((uid, component), time.Value, refresh); + _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} was knocked down for {time.Value.Seconds} seconds"); } else - _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(entity):user} knocked down for an indefinite amount of time"); - - Alerts.ShowAlert(entity, KnockdownAlert, null, (GameTiming.CurTime, component.NextUpdate)); + { + Alerts.ShowAlert(uid, KnockdownAlert); + _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} was knocked down"); + } } public bool TryAddParalyzeDuration(EntityUid uid, TimeSpan duration) { - var knockdown = TryAddKnockdownDuration(uid, duration); - var stunned = TryAddStunDuration(uid, duration); + if (!_status.TryAddStatusEffectDuration(uid, StunId, duration)) + return false; - return knockdown || stunned; + // We can't exit knockdown when we're stunned, so this prevents knockdown lasting longer than the stun. + Knockdown(uid, null, false, true, true); + OnStunnedSuccessfully(uid, duration); + + return true; } public bool TryUpdateParalyzeDuration(EntityUid uid, TimeSpan? duration) { - var knockdown = TryUpdateKnockdownDuration(uid, duration); - var stunned = TryUpdateStunDuration(uid, duration); + if (!_status.TryUpdateStatusEffectDuration(uid, StunId, duration)) + return false; - return knockdown || stunned; + // We can't exit knockdown when we're stunned, so this prevents knockdown lasting longer than the stun. + Knockdown(uid, null, false, true, true); + OnStunnedSuccessfully(uid, duration); + + return true; } public bool TryUnstun(Entity entity) @@ -266,7 +323,7 @@ public abstract partial class SharedStunSystem : EntitySystem return !ev.Cancelled && RemComp(entity); } - private void OnStunEffectApplied(Entity entity, ref StatusEffectAppliedEvent args) + private void OnStunStatusApplied(Entity entity, ref StatusEffectAppliedEvent args) { if (GameTiming.ApplyingState) return; @@ -289,6 +346,18 @@ public abstract partial class SharedStunSystem : EntitySystem args.Args = ev; } + private void OnKnockdownStatusApplied(Entity entity, ref StatusEffectAppliedEvent args) + { + if (GameTiming.ApplyingState) + return; + + // If you make something that shouldn't crawl, crawl, that's your own fault. + if (entity.Comp.Crawl) + Crawl(args.Target, null, true, true, drop: entity.Comp.Drop); + else + Knockdown(args.Target, null, true, true, drop: entity.Comp.Drop); + } + private void OnStandUpAttempt(Entity entity, ref StatusEffectRelayedEvent args) { if (args.Args.Cancelled) diff --git a/Content.Shared/Stunnable/StunnableEvents.cs b/Content.Shared/Stunnable/StunnableEvents.cs index f4a0191c92..f0c08f6136 100644 --- a/Content.Shared/Stunnable/StunnableEvents.cs +++ b/Content.Shared/Stunnable/StunnableEvents.cs @@ -26,7 +26,7 @@ public record struct StunEndAttemptEvent(bool Cancelled); /// knocked down arguments. /// [ByRefEvent] -public record struct KnockDownAttemptEvent(bool AutoStand, bool Drop) +public record struct KnockDownAttemptEvent(bool AutoStand, bool Drop, TimeSpan? Time) { public bool Cancelled; } @@ -35,7 +35,7 @@ public record struct KnockDownAttemptEvent(bool AutoStand, bool Drop) /// Raised directed on an entity when it is knocked down. /// [ByRefEvent] -public record struct KnockedDownEvent(TimeSpan? Time); +public record struct KnockedDownEvent; /// /// Raised on an entity that needs to refresh its knockdown modifiers diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index da68ee109b..d9b2b83993 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1433,6 +1433,7 @@ methods: [ Touch ] effects: - !type:WashCreamPieReaction + - type: Crawler diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 8fcaef42b9..10c39ed7a0 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -163,6 +163,7 @@ - type: SleepEmitSound - type: SSDIndicator - type: StandingState + - type: Crawler - type: Dna - type: MindContainer showExamineInfo: true diff --git a/Resources/Prototypes/Entities/StatusEffects/movement.yml b/Resources/Prototypes/Entities/StatusEffects/movement.yml index a6cbd20724..71142af434 100644 --- a/Resources/Prototypes/Entities/StatusEffects/movement.yml +++ b/Resources/Prototypes/Entities/StatusEffects/movement.yml @@ -58,10 +58,3 @@ - type: StatusEffectAlert alert: Stun - type: StunnedStatusEffect - -- type: entity - parent: MobStandStatusEffectBase - id: StatusEffectKnockdown - name: knocked down - components: - - type: KnockdownStatusEffect