]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Turn some implants into triggers (#39364)
authorslarticodefast <161409025+slarticodefast@users.noreply.github.com>
Wed, 6 Aug 2025 19:52:11 +0000 (21:52 +0200)
committerGitHub <noreply@github.com>
Wed, 6 Aug 2025 19:52:11 +0000 (12:52 -0700)
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
19 files changed:
Content.Client/Forensics/Systems/ForensicsSystem.cs [new file with mode: 0644]
Content.Server/Forensics/Systems/ForensicsSystem.cs
Content.Server/Implants/Components/ScramImplantComponent.cs [deleted file]
Content.Server/Implants/SubdermalImplantSystem.cs
Content.Shared/Forensics/Systems/SharedForensicsSystem.cs [new file with mode: 0644]
Content.Shared/Implants/Components/SubdermalImplantComponent.cs
Content.Shared/Implants/SharedSubdermalImplantSystem.cs
Content.Shared/Trigger/Components/Effects/DnaScrambleOnTriggerComponent.cs [new file with mode: 0644]
Content.Shared/Trigger/Components/Effects/RattleOnTriggerComponent.cs
Content.Shared/Trigger/Components/Effects/ScramOnTriggerComponent.cs [new file with mode: 0644]
Content.Shared/Trigger/Components/Effects/UncuffOnTriggerComponent.cs [new file with mode: 0644]
Content.Shared/Trigger/Systems/DnaScrambleOnTriggerSystem.cs [new file with mode: 0644]
Content.Shared/Trigger/Systems/ScramOnTriggerSystem.cs [new file with mode: 0644]
Content.Shared/Trigger/Systems/UncuffOnTriggerSystem.cs [new file with mode: 0644]
Resources/Locale/en-US/implant/implant.ftl
Resources/Locale/en-US/triggers/rattle-on-trigger.ftl [new file with mode: 0644]
Resources/Locale/en-US/triggers/scramble-on-trigger.ftl [new file with mode: 0644]
Resources/Prototypes/Actions/types.yml
Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml

diff --git a/Content.Client/Forensics/Systems/ForensicsSystem.cs b/Content.Client/Forensics/Systems/ForensicsSystem.cs
new file mode 100644 (file)
index 0000000..048fff6
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Forensics.Systems;
+
+namespace Content.Client.Forensics.Systems;
+
+public sealed class ForensicsSystem : SharedForensicsSystem;
index 9f94e39fb7b8af5992b8eac8e2710a23d56d913b..cc74c1d14143eb48e0442c1f0096659c0c8c978f 100644 (file)
@@ -12,6 +12,7 @@ using Content.Shared.Chemistry.Components.SolutionManager;
 using Content.Shared.DoAfter;
 using Content.Shared.Forensics;
 using Content.Shared.Forensics.Components;
+using Content.Shared.Forensics.Systems;
 using Content.Shared.Interaction;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Inventory;
@@ -23,7 +24,7 @@ using Content.Shared.Hands.Components;
 
 namespace Content.Server.Forensics
 {
-    public sealed class ForensicsSystem : EntitySystem
+    public sealed class ForensicsSystem : SharedForensicsSystem
     {
         [Dependency] private readonly IRobustRandom _random = default!;
         [Dependency] private readonly InventorySystem _inventory = default!;
@@ -317,12 +318,7 @@ namespace Content.Server.Forensics
         }
 
         #region Public API
-
-        /// <summary>
-        /// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
-        /// Does nothing if it does not have the DnaComponent.
-        /// </summary>
-        public void RandomizeDNA(Entity<DnaComponent?> ent)
+        public override void RandomizeDNA(Entity<DnaComponent?> ent)
         {
             if (!Resolve(ent, ref ent.Comp, false))
                 return;
@@ -334,11 +330,7 @@ namespace Content.Server.Forensics
             RaiseLocalEvent(ent.Owner, ref ev);
         }
 
-        /// <summary>
-        /// Give the entity a new, random fingerprint string.
-        /// Does nothing if it does not have the FingerprintComponent.
-        /// </summary>
-        public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
+        public override void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
         {
             if (!Resolve(ent, ref ent.Comp, false))
                 return;
diff --git a/Content.Server/Implants/Components/ScramImplantComponent.cs b/Content.Server/Implants/Components/ScramImplantComponent.cs
deleted file mode 100644 (file)
index f3bbc9e..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-using Content.Server.Implants;
-using Robust.Shared.Audio;
-
-namespace Content.Server.Implants.Components;
-
-/// <summary>
-/// Randomly teleports entity when triggered.
-/// </summary>
-[RegisterComponent]
-public sealed partial class ScramImplantComponent : Component
-{
-    /// <summary>
-    /// Up to how far to teleport the user
-    /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public float TeleportRadius = 100f;
-
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
-}
index e2482b7b60148f9aeb01ec9b0287c8f21d325fa5..f0530358a6e24af727d991a84acea7d25a14d5a7 100644 (file)
@@ -1,66 +1,21 @@
-using Content.Server.Cuffs;
-using Content.Server.Forensics;
-using Content.Server.Humanoid;
-using Content.Server.Implants.Components;
 using Content.Server.Store.Components;
 using Content.Server.Store.Systems;
-using Content.Shared.Cuffs.Components;
-using Content.Shared.Forensics;
-using Content.Shared.Forensics.Components;
-using Content.Shared.Humanoid;
 using Content.Shared.Implants;
-using Content.Shared.Implants.Components;
 using Content.Shared.Interaction;
-using Content.Shared.Physics;
 using Content.Shared.Popups;
-using Content.Shared.Preferences;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Map;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Random;
-using System.Numerics;
-using Content.Shared.Movement.Pulling.Components;
-using Content.Shared.Movement.Pulling.Systems;
-using Content.Server.IdentityManagement;
-using Content.Shared.DetailExaminable;
 using Content.Shared.Store.Components;
-using Robust.Shared.Collections;
-using Robust.Shared.Map.Components;
 
 namespace Content.Server.Implants;
 
 public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
 {
-    [Dependency] private readonly CuffableSystem _cuffable = default!;
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
-    [Dependency] private readonly IRobustRandom _random = default!;
-    [Dependency] private readonly MetaDataSystem _metaData = default!;
     [Dependency] private readonly StoreSystem _store = default!;
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
-    [Dependency] private readonly SharedTransformSystem _xform = default!;
-    [Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
-    [Dependency] private readonly PullingSystem _pullingSystem = default!;
-    [Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
-    [Dependency] private readonly SharedMapSystem _mapSystem = default!;
-    [Dependency] private readonly IdentitySystem _identity = default!;
-
-    private EntityQuery<PhysicsComponent> _physicsQuery;
-    private HashSet<Entity<MapGridComponent>> _targetGrids = [];
-
     public override void Initialize()
     {
         base.Initialize();
 
-        _physicsQuery = GetEntityQuery<PhysicsComponent>();
-
-        SubscribeLocalEvent<SubdermalImplantComponent, UseFreedomImplantEvent>(OnFreedomImplant);
         SubscribeLocalEvent<StoreComponent, ImplantRelayEvent<AfterInteractUsingEvent>>(OnStoreRelay);
-        SubscribeLocalEvent<SubdermalImplantComponent, ActivateImplantEvent>(OnActivateImplantEvent);
-        SubscribeLocalEvent<SubdermalImplantComponent, UseScramImplantEvent>(OnScramImplant);
-        SubscribeLocalEvent<SubdermalImplantComponent, UseDnaScramblerImplantEvent>(OnDnaScramblerImplant);
-
     }
 
     private void OnStoreRelay(EntityUid uid, StoreComponent store, ImplantRelayEvent<AfterInteractUsingEvent> implantRelay)
@@ -85,148 +40,4 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
         var msg = Loc.GetString("store-currency-inserted-implant", ("used", args.Used));
         _popup.PopupEntity(msg, args.User, args.User);
     }
-
-    private void OnFreedomImplant(EntityUid uid, SubdermalImplantComponent component, UseFreedomImplantEvent args)
-    {
-        if (!TryComp<CuffableComponent>(component.ImplantedEntity, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1)
-            return;
-
-        _cuffable.Uncuff(component.ImplantedEntity.Value, cuffs.LastAddedCuffs, cuffs.LastAddedCuffs);
-        args.Handled = true;
-    }
-
-    private void OnActivateImplantEvent(EntityUid uid, SubdermalImplantComponent component, ActivateImplantEvent args)
-    {
-        args.Handled = true;
-    }
-
-    private void OnScramImplant(EntityUid uid, SubdermalImplantComponent component, UseScramImplantEvent args)
-    {
-        if (component.ImplantedEntity is not { } ent)
-            return;
-
-        if (!TryComp<ScramImplantComponent>(uid, out var implant))
-            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<PullableComponent>(ent, out var pull) && _pullingSystem.IsPulled(ent, pull))
-            _pullingSystem.TryStopPull(ent, pull);
-
-        // Check if the user is pulling anything, and drop it if so
-        if (TryComp<PullerComponent>(ent, out var puller) && TryComp<PullableComponent>(puller.Pulling, out var pullable))
-            _pullingSystem.TryStopPull(puller.Pulling.Value, pullable);
-
-        var xform = Transform(ent);
-        var targetCoords = SelectRandomTileInRange(xform, implant.TeleportRadius);
-
-        if (targetCoords != null)
-        {
-            _xform.SetCoordinates(ent, targetCoords.Value);
-            _audio.PlayPvs(implant.TeleportSound, ent);
-            args.Handled = true;
-        }
-    }
-
-    private EntityCoordinates? SelectRandomTileInRange(TransformComponent userXform, float radius)
-    {
-        var userCoords = _xform.ToMapCoordinates(userXform.Coordinates);
-        _targetGrids.Clear();
-        _lookupSystem.GetEntitiesInRange(userCoords, radius, _targetGrids);
-        Entity<MapGridComponent>? 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<MapGridComponent>(userXform.GridUid, out var gridComp))
-        {
-            var userGrid = new Entity<MapGridComponent>(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 = _mapSystem.GetTilesEnumerator(targetGrid.Value.Owner, targetGrid.Value.Comp, box, false);
-            var tileList = new ValueList<Vector2i>();
-
-            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 _mapSystem.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,
-                        _mapSystem.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;
-    }
-
-    private void OnDnaScramblerImplant(EntityUid uid, SubdermalImplantComponent component, UseDnaScramblerImplantEvent args)
-    {
-        if (component.ImplantedEntity is not { } ent)
-            return;
-
-        if (TryComp<HumanoidAppearanceComponent>(ent, out var humanoid))
-        {
-            var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
-            _humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
-            _metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
-
-            // If the entity has the respecive components, then scramble the dna and fingerprint strings
-            _forensicsSystem.RandomizeDNA(ent);
-            _forensicsSystem.RandomizeFingerprint(ent);
-
-            RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
-            _identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
-            _popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
-        }
-
-        args.Handled = true;
-        QueueDel(uid);
-    }
 }
diff --git a/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs b/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs
new file mode 100644 (file)
index 0000000..1220b75
--- /dev/null
@@ -0,0 +1,18 @@
+using Content.Shared.Forensics.Components;
+
+namespace Content.Shared.Forensics.Systems;
+
+public abstract class SharedForensicsSystem : EntitySystem
+{
+    /// <summary>
+    /// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
+    /// Does nothing if it does not have the DnaComponent.
+    /// </summary>
+    public virtual void RandomizeDNA(Entity<DnaComponent?> ent) { }
+
+    /// <summary>
+    /// Give the entity a new, random fingerprint string.
+    /// Does nothing if it does not have the FingerprintComponent.
+    /// </summary>
+    public virtual void RandomizeFingerprint(Entity<FingerprintComponent?> ent) { }
+}
index bd0ff09678bbb74e99f67f48c8587a082080018a..390d113dfbd4058edba072dc72253e1a57452b90 100644 (file)
@@ -49,7 +49,7 @@ public sealed partial class SubdermalImplantComponent : Component
     /// </summary>
     [DataField]
     public EntityWhitelist? Blacklist;
-    
+
     /// <summary>
     /// If set, this ProtoId is used when attempting to draw the implant instead.
     /// Useful if the implant is a child to another implant and you don't want to differentiate between them when drawing.
@@ -66,11 +66,6 @@ public sealed partial class OpenStorageImplantEvent : InstantActionEvent
 
 }
 
-public sealed partial class UseFreedomImplantEvent : InstantActionEvent
-{
-
-}
-
 /// <summary>
 /// Used for triggering trigger events on the implant via action
 /// </summary>
@@ -86,13 +81,3 @@ public sealed partial class OpenUplinkImplantEvent : InstantActionEvent
 {
 
 }
-
-public sealed partial class UseScramImplantEvent : InstantActionEvent
-{
-
-}
-
-public sealed partial class UseDnaScramblerImplantEvent : InstantActionEvent
-{
-
-}
index 177e24ff02cde0a0f08fc9ba145a4f2a122f1eab..4c015f1209cad0b1a0cc3875f56419f972e7b47d 100644 (file)
@@ -44,7 +44,8 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
             _actionsSystem.AddAction(component.ImplantedEntity.Value, ref component.Action, component.ImplantAction, uid);
         }
 
-        //replace micro bomb with macro bomb
+        // replace micro bomb with macro bomb
+        // TODO: this shouldn't be hardcoded here
         if (_container.TryGetContainer(component.ImplantedEntity.Value, ImplanterComponent.ImplantSlotId, out var implantContainer) && _tag.HasTag(uid, MacroBombTag))
         {
             foreach (var implant in implantContainer.ContainedEntities)
diff --git a/Content.Shared/Trigger/Components/Effects/DnaScrambleOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/DnaScrambleOnTriggerComponent.cs
new file mode 100644 (file)
index 0000000..1f3767b
--- /dev/null
@@ -0,0 +1,11 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Trigger.Components.Effects;
+
+/// <summary>
+/// Scrambles the entity's identity and DNA, turning them into a randomized humanoid of the same species.
+/// If TargetUser is true the user will be scrambled instead.
+/// Used for dna scrambler implants.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class DnaScrambleOnTriggerComponent : BaseXOnTriggerComponent;
index 599a64339a16f8b87a5f8ca4a0875190b63f2abf..fa1175c3cb47491d4ac3cfd00d9b4814e0636f54 100644 (file)
@@ -24,7 +24,7 @@ public sealed partial class RattleOnTriggerComponent : BaseXOnTriggerComponent
     [DataField]
     public Dictionary<MobState, LocId> Messages = new()
     {
-        {MobState.Critical, "deathrattle-implant-critical-message"},
-        {MobState.Dead, "deathrattle-implant-dead-message"}
+        {MobState.Critical, "rattle-on-trigger-critical-message"},
+        {MobState.Dead, "rattle-on-trigger-dead-message"}
     };
 }
diff --git a/Content.Shared/Trigger/Components/Effects/ScramOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/ScramOnTriggerComponent.cs
new file mode 100644 (file)
index 0000000..bacf0f6
--- /dev/null
@@ -0,0 +1,25 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Trigger.Components.Effects;
+
+/// <summary>
+/// Randomly teleports the entity when triggered.
+/// If TargetUser is true the user will be teleported instead.
+/// Used for scram implants.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class ScramOnTriggerComponent : BaseXOnTriggerComponent
+{
+    /// <summary>
+    /// Up to how far to teleport the entity.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public float TeleportRadius = 100f;
+
+    /// <summary>
+    /// the sound to play when teleporting.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
+}
diff --git a/Content.Shared/Trigger/Components/Effects/UncuffOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/UncuffOnTriggerComponent.cs
new file mode 100644 (file)
index 0000000..770882f
--- /dev/null
@@ -0,0 +1,10 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Trigger.Components.Effects;
+
+/// <summary>
+/// Removes a pair of handcuffs from the entity.
+/// If TargetUser is true the user will be uncuffed instead.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class UncuffOnTriggerComponent : BaseXOnTriggerComponent;
diff --git a/Content.Shared/Trigger/Systems/DnaScrambleOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/DnaScrambleOnTriggerSystem.cs
new file mode 100644 (file)
index 0000000..246c6a8
--- /dev/null
@@ -0,0 +1,62 @@
+using Content.Shared.DetailExaminable;
+using Content.Shared.Forensics.Systems;
+using Content.Shared.Humanoid;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Preferences;
+using Content.Shared.Popups;
+using Content.Shared.Trigger.Components.Effects;
+using Robust.Shared.Network;
+
+namespace Content.Shared.Trigger.Systems;
+
+public sealed class DnaScrambleOnTriggerSystem : EntitySystem
+{
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
+    [Dependency] private readonly SharedIdentitySystem _identity = default!;
+    [Dependency] private readonly SharedForensicsSystem _forensics = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly INetManager _net = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<DnaScrambleOnTriggerComponent, TriggerEvent>(OnTrigger);
+    }
+
+    private void OnTrigger(Entity<DnaScrambleOnTriggerComponent> 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;
+
+        if (!TryComp<HumanoidAppearanceComponent>(target, out var humanoid))
+            return;
+
+        args.Handled = true;
+
+        // Randomness will mispredict
+        // and LoadProfile causes a debug assert on the client at the moment.
+        if (_net.IsClient)
+            return;
+
+        var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
+        _humanoidAppearance.LoadProfile(target.Value, newProfile, humanoid);
+        _metaData.SetEntityName(target.Value, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
+
+        // If the entity has the respective components, then scramble the dna and fingerprint strings.
+        _forensics.RandomizeDNA(target.Value);
+        _forensics.RandomizeFingerprint(target.Value);
+
+        RemComp<DetailExaminableComponent>(target.Value); // remove MRP+ custom description if one exists
+        _identity.QueueIdentityUpdate(target.Value); // manually queue identity update since we don't raise the event
+
+        // Can't use PopupClient or PopupPredicted because the trigger might be unpredicted.
+        _popup.PopupEntity(Loc.GetString("scramble-on-trigger-popup"), target.Value, target.Value);
+    }
+}
diff --git a/Content.Shared/Trigger/Systems/ScramOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/ScramOnTriggerSystem.cs
new file mode 100644 (file)
index 0000000..163012c
--- /dev/null
@@ -0,0 +1,151 @@
+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<PhysicsComponent> _physicsQuery;
+    private HashSet<Entity<MapGridComponent>> _targetGrids = new();
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<ScramOnTriggerComponent, TriggerEvent>(OnTrigger);
+
+        _physicsQuery = GetEntityQuery<PhysicsComponent>();
+    }
+
+    private void OnTrigger(Entity<ScramOnTriggerComponent> 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<PullableComponent>(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<PullerComponent>(target, out var puller) && TryComp<PullableComponent>(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<MapGridComponent>? 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<MapGridComponent>(userXform.GridUid, out var gridComp))
+        {
+            var userGrid = new Entity<MapGridComponent>(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<Vector2i>();
+
+            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;
+    }
+}
diff --git a/Content.Shared/Trigger/Systems/UncuffOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/UncuffOnTriggerSystem.cs
new file mode 100644 (file)
index 0000000..9b83c4c
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Shared.Cuffs;
+using Content.Shared.Cuffs.Components;
+using Content.Shared.Trigger.Components.Effects;
+
+namespace Content.Shared.Trigger.Systems;
+
+public sealed class UncuffOnTriggerSystem : EntitySystem
+{
+    [Dependency] private readonly SharedCuffableSystem _cuffable = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<UncuffOnTriggerComponent, TriggerEvent>(OnTrigger);
+    }
+
+    private void OnTrigger(Entity<UncuffOnTriggerComponent> 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;
+
+        if (!TryComp<CuffableComponent>(target.Value, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1)
+            return;
+
+        _cuffable.Uncuff(target.Value, args.User, cuffs.LastAddedCuffs);
+        args.Handled = true;
+    }
+}
index 8cddef4c81e0698281cb83df525ac80a02be0794..3f38ae443fe531d417133d0193ade45e32cb1b67 100644 (file)
@@ -25,12 +25,3 @@ implanter-label-draw = [color=red]{$implantName}[/color]
     Mode: [color=white]{$modeString}[/color]
 
 implanter-contained-implant-text = [color=green]{$desc}[/color]
-
-## Implant Popups
-
-scramble-implant-activated-popup = Your appearance shifts and changes!
-
-## Implant Messages
-
-deathrattle-implant-dead-message = {$user} has died {$position}.
-deathrattle-implant-critical-message = {$user} life signs critical, immediate assistance required {$position}.
diff --git a/Resources/Locale/en-US/triggers/rattle-on-trigger.ftl b/Resources/Locale/en-US/triggers/rattle-on-trigger.ftl
new file mode 100644 (file)
index 0000000..3d090f1
--- /dev/null
@@ -0,0 +1,2 @@
+rattle-on-trigger-dead-message = {$user} has died {$position}.
+rattle-on-trigger-critical-message = {$user} life signs critical, immediate assistance required {$position}.
diff --git a/Resources/Locale/en-US/triggers/scramble-on-trigger.ftl b/Resources/Locale/en-US/triggers/scramble-on-trigger.ftl
new file mode 100644 (file)
index 0000000..1e84766
--- /dev/null
@@ -0,0 +1 @@
+scramble-on-trigger-popup = Your appearance shifts and changes!
index e6587ae6b87026de568be239f1536f1b2bd99807..97435c229de584d7442862304170bd209263ddca 100644 (file)
       state: gib
 
 - type: entity
-  parent: BaseAction
+  parent: BaseImplantAction
   id: ActionActivateFreedomImplant
   name: Break Free
   description: Activating your freedom implant will free you from any hand restraints
     icon:
       sprite: Actions/Implants/implants.rsi
       state: freedom
-  - type: InstantAction
-    event: !type:UseFreedomImplantEvent
 
 - type: entity
   parent: BaseAction
       state: icon
 
 - type: entity
-  parent: BaseAction
+  parent: BaseImplantAction
   id: ActionActivateScramImplant
   name: SCRAM!
   description: Randomly teleports you within a large distance.
     icon:
       sprite: Structures/Specific/anomaly.rsi
       state: anom4
-  - type: InstantAction
-    event: !type:UseScramImplantEvent
 
 - type: entity
-  parent: BaseAction
+  parent: BaseImplantAction
   id: ActionActivateDnaScramblerImplant
   name: Scramble DNA
   description:  Randomly changes your name and appearance.
     icon:
       sprite: Clothing/OuterClothing/Hardsuits/lingspacesuit.rsi
       state: icon
-  - type: InstantAction
-    event: !type:UseDnaScramblerImplantEvent
 
 - type: entity
   parent: BaseAction
   components:
   - type: Action
     useDelay: 8
-    icon: 
+    icon:
       sprite: Interface/Actions/jump.rsi
       state: icon
   - type: InstantAction
     event: !type:GravityJumpEvent {}
-    
+
 - type: entity
   parent: BaseToggleAction
   id: ActionToggleRootable
index a369a730cfc8af74eb1309e5337407c43027d0e0..6a4ad2466454cbb9565f77182e3a1ee1103160aa 100644 (file)
   description: This implant lets the user break out of hand restraints up to three times before ceasing to function anymore.
   categories: [ HideSpawnMenu ]
   components:
-    - type: SubdermalImplant
-      implantAction: ActionActivateFreedomImplant
-      whitelist:
-        components:
-        - Cuffable # useless if you cant be cuffed
+  - type: SubdermalImplant
+    implantAction: ActionActivateFreedomImplant
+    whitelist:
+      components:
+      - Cuffable # useless if you cant be cuffed
+  - type: TriggerOnActivateImplant
+  - type: UncuffOnTrigger
+    targetUser: true
 
 - type: entity
   parent: BaseSubdermalImplant
   - type: SubdermalImplant
     implantAction: ActionActivateScramImplant
   - type: TriggerOnActivateImplant
-  - type: ScramImplant
+  - type: ScramOnTrigger
+    targetUser: true
 
 - type: entity
   parent: BaseSubdermalImplant
   description: This implant lets the user randomly change their appearance and name once.
   categories: [ HideSpawnMenu ]
   components:
-    - type: SubdermalImplant
-      implantAction: ActionActivateDnaScramblerImplant
-      whitelist:
-        components:
-        - HumanoidAppearance # syndies cant turn hamlet into a human
+  - type: SubdermalImplant
+    implantAction: ActionActivateDnaScramblerImplant
+    whitelist:
+      components:
+      - HumanoidAppearance # syndies cant turn hamlet into a human
+  - type: TriggerOnActivateImplant
+  - type: DnaScrambleOnTrigger
+    targetUser: true
+  - type: DeleteOnTrigger
 
 - type: entity
   categories: [ HideSpawnMenu, Spawner ]