using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Shared.Bed.Sleep; using Content.Shared.Database; 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 Robust.Shared.Audio.Systems; namespace Content.Shared.Stunnable; public abstract class SharedStunSystem : EntitySystem { [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly StandingStateSystem _standingState = 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.4f; public override void Initialize() { SubscribeLocalEvent(OnKnockInit); SubscribeLocalEvent(OnKnockShutdown); SubscribeLocalEvent(OnStandAttempt); SubscribeLocalEvent(OnSlowInit); SubscribeLocalEvent(OnSlowRemove); SubscribeLocalEvent(UpdateCanMove); SubscribeLocalEvent(UpdateCanMove); // helping people up if they're knocked down SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnRefreshMovespeed); SubscribeLocalEvent(OnKnockedTileFriction); // 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); } 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 UpdateCanMove(EntityUid uid, StunnedComponent component, EntityEventArgs args) { _blocker.UpdateCanMove(uid); } 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(); } 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); } 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) { 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, StatusEffectsComponent? status = null) { if (time <= TimeSpan.Zero) return false; if (!Resolve(uid, ref status, false)) return false; if (!_statusEffect.TryAddStatusEffect(uid, "KnockedDown", time, refresh)) return false; var ev = new KnockedDownEvent(); RaiseLocalEvent(uid, ref ev); 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, status) && 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, 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? walkSpeedMultiplier = Math.Clamp(walkSpeedMultiplier, 0f, 1f); runSpeedMultiplier = Math.Clamp(runSpeedMultiplier, 0f, 1f); slowed.WalkSpeedModifier *= walkSpeedMultiplier; slowed.SprintSpeedModifier *= runSpeedMultiplier; _movementSpeedModifier.RefreshMovementSpeedModifiers(uid); return true; } return false; } private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, InteractHandEvent 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; } private void OnKnockedTileFriction(EntityUid uid, KnockedDownComponent component, ref TileFrictionEvent args) { args.Modifier *= KnockDownModifier; } #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 } /// /// 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;