using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Alert; using Content.Shared.Interaction.Events; using Content.Shared.Inventory.Events; using Content.Shared.Item; 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; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Content.Shared.Standing; using Content.Shared.StatusEffect; using Content.Shared.Throwing; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Physics.Events; using Robust.Shared.Timing; 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] protected readonly SharedDoAfterSystem DoAfter = default!; [Dependency] protected readonly SharedStaminaSystem Stamina = default!; [Dependency] private readonly StatusEffectsSystem _statusEffect = default!; public override void Initialize() { SubscribeLocalEvent(OnSlowInit); SubscribeLocalEvent(OnSlowRemove); SubscribeLocalEvent(OnRefreshMovespeed); SubscribeLocalEvent(UpdateCanMove); SubscribeLocalEvent(OnStunShutdown); SubscribeLocalEvent(OnStunOnContactCollide); // Attempt event subscriptions. SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnMoveAttempt); SubscribeLocalEvent(OnAttemptInteract); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnEquipAttempt); SubscribeLocalEvent(OnUnequipAttempt); SubscribeLocalEvent(OnMobStateChanged); InitializeKnockdown(); InitializeAppearance(); } private void OnAttemptInteract(Entity ent, ref InteractionAttemptEvent args) { args.Cancelled = true; } private void OnMobStateChanged(EntityUid uid, MobStateComponent component, MobStateChangedEvent args) { if (!TryComp(uid, out var status)) { return; } switch (args.NewMobState) { case MobState.Alive: { break; } case MobState.Critical: { _statusEffect.TryRemoveStatusEffect(uid, "Stun"); break; } case MobState.Dead: { _statusEffect.TryRemoveStatusEffect(uid, "Stun"); break; } case MobState.Invalid: default: return; } } private void OnStunShutdown(Entity ent, ref ComponentShutdown args) { // This exists so the client can end their funny animation if they're playing one. UpdateCanMove(ent, ent.Comp, args); Appearance.RemoveData(ent, StunVisuals.SeeingStars); } private void UpdateCanMove(EntityUid uid, StunnedComponent component, EntityEventArgs args) { Blocker.UpdateCanMove(uid); } private void OnStunOnContactCollide(Entity ent, ref StartCollideEvent args) { if (args.OurFixtureId != ent.Comp.FixtureId) return; if (_entityWhitelist.IsBlacklistPass(ent.Comp.Blacklist, args.OtherEntity)) return; if (!TryComp(args.OtherEntity, out var status)) return; TryStun(args.OtherEntity, ent.Comp.Duration, true, status); TryKnockdown(args.OtherEntity, ent.Comp.Duration, ent.Comp.Refresh, ent.Comp.AutoStand); } private void OnSlowInit(EntityUid uid, SlowedDownComponent component, ComponentInit args) { _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); } private void OnSlowRemove(EntityUid uid, SlowedDownComponent component, ComponentShutdown args) { component.SprintSpeedModifier = 1f; component.WalkSpeedModifier = 1f; _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); } // 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) { if (time <= TimeSpan.Zero) return false; if (!Resolve(uid, ref status, false)) return false; if (!_statusEffect.TryAddStatusEffect(uid, "Stun", time, refresh)) return false; var ev = new StunnedEvent(); RaiseLocalEvent(uid, ref ev); _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} stunned for {time.Seconds} seconds"); return true; } /// /// Knocks down the entity, making it fall to the ground. /// public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh, bool autoStand = true, bool drop = true) { if (time <= TimeSpan.Zero) return false; // Can't fall down if you can't actually be downed. if (!HasComp(uid)) return false; var evAttempt = new KnockDownAttemptEvent(autoStand, drop); RaiseLocalEvent(uid, ref evAttempt); if (evAttempt.Cancelled) return false; // 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; } /// /// Applies knockdown and stun to the entity temporarily. /// public bool TryParalyze(EntityUid uid, TimeSpan time, bool refresh, StatusEffectsComponent? status = null) { if (!Resolve(uid, ref status, false)) return false; 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 walkSpeedMod = 1f, float sprintSpeedMod = 1f, StatusEffectsComponent? status = null) { if (!Resolve(uid, ref status, false)) return false; if (time <= TimeSpan.Zero) return false; if (_statusEffect.TryAddStatusEffect(uid, "SlowedDown", time, refresh, status)) { var slowed = Comp(uid); // Doesn't make much sense to have the "TrySlowdown" method speed up entities now does it? walkSpeedMod = Math.Clamp(walkSpeedMod, 0f, 1f); sprintSpeedMod = Math.Clamp(sprintSpeedMod, 0f, 1f); slowed.WalkSpeedModifier *= walkSpeedMod; slowed.SprintSpeedModifier *= sprintSpeedMod; _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); return true; } return false; } /// /// Updates the movement speed modifiers of an entity by applying or removing the . /// If both walk and run modifiers are approximately 1 (i.e. normal speed) and is 0, /// or if the both modifiers are 0, the slowdown component is removed to restore normal movement. /// Otherwise, the slowdown component is created or updated with the provided modifiers, /// and the movement speed is refreshed accordingly. /// /// Entity whose movement speed should be updated. /// New walk speed modifier. Default is 1f (normal speed). /// New run (sprint) speed modifier. Default is 1f (normal speed). public void UpdateStunModifiers(Entity ent, float walkSpeedModifier = 1f, float runSpeedModifier = 1f) { if (!Resolve(ent, ref ent.Comp)) return; if ( (MathHelper.CloseTo(walkSpeedModifier, 1f) && MathHelper.CloseTo(runSpeedModifier, 1f) && ent.Comp.StaminaDamage == 0f) || (walkSpeedModifier == 0f && runSpeedModifier == 0f) ) { RemComp(ent); return; } EnsureComp(ent, out var comp); comp.WalkSpeedModifier = walkSpeedModifier; comp.SprintSpeedModifier = runSpeedModifier; _movementSpeedModifier.RefreshMovementSpeedModifiers(ent); Dirty(ent); } /// /// A convenience overload of that sets both /// walk and run speed modifiers to the same value. /// /// Entity whose movement speed should be updated. /// New walk and run speed modifier. Default is 1f (normal speed). /// /// Optional of the entity. /// public void UpdateStunModifiers(Entity ent, float speedModifier = 1f) { UpdateStunModifiers(ent, speedModifier, speedModifier); } #region friction and movement listeners private void OnRefreshMovespeed(EntityUid ent, SlowedDownComponent comp, RefreshMovementSpeedModifiersEvent args) { args.ModifySpeed(comp.WalkSpeedModifier, comp.SprintSpeedModifier); } #endregion #region Attempt Event Handling private void OnMoveAttempt(EntityUid uid, StunnedComponent stunned, UpdateCanMoveEvent args) { if (stunned.LifeStage > ComponentLifeStage.Running) return; args.Cancel(); } private void OnAttempt(EntityUid uid, StunnedComponent stunned, CancellableEntityEventArgs args) { args.Cancel(); } private void OnEquipAttempt(EntityUid uid, StunnedComponent stunned, IsEquippingAttemptEvent args) { // is this a self-equip, or are they being stripped? if (args.Equipee == uid) args.Cancel(); } private void OnUnequipAttempt(EntityUid uid, StunnedComponent stunned, IsUnequippingAttemptEvent args) { // is this a self-equip, or are they being stripped? if (args.Unequipee == uid) args.Cancel(); } #endregion }