using System.Numerics; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Pulling.Systems; using Content.Shared.Physics; using Content.Shared.Trigger.Components.Effects; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Network; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Audio.Systems; using Robust.Shared.Collections; using Robust.Shared.Random; namespace Content.Shared.Trigger.Systems; public sealed class ScramOnTriggerSystem : EntitySystem { [Dependency] private readonly PullingSystem _pulling = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly INetManager _net = default!; private EntityQuery _physicsQuery; private HashSet> _targetGrids = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnTrigger); _physicsQuery = GetEntityQuery(); } private void OnTrigger(Entity ent, ref TriggerEvent args) { if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key)) return; var target = ent.Comp.TargetUser ? args.User : ent.Owner; if (target == null) return; // We need stop the user from being pulled so they don't just get "attached" with whoever is pulling them. // This can for example happen when the user is cuffed and being pulled. if (TryComp(target, out var pull) && _pulling.IsPulled(target.Value, pull)) _pulling.TryStopPull(ent, pull); // Check if the user is pulling anything, and drop it if so. if (TryComp(target, out var puller) && TryComp(puller.Pulling, out var pullable)) _pulling.TryStopPull(puller.Pulling.Value, pullable); _audio.PlayPredicted(ent.Comp.TeleportSound, ent, args.User); // Can't predict picking random grids and the target location might be out of PVS range. if (_net.IsClient) return; var xform = Transform(target.Value); var targetCoords = SelectRandomTileInRange(xform, ent.Comp.TeleportRadius); if (targetCoords != null) { _transform.SetCoordinates(target.Value, targetCoords.Value); args.Handled = true; } } private EntityCoordinates? SelectRandomTileInRange(TransformComponent userXform, float radius) { var userCoords = _transform.ToMapCoordinates(userXform.Coordinates); _targetGrids.Clear(); _lookup.GetEntitiesInRange(userCoords, radius, _targetGrids); Entity? targetGrid = null; if (_targetGrids.Count == 0) return null; // Give preference to the grid the entity is currently on. // This does not guarantee that if the probability fails that the owner's grid won't be picked. // In reality the probability is higher and depends on the number of grids. if (userXform.GridUid != null && TryComp(userXform.GridUid, out var gridComp)) { var userGrid = new Entity(userXform.GridUid.Value, gridComp); if (_random.Prob(0.5f)) { _targetGrids.Remove(userGrid); targetGrid = userGrid; } } if (targetGrid == null) targetGrid = _random.GetRandom().PickAndTake(_targetGrids); EntityCoordinates? targetCoords = null; do { var valid = false; var range = (float)Math.Sqrt(radius); var box = Box2.CenteredAround(userCoords.Position, new Vector2(range, range)); var tilesInRange = _map.GetTilesEnumerator(targetGrid.Value.Owner, targetGrid.Value.Comp, box, false); var tileList = new ValueList(); while (tilesInRange.MoveNext(out var tile)) { tileList.Add(tile.GridIndices); } while (tileList.Count != 0) { var tile = tileList.RemoveSwap(_random.Next(tileList.Count)); valid = true; foreach (var entity in _map.GetAnchoredEntities(targetGrid.Value.Owner, targetGrid.Value.Comp, tile)) { if (!_physicsQuery.TryGetComponent(entity, out var body)) continue; if (body.BodyType != BodyType.Static || !body.Hard || (body.CollisionLayer & (int)CollisionGroup.MobMask) == 0) continue; valid = false; break; } if (valid) { targetCoords = new EntityCoordinates(targetGrid.Value.Owner, _map.TileCenterToVector(targetGrid.Value, tile)); break; } } if (valid || _targetGrids.Count == 0) // if we don't do the check here then PickAndTake will blow up on an empty set. break; targetGrid = _random.GetRandom().PickAndTake(_targetGrids); } while (true); return targetCoords; } }