using Content.Shared.Administration.Logs; 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 SharedAudioSystem _audio = default!; [Dependency] private readonly SharedStunSystem _stun = 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); // as long as slip-resistant mice are never added, this should be fine (otherwise a mouse-hat will transfer it's power to the wearer). 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.SuperSlippery) return; var attemptEv = new SlipAttemptEvent(); 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.LaunchForwardsMultiplier, body: physics); if (component.SuperSlippery && requiresContact) { var sliding = EnsureComp(other); sliding.CollidingEntities.Add(uid); DebugTools.Assert(_physics.GetContactingEntities(other, physics).Contains(uid)); } } var playSound = !_statusEffects.HasStatusEffect(other, "KnockedDown"); _stun.TryParalyze(other, TimeSpan.FromSeconds(component.ParalyzeTime), true); // Preventing from playing the slip sound when you are already knocked down. if (playSound) { _audio.PlayPredicted(component.SlipSound, other, other); } _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 SlotFlags TargetSlots { get; } = SlotFlags.FEET; } /// /// 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);