diff --git a/Content.Client/Forensics/Systems/ForensicsSystem.cs b/Content.Client/Forensics/Systems/ForensicsSystem.cs new file mode 100644 index 0000000000..048fff600e --- /dev/null +++ b/Content.Client/Forensics/Systems/ForensicsSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Forensics.Systems; + +namespace Content.Client.Forensics.Systems; + +public sealed class ForensicsSystem : SharedForensicsSystem; diff --git a/Content.Server/Forensics/Systems/ForensicsSystem.cs b/Content.Server/Forensics/Systems/ForensicsSystem.cs index 9f94e39fb7..cc74c1d141 100644 --- a/Content.Server/Forensics/Systems/ForensicsSystem.cs +++ b/Content.Server/Forensics/Systems/ForensicsSystem.cs @@ -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 - - /// - /// 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. - /// - public void RandomizeDNA(Entity ent) + public override void RandomizeDNA(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return; @@ -334,11 +330,7 @@ namespace Content.Server.Forensics RaiseLocalEvent(ent.Owner, ref ev); } - /// - /// Give the entity a new, random fingerprint string. - /// Does nothing if it does not have the FingerprintComponent. - /// - public void RandomizeFingerprint(Entity ent) + public override void RandomizeFingerprint(Entity 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 index f3bbc9e584..0000000000 --- a/Content.Server/Implants/Components/ScramImplantComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Implants; -using Robust.Shared.Audio; - -namespace Content.Server.Implants.Components; - -/// -/// Randomly teleports entity when triggered. -/// -[RegisterComponent] -public sealed partial class ScramImplantComponent : Component -{ - /// - /// Up to how far to teleport the user - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float TeleportRadius = 100f; - - [DataField, ViewVariables(VVAccess.ReadWrite)] - public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg"); -} diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs index e2482b7b60..f0530358a6 100644 --- a/Content.Server/Implants/SubdermalImplantSystem.cs +++ b/Content.Server/Implants/SubdermalImplantSystem.cs @@ -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 _physicsQuery; - private HashSet> _targetGrids = []; - public override void Initialize() { base.Initialize(); - _physicsQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnFreedomImplant); SubscribeLocalEvent>(OnStoreRelay); - SubscribeLocalEvent(OnActivateImplantEvent); - SubscribeLocalEvent(OnScramImplant); - SubscribeLocalEvent(OnDnaScramblerImplant); - } private void OnStoreRelay(EntityUid uid, StoreComponent store, ImplantRelayEvent 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(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(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(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(ent, out var puller) && TryComp(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? 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 = _mapSystem.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 _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(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(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 index 0000000000..1220b75fff --- /dev/null +++ b/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs @@ -0,0 +1,18 @@ +using Content.Shared.Forensics.Components; + +namespace Content.Shared.Forensics.Systems; + +public abstract class SharedForensicsSystem : EntitySystem +{ + /// + /// 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. + /// + public virtual void RandomizeDNA(Entity ent) { } + + /// + /// Give the entity a new, random fingerprint string. + /// Does nothing if it does not have the FingerprintComponent. + /// + public virtual void RandomizeFingerprint(Entity ent) { } +} diff --git a/Content.Shared/Implants/Components/SubdermalImplantComponent.cs b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs index bd0ff09678..390d113dfb 100644 --- a/Content.Shared/Implants/Components/SubdermalImplantComponent.cs +++ b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs @@ -49,7 +49,7 @@ public sealed partial class SubdermalImplantComponent : Component /// [DataField] public EntityWhitelist? Blacklist; - + /// /// 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 -{ - -} - /// /// Used for triggering trigger events on the implant via action /// @@ -86,13 +81,3 @@ public sealed partial class OpenUplinkImplantEvent : InstantActionEvent { } - -public sealed partial class UseScramImplantEvent : InstantActionEvent -{ - -} - -public sealed partial class UseDnaScramblerImplantEvent : InstantActionEvent -{ - -} diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index 177e24ff02..4c015f1209 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -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 index 0000000000..1f3767b392 --- /dev/null +++ b/Content.Shared/Trigger/Components/Effects/DnaScrambleOnTriggerComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Trigger.Components.Effects; + +/// +/// 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. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class DnaScrambleOnTriggerComponent : BaseXOnTriggerComponent; diff --git a/Content.Shared/Trigger/Components/Effects/RattleOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/RattleOnTriggerComponent.cs index 599a64339a..fa1175c3cb 100644 --- a/Content.Shared/Trigger/Components/Effects/RattleOnTriggerComponent.cs +++ b/Content.Shared/Trigger/Components/Effects/RattleOnTriggerComponent.cs @@ -24,7 +24,7 @@ public sealed partial class RattleOnTriggerComponent : BaseXOnTriggerComponent [DataField] public Dictionary 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 index 0000000000..bacf0f69e8 --- /dev/null +++ b/Content.Shared/Trigger/Components/Effects/ScramOnTriggerComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Trigger.Components.Effects; + +/// +/// Randomly teleports the entity when triggered. +/// If TargetUser is true the user will be teleported instead. +/// Used for scram implants. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ScramOnTriggerComponent : BaseXOnTriggerComponent +{ + /// + /// Up to how far to teleport the entity. + /// + [DataField, AutoNetworkedField] + public float TeleportRadius = 100f; + + /// + /// the sound to play when teleporting. + /// + [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 index 0000000000..770882f3e6 --- /dev/null +++ b/Content.Shared/Trigger/Components/Effects/UncuffOnTriggerComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Trigger.Components.Effects; + +/// +/// Removes a pair of handcuffs from the entity. +/// If TargetUser is true the user will be uncuffed instead. +/// +[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 index 0000000000..246c6a8c7a --- /dev/null +++ b/Content.Shared/Trigger/Systems/DnaScrambleOnTriggerSystem.cs @@ -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(OnTrigger); + } + + 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; + + if (!TryComp(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(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 index 0000000000..163012cec5 --- /dev/null +++ b/Content.Shared/Trigger/Systems/ScramOnTriggerSystem.cs @@ -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 _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; + } +} diff --git a/Content.Shared/Trigger/Systems/UncuffOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/UncuffOnTriggerSystem.cs new file mode 100644 index 0000000000..9b83c4cf8e --- /dev/null +++ b/Content.Shared/Trigger/Systems/UncuffOnTriggerSystem.cs @@ -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(OnTrigger); + } + + 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; + + if (!TryComp(target.Value, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1) + return; + + _cuffable.Uncuff(target.Value, args.User, cuffs.LastAddedCuffs); + args.Handled = true; + } +} diff --git a/Resources/Locale/en-US/implant/implant.ftl b/Resources/Locale/en-US/implant/implant.ftl index 8cddef4c81..3f38ae443f 100644 --- a/Resources/Locale/en-US/implant/implant.ftl +++ b/Resources/Locale/en-US/implant/implant.ftl @@ -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 index 0000000000..3d090f1ae3 --- /dev/null +++ b/Resources/Locale/en-US/triggers/rattle-on-trigger.ftl @@ -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 index 0000000000..1e84766032 --- /dev/null +++ b/Resources/Locale/en-US/triggers/scramble-on-trigger.ftl @@ -0,0 +1 @@ +scramble-on-trigger-popup = Your appearance shifts and changes! diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index e6587ae6b8..97435c229d 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -121,7 +121,7 @@ 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 @@ -135,8 +135,6 @@ icon: sprite: Actions/Implants/implants.rsi state: freedom - - type: InstantAction - event: !type:UseFreedomImplantEvent - type: entity parent: BaseAction @@ -171,7 +169,7 @@ state: icon - type: entity - parent: BaseAction + parent: BaseImplantAction id: ActionActivateScramImplant name: SCRAM! description: Randomly teleports you within a large distance. @@ -186,11 +184,9 @@ 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. @@ -205,8 +201,6 @@ icon: sprite: Clothing/OuterClothing/Hardsuits/lingspacesuit.rsi state: icon - - type: InstantAction - event: !type:UseDnaScramblerImplantEvent - type: entity parent: BaseAction @@ -409,12 +403,12 @@ 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 diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index a369a730cf..6a4ad24664 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -135,11 +135,14 @@ 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 @@ -198,7 +201,8 @@ - type: SubdermalImplant implantAction: ActionActivateScramImplant - type: TriggerOnActivateImplant - - type: ScramImplant + - type: ScramOnTrigger + targetUser: true - type: entity parent: BaseSubdermalImplant @@ -207,11 +211,15 @@ 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 ]