From 671324bef8abb33209159542e16384195381bb93 Mon Sep 17 00:00:00 2001 From: keronshb <54602815+keronshb@users.noreply.github.com> Date: Sun, 20 Nov 2022 01:49:37 -0500 Subject: [PATCH] Implanters and Subdermal Implants (#11840) Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Co-authored-by: metalgearsloth --- Content.Client/Implants/ImplanterSystem.cs | 33 +++ .../Implants/UI/ImplanterStatusControl.cs | 53 +++++ Content.Server/Body/Systems/BodySystem.cs | 15 +- .../Cuffs/Components/CuffableComponent.cs | 133 +++++++------ .../Components/GibOnTriggerComponent.cs | 16 ++ .../Components/OnUseTimerTriggerComponent.cs | 1 + .../TriggerOnMobstateChangeComponent.cs | 16 ++ .../EntitySystems/TriggerSystem.OnUse.cs | 1 + .../Explosion/EntitySystems/TriggerSystem.cs | 47 +++++ Content.Server/Implants/ImplantedSystem.cs | 25 +++ Content.Server/Implants/ImplanterSystem.cs | 188 ++++++++++++++++++ .../Implants/SubdermalImplantSystem.cs | 58 ++++++ Content.Server/Jobs/AddImplantSpecial.cs | 39 ++++ .../SuitSensors/SuitSensorComponent.cs | 7 + .../Medical/SuitSensors/SuitSensorSystem.cs | 19 ++ .../Storage/EntitySystems/StorageSystem.cs | 13 ++ .../Body/Systems/SharedBodySystem.Body.cs | 2 +- .../Implants/Components/ImplantedComponent.cs | 13 ++ .../Implants/Components/ImplanterComponent.cs | 107 ++++++++++ .../Components/SubdermalImplantComponent.cs | 53 +++++ .../TriggerImplantActionComponent.cs | 9 + .../Implants/SharedImplanterSystem.cs | 156 +++++++++++++++ .../Implants/SharedSubdermalImplantSystem.cs | 120 +++++++++++ Resources/Audio/Effects/licenses.txt | 5 + Resources/Audio/Effects/sadtrombone.ogg | Bin 0 -> 34399 bytes Resources/Locale/en-US/implant/implant.ftl | 25 +++ .../prototypes/catalog/cargo/cargo-armory.ftl | 5 +- .../catalog/fills/crates/armory-crates.ftl | 5 +- .../catalog/fills/crates/fun-crates.ftl | 6 + Resources/Prototypes/Actions/types.yml | 33 +++ .../Prototypes/Catalog/Cargo/cargo_armory.yml | 12 +- .../Prototypes/Catalog/Cargo/cargo_fun.yml | 20 ++ .../Catalog/Fills/Crates/armory.yml | 11 +- .../Prototypes/Catalog/Fills/Crates/fun.yml | 18 ++ .../Catalog/Research/technologies.yml | 1 + .../Prototypes/Catalog/uplink_catalog.yml | 50 +++++ .../Entities/Objects/Misc/implanters.yml | 146 ++++++++++++++ .../Objects/Misc/subdermal_implants.yml | 174 ++++++++++++++++ .../Entities/Objects/Specific/syndicate.yml | 10 +- .../Entities/Structures/Machines/lathe.yml | 2 + .../Prototypes/Recipes/Lathes/medical.yml | 9 + .../Prototypes/Roles/Jobs/Civilian/clown.yml | 2 + .../SoundCollections/sadtrombone.yml | 4 + Resources/Prototypes/explosion.yml | 22 +- Resources/Prototypes/tags.yml | 11 + .../Implants/implants.rsi/explosive.png | Bin 0 -> 1811 bytes .../Actions/Implants/implants.rsi/freedom.png | Bin 0 -> 595 bytes .../Actions/Implants/implants.rsi/meta.json | 17 ++ 48 files changed, 1633 insertions(+), 79 deletions(-) create mode 100644 Content.Client/Implants/ImplanterSystem.cs create mode 100644 Content.Client/Implants/UI/ImplanterStatusControl.cs create mode 100644 Content.Server/Explosion/Components/GibOnTriggerComponent.cs create mode 100644 Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs create mode 100644 Content.Server/Implants/ImplantedSystem.cs create mode 100644 Content.Server/Implants/ImplanterSystem.cs create mode 100644 Content.Server/Implants/SubdermalImplantSystem.cs create mode 100644 Content.Server/Jobs/AddImplantSpecial.cs create mode 100644 Content.Shared/Implants/Components/ImplantedComponent.cs create mode 100644 Content.Shared/Implants/Components/ImplanterComponent.cs create mode 100644 Content.Shared/Implants/Components/SubdermalImplantComponent.cs create mode 100644 Content.Shared/Implants/Components/TriggerImplantActionComponent.cs create mode 100644 Content.Shared/Implants/SharedImplanterSystem.cs create mode 100644 Content.Shared/Implants/SharedSubdermalImplantSystem.cs create mode 100644 Resources/Audio/Effects/sadtrombone.ogg create mode 100644 Resources/Locale/en-US/implant/implant.ftl create mode 100644 Resources/Prototypes/Entities/Objects/Misc/implanters.yml create mode 100644 Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml create mode 100644 Resources/Prototypes/SoundCollections/sadtrombone.yml create mode 100644 Resources/Textures/Actions/Implants/implants.rsi/explosive.png create mode 100644 Resources/Textures/Actions/Implants/implants.rsi/freedom.png create mode 100644 Resources/Textures/Actions/Implants/implants.rsi/meta.json diff --git a/Content.Client/Implants/ImplanterSystem.cs b/Content.Client/Implants/ImplanterSystem.cs new file mode 100644 index 0000000000..bf15b9b26b --- /dev/null +++ b/Content.Client/Implants/ImplanterSystem.cs @@ -0,0 +1,33 @@ +using Content.Client.Implants.UI; +using Content.Client.Items; +using Content.Shared.Implants; +using Content.Shared.Implants.Components; +using Robust.Shared.GameStates; + +namespace Content.Client.Implants; + +public sealed class ImplanterSystem : SharedImplanterSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHandleImplanterState); + SubscribeLocalEvent(OnItemImplanterStatus); + } + + private void OnHandleImplanterState(EntityUid uid, ImplanterComponent component, ref ComponentHandleState args) + { + if (args.Current is not ImplanterComponentState state) + return; + + component.CurrentMode = state.CurrentMode; + component.ImplantOnly = state.ImplantOnly; + component.UiUpdateNeeded = true; + } + + private void OnItemImplanterStatus(EntityUid uid, ImplanterComponent component, ItemStatusCollectMessage args) + { + args.Controls.Add(new ImplanterStatusControl(component)); + } +} diff --git a/Content.Client/Implants/UI/ImplanterStatusControl.cs b/Content.Client/Implants/UI/ImplanterStatusControl.cs new file mode 100644 index 0000000000..996e0ad886 --- /dev/null +++ b/Content.Client/Implants/UI/ImplanterStatusControl.cs @@ -0,0 +1,53 @@ +using Content.Client.Message; +using Content.Client.Stylesheets; +using Content.Shared.Implants.Components; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Timing; + +namespace Content.Client.Implants.UI; + +public sealed class ImplanterStatusControl : Control +{ + private readonly ImplanterComponent _parent; + private readonly RichTextLabel _label; + + public ImplanterStatusControl(ImplanterComponent parent) + { + _parent = parent; + _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; + AddChild(_label); + + Update(); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + if (!_parent.UiUpdateNeeded) + return; + + Update(); + } + + private void Update() + { + _parent.UiUpdateNeeded = false; + + var modeStringLocalized = _parent.CurrentMode switch + { + ImplanterToggleMode.Draw => Loc.GetString("implanter-draw-text"), + ImplanterToggleMode.Inject => Loc.GetString("implanter-inject-text"), + _ => Loc.GetString("injector-invalid-injector-toggle-mode") + }; + + var entitiesStringLocalized = _parent.ImplanterSlot.HasItem switch + { + false => Loc.GetString("implanter-empty-text"), + true => Loc.GetString("implanter-implant-text", ("implantName", _parent.ImplantData.Item1), ("implantDescription", _parent.ImplantData.Item2), ("lineBreak", "\n")), + }; + + + _label.SetMarkup(Loc.GetString("implanter-label", ("currentEntities", entitiesStringLocalized), ("modeString", modeStringLocalized), ("lineBreak", "\n"))); + } +} diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs index 46c276f45a..87f4f2492c 100644 --- a/Content.Server/Body/Systems/BodySystem.cs +++ b/Content.Server/Body/Systems/BodySystem.cs @@ -130,7 +130,7 @@ public sealed class BodySystem : SharedBodySystem InitPart(partComponent, prototype, prototype.Root); } - public override HashSet GibBody(EntityUid? bodyId, bool gibOrgans = false, BodyComponent? body = null) + public override HashSet GibBody(EntityUid? bodyId, bool gibOrgans = false, BodyComponent? body = null, bool deleteItems = false) { if (bodyId == null || !Resolve(bodyId.Value, ref body, false)) return new HashSet(); @@ -150,9 +150,16 @@ public sealed class BodySystem : SharedBodySystem { foreach (var ent in cont.ContainedEntities) { - cont.ForceRemove(ent); - Transform(ent).Coordinates = coordinates; - ent.RandomOffset(0.25f); + if (deleteItems) + { + QueueDel(ent); + } + else + { + cont.ForceRemove(ent); + Transform(ent).Coordinates = coordinates; + ent.RandomOffset(0.25f); + } } } } diff --git a/Content.Server/Cuffs/Components/CuffableComponent.cs b/Content.Server/Cuffs/Components/CuffableComponent.cs index d1e07fe1aa..b230b2120f 100644 --- a/Content.Server/Cuffs/Components/CuffableComponent.cs +++ b/Content.Server/Cuffs/Components/CuffableComponent.cs @@ -36,7 +36,7 @@ namespace Content.Server.Cuffs.Components [ViewVariables] public int CuffedHandCount => Container.ContainedEntities.Count * 2; - private EntityUid LastAddedCuffs => Container.ContainedEntities[^1]; + public EntityUid LastAddedCuffs => Container.ContainedEntities[^1]; public IReadOnlyList StoredEntities => Container.ContainedEntities; @@ -254,70 +254,7 @@ namespace Content.Server.Cuffs.Components if (result != DoAfterStatus.Cancelled) { - SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner); - - _entMan.EntitySysManager.GetEntitySystem().DeleteInHandsMatching(user, cuffsToRemove.Value); - _entMan.EntitySysManager.GetEntitySystem().PickupOrDrop(user, cuffsToRemove.Value); - - if (cuff.BreakOnRemove) - { - cuff.Broken = true; - - var meta = _entMan.GetComponent(cuffsToRemove.Value); - meta.EntityName = cuff.BrokenName; - meta.EntityDescription = cuff.BrokenDesc; - - if (_entMan.TryGetComponent(cuffsToRemove, out var sprite) && cuff.BrokenState != null) - { - sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state? - } - - _entMan.AddComponent(cuffsToRemove.Value); - } - - CanStillInteract = _entMan.TryGetComponent(Owner, out HandsComponent? handsComponent) && handsComponent.SortedHands.Count() > CuffedHandCount; - _entMan.EntitySysManager.GetEntitySystem().UpdateCanMove(Owner); - - var ev = new CuffedStateChangeEvent(); - _entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true); - UpdateAlert(); - Dirty(_entMan); - - if (CuffedHandCount == 0) - { - user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-success-message")); - - if (!isOwner) - { - user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message", ("otherName", user))); - } - - if (user == Owner) - { - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed themselves"); - } - else - { - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed {_entMan.ToPrettyString(Owner):player}"); - } - - } - else - { - if (!isOwner) - { - user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", - ("cuffedHandCount", CuffedHandCount), - ("otherName", user))); - user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-partial-success-message", - ("otherName", user), - ("cuffedHandCount", CuffedHandCount))); - } - else - { - user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount))); - } - } + Uncuff(user, cuffsToRemove.Value, cuff, isOwner); } else { @@ -326,5 +263,71 @@ namespace Content.Server.Cuffs.Components return; } + + //Lord forgive me for putting this here + //Cuff ECS when + public void Uncuff(EntityUid user, EntityUid cuffsToRemove, HandcuffComponent cuff, bool isOwner) + { + SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner); + + _entMan.EntitySysManager.GetEntitySystem().DeleteInHandsMatching(user, cuffsToRemove); + _entMan.EntitySysManager.GetEntitySystem().PickupOrDrop(user, cuffsToRemove); + + if (cuff.BreakOnRemove) + { + cuff.Broken = true; + + var meta = _entMan.GetComponent(cuffsToRemove); + meta.EntityName = cuff.BrokenName; + meta.EntityDescription = cuff.BrokenDesc; + + if (_entMan.TryGetComponent(cuffsToRemove, out var sprite) && cuff.BrokenState != null) + { + sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state? + } + + _entMan.AddComponent(cuffsToRemove); + } + + CanStillInteract = _entMan.TryGetComponent(Owner, out HandsComponent? handsComponent) && handsComponent.SortedHands.Count() > CuffedHandCount; + _entMan.EntitySysManager.GetEntitySystem().UpdateCanMove(Owner); + + var ev = new CuffedStateChangeEvent(); + _entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true); + UpdateAlert(); + Dirty(_entMan); + + if (CuffedHandCount == 0) + { + user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-success-message")); + + if (!isOwner) + { + user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message", ("otherName", user))); + } + + if (user == Owner) + { + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed themselves"); + } + else + { + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed {_entMan.ToPrettyString(Owner):player}"); + } + + } + else + { + if (!isOwner) + { + user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount), ("otherName", user))); + user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-partial-success-message", ("otherName", user), ("cuffedHandCount", CuffedHandCount))); + } + else + { + user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount))); + } + } + } } } diff --git a/Content.Server/Explosion/Components/GibOnTriggerComponent.cs b/Content.Server/Explosion/Components/GibOnTriggerComponent.cs new file mode 100644 index 0000000000..7f0b22faa6 --- /dev/null +++ b/Content.Server/Explosion/Components/GibOnTriggerComponent.cs @@ -0,0 +1,16 @@ +namespace Content.Server.Explosion.Components; + +/// +/// Gibs on trigger, self explanatory. +/// Also in case of an implant using this, gibs the implant user instead. +/// +[RegisterComponent] +public sealed class GibOnTriggerComponent : Component +{ + /// + /// Should gibbing also delete the owners items? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("deleteItems")] + public bool DeleteItems = false; +} diff --git a/Content.Server/Explosion/Components/OnUseTimerTriggerComponent.cs b/Content.Server/Explosion/Components/OnUseTimerTriggerComponent.cs index be66d23f9d..ae7dd95815 100644 --- a/Content.Server/Explosion/Components/OnUseTimerTriggerComponent.cs +++ b/Content.Server/Explosion/Components/OnUseTimerTriggerComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.MobState; using Robust.Shared.Audio; namespace Content.Server.Explosion.Components diff --git a/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs b/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs new file mode 100644 index 0000000000..ded8736e20 --- /dev/null +++ b/Content.Server/Explosion/Components/TriggerOnMobstateChangeComponent.cs @@ -0,0 +1,16 @@ +using Content.Shared.MobState; + +namespace Content.Server.Explosion.Components; + +/// +/// Use where you want something to trigger on mobstate change +/// +[RegisterComponent] +public sealed class TriggerOnMobstateChangeComponent : Component +{ + /// + /// What state should trigger this? + /// + [DataField("mobState", required: true)] + public DamageState MobState = DamageState.Alive; +} diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs index d0c0d27449..5b3370b1c2 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs @@ -3,6 +3,7 @@ using Content.Server.Sticky.Events; using Content.Shared.Examine; using Content.Shared.Popups; using Content.Shared.Interaction.Events; +using Content.Shared.MobState; using Content.Shared.Verbs; using Robust.Shared.Player; diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index c0d019805a..4e86cc6dab 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -1,11 +1,13 @@ using System.Linq; using Content.Server.Administration.Logs; +using Content.Server.Body.Systems; using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Explosion.Components; using Content.Server.Flash; using Content.Server.Flash.Components; using Content.Server.Sticky.Events; using Content.Shared.Actions; +using Content.Shared.Body.Components; using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.Physics; @@ -14,7 +16,9 @@ using Robust.Shared.Player; using Content.Shared.Trigger; using Content.Shared.Database; using Content.Shared.Explosion; +using Content.Shared.Implants.Components; using Content.Shared.Interaction; +using Content.Shared.MobState; using Content.Shared.Payload.Components; using Content.Shared.StepTrigger.Systems; using Robust.Server.Containers; @@ -48,6 +52,7 @@ namespace Content.Server.Explosion.EntitySystems [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly BodySystem _body = default!; public override void Initialize() { @@ -61,11 +66,14 @@ namespace Content.Server.Explosion.EntitySystems SubscribeLocalEvent(OnTriggerCollide); SubscribeLocalEvent(OnActivate); + SubscribeLocalEvent(OnImplantTrigger); SubscribeLocalEvent(OnStepTriggered); + SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(HandleDeleteTrigger); SubscribeLocalEvent(HandleExplodeTrigger); SubscribeLocalEvent(HandleFlashTrigger); + SubscribeLocalEvent(HandleGibTrigger); } private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args) @@ -89,6 +97,17 @@ namespace Content.Server.Explosion.EntitySystems args.Handled = true; } + private void HandleGibTrigger(EntityUid uid, GibOnTriggerComponent component, TriggerEvent args) + { + if (!TryComp(uid, out var xform)) + return; + + _body.GibBody(xform.ParentUid, deleteItems: component.DeleteItems); + + args.Handled = true; + } + + private void OnTriggerCollide(EntityUid uid, TriggerOnCollideComponent component, ref StartCollideEvent args) { if(args.OurFixture.ID == component.FixtureID) @@ -101,11 +120,39 @@ namespace Content.Server.Explosion.EntitySystems args.Handled = true; } + private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args) + { + Trigger(uid); + } + private void OnStepTriggered(EntityUid uid, TriggerOnStepTriggerComponent component, ref StepTriggeredEvent args) { Trigger(uid, args.Tripper); } + private void OnMobStateChanged(EntityUid uid, TriggerOnMobstateChangeComponent component, MobStateChangedEvent args) + { + if (component.MobState < args.CurrentMobState) + return; + + //This chains Mobstate Changed triggers with OnUseTimerTrigger if they have it + //Very useful for things that require a mobstate change and a timer + if (TryComp(uid, out var timerTrigger)) + { + HandleTimerTrigger( + uid, + args.Origin, + timerTrigger.Delay, + timerTrigger.BeepInterval, + timerTrigger.InitialBeepDelay, + timerTrigger.BeepSound, + timerTrigger.BeepParams); + } + + else + Trigger(uid); + } + public bool Trigger(EntityUid trigger, EntityUid? user = null) { var triggerEvent = new TriggerEvent(trigger, user); diff --git a/Content.Server/Implants/ImplantedSystem.cs b/Content.Server/Implants/ImplantedSystem.cs new file mode 100644 index 0000000000..5d27e64711 --- /dev/null +++ b/Content.Server/Implants/ImplantedSystem.cs @@ -0,0 +1,25 @@ +using Content.Shared.Implants.Components; +using Robust.Shared.Containers; + +namespace Content.Server.Implants; + +public sealed partial class ImplanterSystem +{ + public void InitializeImplanted() + { + SubscribeLocalEvent(OnImplantedInit); + SubscribeLocalEvent(OnShutdown); + } + + private void OnImplantedInit(EntityUid uid, ImplantedComponent component, ComponentInit args) + { + component.ImplantContainer = _container.EnsureContainer(uid, ImplanterComponent.ImplantSlotId); + component.ImplantContainer.OccludesLight = false; + } + + private void OnShutdown(EntityUid uid, ImplantedComponent component, ComponentShutdown args) + { + //If the entity is deleted, get rid of the implants + _container.CleanContainer(component.ImplantContainer); + } +} diff --git a/Content.Server/Implants/ImplanterSystem.cs b/Content.Server/Implants/ImplanterSystem.cs new file mode 100644 index 0000000000..f5902db56c --- /dev/null +++ b/Content.Server/Implants/ImplanterSystem.cs @@ -0,0 +1,188 @@ +using System.Threading; +using Content.Server.DoAfter; +using Content.Server.Guardian; +using Content.Server.Popups; +using Content.Shared.Hands; +using Content.Shared.IdentityManagement; +using Content.Shared.Implants; +using Content.Shared.Implants.Components; +using Content.Shared.Interaction; +using Content.Shared.MobState.Components; +using Content.Shared.Popups; +using Robust.Shared.Containers; +using Robust.Shared.GameStates; +using Robust.Shared.Player; + +namespace Content.Server.Implants; + +public sealed partial class ImplanterSystem : SharedImplanterSystem +{ + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly DoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + + public override void Initialize() + { + base.Initialize(); + InitializeImplanted(); + + SubscribeLocalEvent(OnHandDeselect); + SubscribeLocalEvent(OnImplanterAfterInteract); + SubscribeLocalEvent(OnImplanterGetState); + + SubscribeLocalEvent(OnImplantAttemptSuccess); + SubscribeLocalEvent(OnDrawAttemptSuccess); + SubscribeLocalEvent(OnImplantAttemptFail); + } + + private void OnImplanterAfterInteract(EntityUid uid, ImplanterComponent component, AfterInteractEvent args) + { + if (args.Target == null || !args.CanReach || args.Handled) + return; + + if (component.CancelToken != null) + { + args.Handled = true; + return; + } + + //Simplemobs and regular mobs should be injectable, but only regular mobs have mind. + //So just don't implant/draw anything that isn't living or is a guardian + //TODO: Rework a bit when surgery is in to work with implant cases + if (!HasComp(args.Target.Value) || HasComp(args.Target.Value)) + return; + + //TODO: Rework when surgery is in for implant cases + if (component.CurrentMode == ImplanterToggleMode.Draw && !component.ImplantOnly) + { + TryDraw(component, args.User, args.Target.Value, uid); + } + else + { + //Implant self instantly, otherwise try to inject the target. + if (args.User == args.Target) + Implant(uid, args.Target.Value, component); + + else + TryImplant(component, args.User, args.Target.Value, uid); + } + args.Handled = true; + } + + private void OnHandDeselect(EntityUid uid, ImplanterComponent component, HandDeselectedEvent args) + { + component.CancelToken?.Cancel(); + component.CancelToken = null; + } + + /// + /// Attempt to implant someone else. + /// + /// Implanter component + /// The entity using the implanter + /// The entity being implanted + /// The implanter being used + public void TryImplant(ImplanterComponent component, EntityUid user, EntityUid target, EntityUid implanter) + { + _popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, Filter.Entities(user)); + + var userName = Identity.Entity(user, EntityManager); + _popup.PopupEntity(Loc.GetString("implanter-component-implanting-target", ("user", userName)), user, Filter.Entities(target), PopupType.LargeCaution); + + component.CancelToken?.Cancel(); + component.CancelToken = new CancellationTokenSource(); + + _doAfter.DoAfter(new DoAfterEventArgs(user, component.ImplantTime, component.CancelToken.Token, target, implanter) + { + BreakOnUserMove = true, + BreakOnTargetMove = true, + BreakOnDamage = true, + BreakOnStun = true, + UsedFinishedEvent = new ImplanterImplantCompleteEvent(implanter, target), + UserCancelledEvent = new ImplanterCancelledEvent() + }); + } + + /// + /// Try to remove an implant and store it in an implanter + /// + /// Implanter component + /// The entity using the implanter + /// The entity getting their implant removed + /// The implanter being used + //TODO: Remove when surgery is in + public void TryDraw(ImplanterComponent component, EntityUid user, EntityUid target, EntityUid implanter) + { + _popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, Filter.Entities(user)); + + component.CancelToken?.Cancel(); + component.CancelToken = new CancellationTokenSource(); + + _doAfter.DoAfter(new DoAfterEventArgs(user, component.DrawTime, component.CancelToken.Token, target ,implanter) + { + BreakOnUserMove = true, + BreakOnTargetMove = true, + BreakOnDamage = true, + BreakOnStun = true, + UsedFinishedEvent = new ImplanterDrawCompleteEvent(implanter, user, target), + UsedCancelledEvent = new ImplanterCancelledEvent() + }); + } + + private void OnImplanterGetState(EntityUid uid, ImplanterComponent component, ref ComponentGetState args) + { + args.State = new ImplanterComponentState(component.CurrentMode, component.ImplantOnly); + } + + private void OnImplantAttemptSuccess(EntityUid uid, ImplanterComponent component, ImplanterImplantCompleteEvent args) + { + component.CancelToken?.Cancel(); + component.CancelToken = null; + Implant(args.Implanter, args.Target, component); + } + + private void OnDrawAttemptSuccess(EntityUid uid, ImplanterComponent component, ImplanterDrawCompleteEvent args) + { + component.CancelToken?.Cancel(); + component.CancelToken = null; + Draw(args.Implanter, args.User, args.Target, component); + } + + private void OnImplantAttemptFail(EntityUid uid, ImplanterComponent component, ImplanterCancelledEvent args) + { + component.CancelToken?.Cancel(); + component.CancelToken = null; + } + + private sealed class ImplanterImplantCompleteEvent : EntityEventArgs + { + public EntityUid Implanter; + public EntityUid Target; + + public ImplanterImplantCompleteEvent(EntityUid implanter, EntityUid target) + { + Implanter = implanter; + Target = target; + } + } + + private sealed class ImplanterCancelledEvent : EntityEventArgs + { + + } + + private sealed class ImplanterDrawCompleteEvent : EntityEventArgs + { + public EntityUid Implanter; + public EntityUid User; + public EntityUid Target; + + public ImplanterDrawCompleteEvent(EntityUid implanter, EntityUid user, EntityUid target) + { + Implanter = implanter; + User = user; + Target = target; + } + } + +} diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs new file mode 100644 index 0000000000..4ead1775f7 --- /dev/null +++ b/Content.Server/Implants/SubdermalImplantSystem.cs @@ -0,0 +1,58 @@ +using Content.Server.Cuffs.Components; +using Content.Shared.Implants; +using Content.Shared.Implants.Components; +using Content.Shared.MobState; +using Robust.Shared.Containers; + +namespace Content.Server.Implants; + +public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem +{ + [Dependency] private readonly SharedContainerSystem _container = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnFreedomImplant); + + SubscribeLocalEvent(RelayToImplantEvent); + } + + private void OnFreedomImplant(EntityUid uid, SubdermalImplantComponent component, UseFreedomImplantEvent args) + { + if (!TryComp(component.ImplantedEntity, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1) + return; + + if (TryComp(cuffs.LastAddedCuffs, out var cuff)) + { + cuffs.Uncuff(component.ImplantedEntity.Value, cuffs.LastAddedCuffs, cuff, true); + } + } + + #region Relays + + + //Relays from the implanted to the implant + private void RelayToImplantEvent(EntityUid uid, ImplantedComponent component, T args) where T : EntityEventArgs + { + if (!_container.TryGetContainer(uid, ImplanterComponent.ImplantSlotId, out var implantContainer)) + return; + + foreach (var implant in implantContainer.ContainedEntities) + { + RaiseLocalEvent(implant, args); + } + } + + //Relays from the implant to the implanted + private void RelayToImplantedEvent(EntityUid uid, SubdermalImplantComponent component, T args) where T : EntityEventArgs + { + if (component.ImplantedEntity != null) + { + RaiseLocalEvent(component.ImplantedEntity.Value, args); + } + } + + #endregion +} diff --git a/Content.Server/Jobs/AddImplantSpecial.cs b/Content.Server/Jobs/AddImplantSpecial.cs new file mode 100644 index 0000000000..c1ec09edae --- /dev/null +++ b/Content.Server/Jobs/AddImplantSpecial.cs @@ -0,0 +1,39 @@ +using Content.Shared.Implants; +using Content.Shared.Implants.Components; +using Content.Shared.Roles; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; + +namespace Content.Server.Jobs; + +/// +/// Adds implants on spawn to the entity +/// +[UsedImplicitly] +public sealed class AddImplantSpecial : JobSpecial +{ + + [DataField("implants", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] + public HashSet Implants { get; } = new(); + + public override void AfterEquip(EntityUid mob) + { + var entMan = IoCManager.Resolve(); + var implantSystem = entMan.System(); + var xformQuery = entMan.GetEntityQuery(); + + if (!xformQuery.TryGetComponent(mob, out var xform)) + return; + + foreach (var implantId in Implants) + { + var implant = entMan.SpawnEntity(implantId, xform.Coordinates); + + if (!entMan.TryGetComponent(implant, out var implantComp)) + return; + + implantSystem.ForceImplant(mob, implant, implantComp); + } + } +} diff --git a/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs b/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs index c1a35b77d5..96a85064d9 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorComponent.cs @@ -34,6 +34,12 @@ namespace Content.Server.Medical.SuitSensors [DataField("activationSlot")] public string ActivationSlot = "jumpsuit"; + /// + /// Activate sensor if user has this in a sensor-compatible container. + /// + [DataField("activationContainer")] + public string? ActivationContainer; + /// /// How often does sensor update its owners status (in seconds). Limited by the system update rate. /// @@ -43,6 +49,7 @@ namespace Content.Server.Medical.SuitSensors /// /// Current user that wears suit sensor. Null if nobody wearing it. /// + [ViewVariables] public EntityUid? User = null; /// diff --git a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs index c80e1eabd1..cd65d46c42 100644 --- a/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs +++ b/Content.Server/Medical/SuitSensors/SuitSensorSystem.cs @@ -9,6 +9,7 @@ using Content.Shared.Inventory.Events; using Content.Shared.Medical.SuitSensor; using Content.Shared.MobState.Components; using Content.Shared.Verbs; +using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Random; @@ -35,6 +36,8 @@ namespace Content.Server.Medical.SuitSensors SubscribeLocalEvent(OnUnequipped); SubscribeLocalEvent(OnExamine); SubscribeLocalEvent>(OnVerb); + SubscribeLocalEvent(OnInsert); + SubscribeLocalEvent(OnRemove); } public override void Update(float frameTime) @@ -150,6 +153,22 @@ namespace Content.Server.Medical.SuitSensors }); } + private void OnInsert(EntityUid uid, SuitSensorComponent component, EntGotInsertedIntoContainerMessage args) + { + if (args.Container.ID != component.ActivationContainer) + return; + + component.User = args.Container.Owner; + } + + private void OnRemove(EntityUid uid, SuitSensorComponent component, EntGotRemovedFromContainerMessage args) + { + if (args.Container.ID != component.ActivationContainer) + return; + + component.User = null; + } + private Verb CreateVerb(EntityUid uid, SuitSensorComponent component, EntityUid userUid, SuitSensorMode mode) { return new Verb() diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index cb045c213b..671235856e 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -29,6 +29,7 @@ using Content.Shared.Destructible; using static Content.Shared.Storage.SharedStorageComponent; using Content.Shared.ActionBlocker; using Content.Shared.CombatMode; +using Content.Shared.Implants.Components; using Content.Shared.Movement.Events; namespace Content.Server.Storage.EntitySystems @@ -62,6 +63,7 @@ namespace Content.Server.Storage.EntitySystems SubscribeLocalEvent>(AddTransferVerbs); SubscribeLocalEvent(OnInteractUsing); SubscribeLocalEvent(OnActivate); + SubscribeLocalEvent(OnImplantActivate); SubscribeLocalEvent(AfterInteract); SubscribeLocalEvent(OnDestroy); SubscribeLocalEvent(OnInteractWithItem); @@ -277,6 +279,17 @@ namespace Content.Server.Storage.EntitySystems OpenStorageUI(uid, args.User, storageComp); } + /// + /// Specifically for storage implants. + /// + private void OnImplantActivate(EntityUid uid, ServerStorageComponent storageComp, OpenStorageImplantEvent args) + { + if (args.Handled || !TryComp(uid, out var xform)) + return; + + OpenStorageUI(uid, xform.ParentUid, storageComp); + } + /// /// Allows a user to pick up entities by clicking them, or pick up all entities in a certain radius /// around a click. diff --git a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs index 60a8739683..80d8d40dc4 100644 --- a/Content.Shared/Body/Systems/SharedBodySystem.Body.cs +++ b/Content.Shared/Body/Systems/SharedBodySystem.Body.cs @@ -162,7 +162,7 @@ public partial class SharedBodySystem } public virtual HashSet GibBody(EntityUid? partId, bool gibOrgans = false, - BodyComponent? body = null) + BodyComponent? body = null, bool deleteItems = false) { if (partId == null || !Resolve(partId.Value, ref body, false)) return new HashSet(); diff --git a/Content.Shared/Implants/Components/ImplantedComponent.cs b/Content.Shared/Implants/Components/ImplantedComponent.cs new file mode 100644 index 0000000000..6317fe77c7 --- /dev/null +++ b/Content.Shared/Implants/Components/ImplantedComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.Containers; + +namespace Content.Shared.Implants.Components; + +/// +/// Added to an entity via the on implant +/// Used in instances where mob info needs to be passed to the implant such as MobState triggers +/// +[RegisterComponent] +public sealed class ImplantedComponent : Component +{ + public Container ImplantContainer = default!; +} diff --git a/Content.Shared/Implants/Components/ImplanterComponent.cs b/Content.Shared/Implants/Components/ImplanterComponent.cs new file mode 100644 index 0000000000..dc34639b8c --- /dev/null +++ b/Content.Shared/Implants/Components/ImplanterComponent.cs @@ -0,0 +1,107 @@ +using System.Threading; +using Content.Shared.Containers.ItemSlots; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Implants.Components; +/// +/// Implanters are used to implant or extract implants from an entity +/// Some can be single use (implant only) or some can draw out an implant +/// +//TODO: Rework drawing to work with implant cases when surgery is in +[RegisterComponent, NetworkedComponent] +public sealed class ImplanterComponent : Component +{ + public const string ImplanterSlotId = "implanter_slot"; + public const string ImplantSlotId = "implant"; + + /// + /// Used for implanters that start with specific implants + /// + [ViewVariables] + [DataField("implant", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? Implant; + + /// + /// The time it takes to implant someone else + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("implantTime")] + public float ImplantTime = 5f; + + //TODO: Remove when surgery is a thing + /// + /// The time it takes to extract an implant from someone + /// It's excessively long to deter from implant checking any antag + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("drawTime")] + public float DrawTime = 300f; + + /// + /// Good for single-use injectors + /// + [ViewVariables] + [DataField("implantOnly")] + public bool ImplantOnly = false; + + /// + /// The current mode of the implanter + /// Mode is changed automatically depending if it implants or draws + /// + [ViewVariables] + [DataField("currentMode")] + public ImplanterToggleMode CurrentMode; + + /// + /// The name and description of the implant to show on the implanter + /// + [ViewVariables] + [DataField("implantData")] + public (string, string) ImplantData; + + /// + /// The for this implanter + /// + [ViewVariables] + [DataField("implanterSlot")] + public ItemSlot ImplanterSlot = new(); + + public bool UiUpdateNeeded; + + public CancellationTokenSource? CancelToken; +} + +[Serializable, NetSerializable] +public sealed class ImplanterComponentState : ComponentState +{ + public ImplanterToggleMode CurrentMode; + public bool ImplantOnly; + + public ImplanterComponentState(ImplanterToggleMode currentMode, bool implantOnly) + { + CurrentMode = currentMode; + ImplantOnly = implantOnly; + } +} + +[Serializable, NetSerializable] +public enum ImplanterToggleMode : byte +{ + Inject, + Draw +} + +[Serializable, NetSerializable] +public enum ImplanterVisuals : byte +{ + Full +} + +[Serializable, NetSerializable] +public enum ImplanterImplantOnlyVisuals : byte +{ + ImplantOnly +} diff --git a/Content.Shared/Implants/Components/SubdermalImplantComponent.cs b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs new file mode 100644 index 0000000000..5a974d05db --- /dev/null +++ b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs @@ -0,0 +1,53 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Implants.Components; + +/// +/// Subdermal implants get stored in a container on an entity and grant the entity special actions +/// The actions can be activated via an action, a passive ability (ie tracking), or a reactive ability (ie on death) or some sort of combination +/// They're added and removed with implanters +/// +[RegisterComponent] +public sealed class SubdermalImplantComponent : Component +{ + /// + /// Used where you want the implant to grant the owner an instant action. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("implantAction")] + public string? ImplantAction; + + /// + /// The entity this implant is inside + /// + [ViewVariables] + public EntityUid? ImplantedEntity; + + /// + /// Should this implant be removeable? + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("permanent")] + public bool Permanent = false; +} + +/// +/// Used for opening the storage implant via action. +/// +public sealed class OpenStorageImplantEvent : InstantActionEvent +{ + +} + +public sealed class UseFreedomImplantEvent : InstantActionEvent +{ + +} + +/// +/// Used for triggering trigger events on the implant via action +/// +public sealed class ActivateImplantEvent : InstantActionEvent +{ + +} diff --git a/Content.Shared/Implants/Components/TriggerImplantActionComponent.cs b/Content.Shared/Implants/Components/TriggerImplantActionComponent.cs new file mode 100644 index 0000000000..a0dd8248b5 --- /dev/null +++ b/Content.Shared/Implants/Components/TriggerImplantActionComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Implants.Components; +/// +/// Triggers implants when the action is pressed +/// +[RegisterComponent] +public sealed class TriggerImplantActionComponent : Component +{ + +} diff --git a/Content.Shared/Implants/SharedImplanterSystem.cs b/Content.Shared/Implants/SharedImplanterSystem.cs new file mode 100644 index 0000000000..53c9cd81dd --- /dev/null +++ b/Content.Shared/Implants/SharedImplanterSystem.cs @@ -0,0 +1,156 @@ +using System.Linq; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.IdentityManagement; +using Content.Shared.Implants.Components; +using Content.Shared.Popups; +using Robust.Shared.Containers; +using Robust.Shared.Player; + +namespace Content.Shared.Implants; + +public abstract class SharedImplanterSystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnImplanterInit); + SubscribeLocalEvent(OnEntInserted); + + } + + private void OnImplanterInit(EntityUid uid, ImplanterComponent component, ComponentInit args) + { + if (component.Implant != null) + component.ImplanterSlot.StartingItem = component.Implant; + + _itemSlots.AddItemSlot(uid, ImplanterComponent.ImplanterSlotId, component.ImplanterSlot); + } + + private void OnEntInserted(EntityUid uid, ImplanterComponent component, EntInsertedIntoContainerMessage args) + { + var implantData = EntityManager.GetComponent(args.Entity); + component.ImplantData = (implantData.EntityName, implantData.EntityDescription); + } + + //Instantly implant something and add all necessary components and containers. + //Set to draw mode if not implant only + public void Implant(EntityUid implanter, EntityUid target, ImplanterComponent component) + { + var implanterContainer = component.ImplanterSlot.ContainerSlot; + + if (implanterContainer is null) + return; + + var implant = implanterContainer.ContainedEntities.FirstOrDefault(); + + if (!TryComp(implant, out var implantComp)) + return; + + //If the target doesn't have the implanted component, add it. + var implantedComp = EnsureComp(target); + var implantContainer = implantedComp.ImplantContainer; + + implanterContainer.Remove(implant); + implantComp.ImplantedEntity = target; + implantContainer.OccludesLight = false; + implantContainer.Insert(implant); + + if (component.CurrentMode == ImplanterToggleMode.Inject && !component.ImplantOnly) + DrawMode(component); + + else + ImplantMode(component); + + Dirty(component); + } + + //Draw the implant out of the target + //TODO: Rework when surgery is in so implant cases can be a thing + public void Draw(EntityUid implanter, EntityUid user, EntityUid target, ImplanterComponent component) + { + var implanterContainer = component.ImplanterSlot.ContainerSlot; + + if (implanterContainer is null) + return; + + var permanentFound = false; + + if (_container.TryGetContainer(target, ImplanterComponent.ImplantSlotId, out var implantContainer)) + { + var implantCompQuery = GetEntityQuery(); + + foreach (var implant in implantContainer.ContainedEntities) + { + if (!implantCompQuery.TryGetComponent(implant, out var implantComp)) + return; + + //Don't remove a permanent implant and look for the next that can be drawn + if (!implantContainer.CanRemove(implant)) + { + var implantName = Identity.Entity(implant, EntityManager); + var targetName = Identity.Entity(target, EntityManager); + var failedPermanentMessage = Loc.GetString("implanter-draw-failed-permanent", ("implant", implantName), ("target", targetName)); + _popup.PopupEntity(failedPermanentMessage, target, Filter.Entities(user)); + permanentFound = implantComp.Permanent; + continue; + } + + implantContainer.Remove(implant); + implantComp.ImplantedEntity = null; + implanterContainer.Insert(implant); + permanentFound = implantComp.Permanent; + //Break so only one implant is drawn + break; + } + + if (component.CurrentMode == ImplanterToggleMode.Draw && !component.ImplantOnly && !permanentFound) + ImplantMode(component); + + Dirty(component); + } + } + + private void ImplantMode(ImplanterComponent component) + { + component.CurrentMode = ImplanterToggleMode.Inject; + ChangeOnImplantVisualizer(component); + } + + private void DrawMode(ImplanterComponent component) + { + component.CurrentMode = ImplanterToggleMode.Draw; + ChangeOnImplantVisualizer(component); + } + + private void ChangeOnImplantVisualizer(ImplanterComponent component) + { + if (!TryComp(component.Owner, out var appearance)) + return; + + bool implantFound; + + if (component.ImplanterSlot.HasItem) + implantFound = true; + + else + implantFound = false; + + if (component.CurrentMode == ImplanterToggleMode.Inject && !component.ImplantOnly) + _appearance.SetData(component.Owner, ImplanterVisuals.Full, implantFound, appearance); + + else if (component.CurrentMode == ImplanterToggleMode.Inject && component.ImplantOnly) + { + _appearance.SetData(component.Owner, ImplanterVisuals.Full, implantFound, appearance); + _appearance.SetData(component.Owner, ImplanterImplantOnlyVisuals.ImplantOnly, component.ImplantOnly, appearance); + } + + else + _appearance.SetData(component.Owner, ImplanterVisuals.Full, implantFound, appearance); + } +} diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs new file mode 100644 index 0000000000..858748255d --- /dev/null +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -0,0 +1,120 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Implants.Components; +using Content.Shared.Tag; +using Robust.Shared.Containers; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Implants; + +public abstract class SharedSubdermalImplantSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly TagSystem _tag = default!; + + public const string BaseStorageId = "storagebase"; + + public override void Initialize() + { + SubscribeLocalEvent(OnInsert); + SubscribeLocalEvent(OnRemoveAttempt); + SubscribeLocalEvent(OnRemove); + } + + private void OnInsert(EntityUid uid, SubdermalImplantComponent component, EntGotInsertedIntoContainerMessage args) + { + if (component.ImplantedEntity == null) + return; + + if (component.ImplantAction != null) + { + var action = new InstantAction(_prototypeManager.Index(component.ImplantAction)); + _actionsSystem.AddAction(component.ImplantedEntity.Value, action, uid); + } + + //replace micro bomb with macro bomb + if (_container.TryGetContainer(component.ImplantedEntity.Value, ImplanterComponent.ImplantSlotId, out var implantContainer) && _tag.HasTag(uid, "MacroBomb")) + { + foreach (var implant in implantContainer.ContainedEntities) + { + if (_tag.HasTag(implant, "MicroBomb")) + { + implantContainer.Remove(implant); + QueueDel(implant); + } + } + } + } + + private void OnRemoveAttempt(EntityUid uid, SubdermalImplantComponent component, ContainerGettingRemovedAttemptEvent args) + { + if (component.Permanent && component.ImplantedEntity != null) + args.Cancel(); + } + + private void OnRemove(EntityUid uid, SubdermalImplantComponent component, EntGotRemovedFromContainerMessage args) + { + if (component.ImplantedEntity == null) + return; + + var entCoords = Transform(component.ImplantedEntity.Value).Coordinates; + + if (component.ImplantAction != null) + _actionsSystem.RemoveProvidedActions(component.ImplantedEntity.Value, uid); + + if (!_container.TryGetContainer(uid, BaseStorageId, out var storageImplant)) + return; + + _container.EmptyContainer(storageImplant, moveTo: entCoords); + } + + /// + /// Forces an implant into a person + /// Good for on spawn related code or admin additions + /// + /// The entity to be implanted + /// The implant + /// The implant component + public void ForceImplant(EntityUid target, EntityUid implant, SubdermalImplantComponent component) + { + //If the target doesn't have the implanted component, add it. + var implantedComp = EnsureComp(target); + var implantContainer = implantedComp.ImplantContainer; + + component.ImplantedEntity = target; + implantContainer.Insert(implant); + } + + /// + /// Force remove a singular implant + /// + /// the implanted entity + /// the implant + /// the implant component + public void ForceRemove(EntityUid target, EntityUid implant) + { + if (!TryComp(target, out var implanted)) + return; + + var implantContainer = implanted.ImplantContainer; + + implantContainer.Remove(implant); + QueueDel(implant); + } + + /// + /// Removes and deletes implants by force + /// + /// The entity to have implants removed + public void WipeImplants(EntityUid target) + { + if (!TryComp(target, out var implanted)) + return; + + var implantContainer = implanted.ImplantContainer; + + _container.CleanContainer(implantContainer); + } +} diff --git a/Resources/Audio/Effects/licenses.txt b/Resources/Audio/Effects/licenses.txt index 94c3613c63..93e2815927 100644 --- a/Resources/Audio/Effects/licenses.txt +++ b/Resources/Audio/Effects/licenses.txt @@ -32,6 +32,11 @@ bite.ogg take from https://github.com/tgstation/tgstation/commit/d4f678a1772007f bone_rattle.ogg licensed under CC0 1.0 and taken from spookymodem at https://freesound.org/people/spookymodem/sounds/202102/ +- files: ["sadtrombone.ogg"] + license: "CC-BY-NC-SA-3.0" + copyright: "sadtrombone.ogg taken from Citadel Station." + source: "https://github.com/Citadel-Station-13/Citadel-Station-13/commit/35a1723e98a60f375df590ca572cc90f1bb80bd5" + The following sounds are taken from TGstation github (licensed under CC by 3.0): demon_attack1.ogg: taken at https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0 diff --git a/Resources/Audio/Effects/sadtrombone.ogg b/Resources/Audio/Effects/sadtrombone.ogg new file mode 100644 index 0000000000000000000000000000000000000000..788d263cba925064fabb37b1579c979036cdef0a GIT binary patch literal 34399 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