using Content.Shared.ActionBlocker; using Content.Shared.Movement.Events; using Content.Shared.StepTrigger.Systems; using Content.Shared.Weapons.Misc; using Content.Shared.Whitelist; using JetBrains.Annotations; using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Shared.Chasm; /// /// Handles making entities fall into chasms when stepped on. /// public sealed partial class ChasmSystem : EntitySystem { [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private ActionBlockerSystem _blocker = default!; [Dependency] private SharedGrapplingGunSystem _grapple = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private EntityWhitelistSystem _whitelist = default!; [Dependency] private EntityQuery _chasmFallingQuery; [Dependency] private EntityQuery _chasmQuery; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStepTriggered); SubscribeLocalEvent(OnStepTriggerAttempt); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnUpdateCanMove); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var chasm)) { if (_timing.CurTime < chasm.NextDeletionTime) continue; var chasmEvent = new EntityCompletedFallingIntoChasmEvent((uid, chasm)); RaiseLocalEvent(chasm.FallingInto, ref chasmEvent); if (_chasmQuery.TryComp(chasm.FallingInto, out var chasmComp)) { var tripperEvent = new CompletedFallingIntoChasmEvent((chasm.FallingInto, chasmComp)); RaiseLocalEvent(uid, ref tripperEvent); } else { DebugTools.Assert($"{ToPrettyString(chasm.FallingInto)} is missing {nameof(ChasmComponent)}"); } PredictedQueueDel(uid); } } private void OnStepTriggered(Entity entity, ref StepTriggeredOffEvent args) { // already doomed if (_chasmFallingQuery.HasComp(args.Tripper)) return; // Check the white-/blacklists and inform on rejection. if (!(entity.Comp.Whitelist == null && entity.Comp.Blacklist == null || _whitelist.CheckBoth(args.Tripper, entity.Comp.Blacklist, entity.Comp.Whitelist))) { var rejected = new FallerRejectedByChasmEvent(args.Tripper); RaiseLocalEvent(entity, ref rejected); return; } // Give an opportunity to cancel the fall for whatever reason. var checkEvent = new EntityStartFallingAttemptEvent(args.Tripper); RaiseLocalEvent(entity, ref checkEvent); if (checkEvent.Cancelled) return; StartFalling(entity.AsNullable(), args.Tripper); } /// /// Causes to fall into : starts a falling animation, optionally /// plays a sound, and eventually deletes . /// If does not have a component, does nothing and returns null. /// /// /// with its new , if the entity did start falling. Null otherwise. /// [PublicAPI] public Entity? StartFalling( Entity chasm, EntityUid tripper, bool playSound = true ) { if (!_chasmQuery.Resolve(chasm, ref chasm.Comp, logMissing: false)) return null; var falling = AddComp(tripper); falling.FallingInto = chasm; falling.NextDeletionTime = _timing.CurTime + falling.DeletionTime; _blocker.UpdateCanMove(tripper); if (playSound) _audio.PlayPredicted(chasm.Comp.FallingSound, chasm, tripper); var chasmEvent = new EntityStartedFallingIntoChasmEvent((tripper, falling)); RaiseLocalEvent(chasm, ref chasmEvent); var tripperEvent = new StartedFallingIntoChasmEvent((chasm, chasm.Comp)); RaiseLocalEvent(tripper, ref tripperEvent); Entity ret = (tripper, falling); Dirty(ret); return ret; } private void OnStepTriggerAttempt(Entity entity, ref StepTriggerAttemptEvent args) { if (_grapple.IsEntityHooked(args.Tripper)) { args.Cancelled = true; return; } args.Continue = true; } private static void OnUpdateCanMove(Entity entity, ref UpdateCanMoveEvent args) { args.Cancel(); } private void OnShutdown(Entity entity, ref ComponentShutdown args) { var e = EntityQueryEnumerator(); while (e.MoveNext(out var fallingEnt, out var falling)) { if (falling.FallingInto != entity.Owner) continue; RemCompDeferred(fallingEnt); } } }