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.Jittering; using Content.Shared.Speech.EntitySystems; using Content.Shared.StatusEffect; using Content.Shared.Throwing; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Content.Shared.Actions.Events; using Content.Shared.Climbing.Components; using Content.Shared._Goobstation.MartialArts.Components; namespace Content.Shared.Stunnable; public abstract class SharedStunSystem : EntitySystem { [Dependency] private readonly IComponentFactory _componentFactory = default!; [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!; [Dependency] private readonly StandingStateSystem _standingState = default!; [Dependency] private readonly StatusEffectsSystem _statusEffect = default!; [Dependency] private readonly SharedLayingDownSystem _layingDown = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedStutteringSystem _stutter = default!; // Stun meta [Dependency] private readonly SharedJitteringSystem _jitter = default!; // Stun meta [Dependency] private readonly ClothingModifyStunTimeSystem _modify = default!; // goob edit /// /// Friction modifier for knocked down players. /// Doesn't make them faster but makes them slow down... slower. /// public const float KnockDownFrictionModifier = 1f; // WWDP edit public override void Initialize() { SubscribeLocalEvent(OnKnockInit); SubscribeLocalEvent(OnKnockShutdown); SubscribeLocalEvent(OnStandAttempt); SubscribeLocalEvent(KnockdownStun); SubscribeLocalEvent(OnSlowInit); SubscribeLocalEvent(OnSlowRemove); SubscribeLocalEvent(UpdateCanMove); SubscribeLocalEvent(UpdateCanMove); SubscribeLocalEvent(OnStunOnContactStartup); SubscribeLocalEvent(OnStunOnContactCollide); // 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 OnStunOnContactStartup(Entity ent, ref ComponentStartup args) { if (TryComp(ent, out var body)) _broadphase.RegenerateContacts(ent, body); } 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, true, status); } private void OnKnockInit(EntityUid uid, KnockedDownComponent component, ComponentInit args) { RaiseNetworkEvent(new CheckAutoGetUpEvent(GetNetEntity(uid))); _layingDown.TryLieDown(uid, null, component.DropHeldItemsBehavior); // WD EDIT } private void OnKnockShutdown(EntityUid uid, KnockedDownComponent component, ComponentShutdown args) { if (!TryComp(uid, out StandingStateComponent? standing)) return; if (TryComp(uid, out LayingDownComponent? layingDown)) { if (layingDown.AutoGetUp && !_container.IsEntityInContainer(uid)) _layingDown.TryStandUp(uid, layingDown); return; } _standingState.Stand(uid, standing); } private void OnStandAttempt(EntityUid uid, KnockedDownComponent component, StandAttemptEvent args) { if (component.LifeStage <= ComponentLifeStage.Running) args.Cancel(); component.FollowUp = false; } 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) { time *= _modify.GetModifier(uid); // Goobstation if (time <= TimeSpan.Zero || !Resolve(uid, ref status, false) || !_statusEffect.TryAddStatusEffect(uid, "Stun", time, refresh)) return false; // goob edit _jitter.DoJitter(uid, time, refresh); _stutter.DoStutter(uid, time, refresh); // goob edit end 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, DropHeldItemsBehavior behavior, StatusEffectsComponent? status = null, float frictionMultiplier = KnockDownFrictionModifier) // WWDP slowdown moved to StandingStateComponent/LayingFrictionMultiplier { time *= _modify.GetModifier(uid); // Goobstation if (time <= TimeSpan.Zero || !Resolve(uid, ref status, false)) return false; var component = _componentFactory.GetComponent(); component.DropHeldItemsBehavior = behavior; component.FrictionMultiplier = frictionMultiplier; // WWDP if (!_statusEffect.TryAddStatusEffect(uid, "KnockedDown", time, refresh, component)) return false; var ev = new KnockedDownEvent(); RaiseLocalEvent(uid, ref ev); return true; } /// /// Knocks down the entity, making it fall to the ground. /// public bool TryKnockdown(EntityUid uid, TimeSpan time, bool refresh, StatusEffectsComponent? status = null, float frictionMultiplier = KnockDownFrictionModifier) // WWDP { // WWDP edit start time *= _modify.GetModifier(uid); // Goobstation if (time <= TimeSpan.Zero || !Resolve(uid, ref status, false)) return false; var component = _componentFactory.GetComponent(); component.FrictionMultiplier = frictionMultiplier; // WWDP if (!_statusEffect.TryAddStatusEffect(uid, "KnockedDown", time, refresh, component)) return false; // WWDP edit end 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, float frictionMultiplier = KnockDownFrictionModifier) // WWDP { if (!Resolve(uid, ref status, false)) return false; return TryKnockdown(uid, time, refresh, status, frictionMultiplier) && TryStun(uid, time, refresh, status); // WWDP } /// /// 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) || 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 || 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 *= component.FrictionMultiplier; // WWDP } // should make it so that one time when somebody gets knocked over, you can push them for a short stun. // On the slate for a rework once I make combos eat inputs, but that's not my goal right now. private void KnockdownStun(Entity ent, ref DisarmAttemptEvent args) { if (ent.Comp.FollowUp || !TryComp(ent, out var component) || !component.IsClimbing) return; TryParalyze(ent, TimeSpan.FromSeconds(1.5f), false); ent.Comp.FollowUp = true; } #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) return; 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) return; 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;