using Content.Shared.Administration.Logs; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Inventory; using Robust.Shared.Network; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.StatusEffect; using Content.Shared.StepTrigger.Systems; using Content.Shared.Stunnable; using Content.Shared.Throwing; using JetBrains.Annotations; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Events; using Robust.Shared.Utility; namespace Content.Shared.Slippery; [UsedImplicitly] 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!; [Dependency] private readonly SpeedModifierContactsSystem _speedModifier = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(HandleAttemptCollide); SubscribeLocalEvent(HandleStepTrigger); SubscribeLocalEvent(OnNoSlipAttempt); SubscribeLocalEvent(OnSlowedOverSlipAttempt); SubscribeLocalEvent(OnThrownSlipAttempt); SubscribeLocalEvent>((e, c, ev) => OnNoSlipAttempt(e, c, ev.Args)); SubscribeLocalEvent>((e, c, ev) => OnSlowedOverSlipAttempt(e, c, ev.Args)); SubscribeLocalEvent>(OnGetSlowedOverSlipperyModifier); SubscribeLocalEvent(OnEntityExit); } private void HandleStepTrigger(EntityUid uid, SlipperyComponent component, ref StepTriggeredOffEvent args) { TrySlip(uid, component, args.Tripper); } private void HandleAttemptCollide( EntityUid uid, SlipperyComponent component, ref StepTriggerAttemptEvent args) { args.Continue |= CanSlip(uid, args.Tripper); } private static void OnNoSlipAttempt(EntityUid uid, NoSlipComponent component, SlipAttemptEvent args) { args.NoSlip = true; } private void OnSlowedOverSlipAttempt(EntityUid uid, SlowedOverSlipperyComponent component, SlipAttemptEvent args) { args.SlowOverSlippery = true; } private void OnThrownSlipAttempt(EntityUid uid, ThrownItemComponent comp, ref SlipCausingAttemptEvent args) { args.Cancelled = true; } private void OnGetSlowedOverSlipperyModifier(EntityUid uid, SlowedOverSlipperyComponent comp, ref InventoryRelayedEvent args) { args.Args.SlowdownModifier *= comp.SlowdownModifier; } private void OnEntityExit(EntityUid uid, SlipperyComponent component, ref EndCollideEvent args) { if (HasComp(args.OtherEntity)) _speedModifier.AddModifiedEntity(args.OtherEntity); } private bool CanSlip(EntityUid uid, EntityUid toSlip) { return !_container.IsEntityInContainer(uid) && _statusEffects.CanApplyEffect(toSlip, "Stun"); //Should be KnockedDown instead? } public void TrySlip(EntityUid uid, SlipperyComponent component, EntityUid other, bool requiresContact = true) { if (HasComp(other) && !component.SlipData.SuperSlippery) return; var attemptEv = new SlipAttemptEvent(uid); RaiseLocalEvent(other, attemptEv); if (attemptEv.SlowOverSlippery) _speedModifier.AddModifiedEntity(other); if (attemptEv.NoSlip) return; var attemptCausingEv = new SlipCausingAttemptEvent(); RaiseLocalEvent(uid, ref attemptCausingEv); if (attemptCausingEv.Cancelled) return; var ev = new SlipEvent(other); RaiseLocalEvent(uid, ref ev); if (TryComp(other, out PhysicsComponent? physics) && !HasComp(other)) { _physics.SetLinearVelocity(other, physics.LinearVelocity * component.SlipData.LaunchForwardsMultiplier, body: physics); if (component.AffectsSliding && requiresContact) EnsureComp(other); } // 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}"); } } /// /// Raised on an entity to determine if it can slip or not. /// public sealed class SlipAttemptEvent : EntityEventArgs, IInventoryRelayEvent { public bool NoSlip; public bool SlowOverSlippery; public EntityUid? SlipCausingEntity; public SlotFlags TargetSlots { get; } = SlotFlags.FEET; public SlipAttemptEvent(EntityUid? slipCausingEntity) { SlipCausingEntity = slipCausingEntity; } } /// /// Raised on an entity that is causing the slip event (e.g, the banana peel), to determine if the slip attempt should be cancelled. /// /// If the slip should be cancelled [ByRefEvent] public record struct SlipCausingAttemptEvent (bool Cancelled); /// Raised on an entity that CAUSED some other entity to slip (e.g., the banana peel). /// The entity being slipped [ByRefEvent] public readonly record struct SlipEvent(EntityUid Slipped);