using Content.Shared._Sunrise.Mood; using Content.Shared._Sunrise.SunriseCCVars; using Content.Shared.Administration.Logs; using Content.Shared.Chat; using Content.Shared.Chat.Prototypes; 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.Configuration; using Robust.Shared.Containers; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Events; using Robust.Shared.Random; 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!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; // Sunrise-Start private static float _deadChance; [ValidatePrototypeId] private const string EmoteFallOnNeckProto = "FallOnNeck"; // Sunrise-End 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); _cfg.OnValueChanged(SunriseCCVars.SlipDeadChanse, OnSLipDeadChanseChanged, true); } // Sunrise-Start private void OnSLipDeadChanseChanged(float deadChanse) { _deadChance = deadChanse; } // Sunrise-End 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); RaiseLocalEvent(other, new MoodEffectEvent("MobSlipped")); // Sunrise-Start var evSlipped = new SlippedEvent(other); RaiseLocalEvent(other, ref evSlipped); if (_random.Prob(_deadChance) && _net.IsServer) { RaiseLocalEvent(other, new PlayEmoteMessage(EmoteFallOnNeckProto)); } // Sunrise-End // 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); // Sunrise-Start [ByRefEvent] public readonly record struct SlippedEvent(EntityUid Target); // Sunrise-End