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 0000000000..788d263cba Binary files /dev/null and b/Resources/Audio/Effects/sadtrombone.ogg differ diff --git a/Resources/Locale/en-US/implant/implant.ftl b/Resources/Locale/en-US/implant/implant.ftl new file mode 100644 index 0000000000..81ff95311b --- /dev/null +++ b/Resources/Locale/en-US/implant/implant.ftl @@ -0,0 +1,25 @@ +## Implanter Attempt Messages + +implanter-component-implanting-target = {$user} is trying to implant you with something! +implanter-draw-failed-permanent = The {$implant} in {$target} is fused with them and cannot be removed! +implanter-draw-failed = You tried to remove an implant but found nothing. + +## UI +implanter-draw-text = Draw +implanter-inject-text = Inject + +implanter-empty-text = None +implanter-implant-text = {$implantName}{$lineBreak}{$implantDescription} + +implanter-label = [color=white]Implant: {$currentEntities}{$lineBreak}Mode: {$modeString}[/color] + +## Implanter Actions + +open-storage-implant-action-name = open storage implant +open-storage-implant-action-description = opens the storage implant embedded under your skin + +activate-micro-bomb-action-name = activate micro bomb +activate-micro-bomb-action-description = activates your internal microbomb, completely destroying you and your equipment + +use-freedom-implant-action-name = use freedom implant +use-freedom-implant-action-description = activating the implant will free you from any hand restraints diff --git a/Resources/Locale/en-US/prototypes/catalog/cargo/cargo-armory.ftl b/Resources/Locale/en-US/prototypes/catalog/cargo/cargo-armory.ftl index 2c33cd2acd..340efc730d 100644 --- a/Resources/Locale/en-US/prototypes/catalog/cargo/cargo-armory.ftl +++ b/Resources/Locale/en-US/prototypes/catalog/cargo/cargo-armory.ftl @@ -4,5 +4,8 @@ ent-ArmorySmg = { ent-CrateArmorySMG } ent-ArmoryShotgun = { ent-CrateArmoryShotgun } .desc = { ent-CrateArmoryShotgun.desc } +ent-TrackingImplants = { ent-CrateTrackingImplants } + .desc = { ent-CrateTrackingImplants.desc } + ent-ArmoryLaser = { ent-CrateArmoryLaser } - .desc = { ent-CrateArmoryLaser.desc } \ No newline at end of file + .desc = { ent-CrateArmoryLaser.desc } diff --git a/Resources/Locale/en-US/prototypes/catalog/fills/crates/armory-crates.ftl b/Resources/Locale/en-US/prototypes/catalog/fills/crates/armory-crates.ftl index d119182b23..ea92aa9f0f 100644 --- a/Resources/Locale/en-US/prototypes/catalog/fills/crates/armory-crates.ftl +++ b/Resources/Locale/en-US/prototypes/catalog/fills/crates/armory-crates.ftl @@ -4,5 +4,8 @@ ent-CrateArmorySMG = SMG crate ent-CrateArmoryShotgun = Shotgun crate .desc = For when the enemy absolutely needs to be replaced with lead. Contains two Enforcer Combat Shotguns, and some standard shotgun shells. Requires Armory access to open. +ent-CrateTrackingImplants = Tracking implants + .desc = Contains a handful of tracking implanters. Good for prisoners you'd like to release but still keep track of. + ent-CrateArmoryLaser = lasers crate - .desc = Contains three lethal, high-energy laser guns. Requires Armory access to open. \ No newline at end of file + .desc = Contains three lethal, high-energy laser guns. Requires Armory access to open. diff --git a/Resources/Locale/en-US/prototypes/catalog/fills/crates/fun-crates.ftl b/Resources/Locale/en-US/prototypes/catalog/fills/crates/fun-crates.ftl index f331660b45..a6824d83d0 100644 --- a/Resources/Locale/en-US/prototypes/catalog/fills/crates/fun-crates.ftl +++ b/Resources/Locale/en-US/prototypes/catalog/fills/crates/fun-crates.ftl @@ -16,5 +16,11 @@ ent-CrateFunBoardGames = Board game crate ent-CrateFunATV = ATV crate .desc = An Absolutely Taxable Vehicle to help cargo with hauling. +ent-CrateFunSadTromboneImplants = Sad Trombone Implants + .desc = Death's never been so fun before! Implant these to make dying a bit more happy. + +ent-CrateFunLightImplants = Light Implants + .desc = Light up your skin with these implants! + ent-CrateFunSyndicateSegway = Syndicate segway crate .desc = A crate containing a two-wheeler that will help you escape from the security officers. Or not. diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index aaddd9965f..5510c1e1ff 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -22,6 +22,39 @@ iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png event: !type:ToggleActionEvent +- type: instantAction + id: OpenStorageImplant + name: open-storage-implant-action-name + description: open-storage-implant-action-description + itemIconStyle: BigAction + icon: + sprite: Clothing/Back/Backpacks/backpack.rsi + state: icon + event: !type:OpenStorageImplantEvent + +- type: instantAction + id: ActivateMicroBomb + name: activate-micro-bomb-action-name + description: activate-micro-bomb-action-description + checkCanInteract: false + itemIconStyle: BigAction + icon: + sprite: Actions/Implants/implants.rsi + state: explosive + event: !type:ActivateImplantEvent + +- type: instantAction + id: ActivateFreedomImplant + name: use-freedom-implant-action-name + description: use-freedom-implant-action-description + charges: 3 + checkCanInteract: false + itemIconStyle: BigAction + icon: + sprite: Actions/Implants/implants.rsi + state: freedom + event: !type:UseFreedomImplantEvent + - type: instantAction id: ToggleSuitHelmet name: action-name-hardsuit diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_armory.yml b/Resources/Prototypes/Catalog/Cargo/cargo_armory.yml index c66fe08bee..d989e10fe8 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_armory.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_armory.yml @@ -18,6 +18,16 @@ category: Armory group: market +- type: cargoProduct + id: TrackingImplant + icon: + sprite: Objects/Specific/Chemistry/syringe.rsi + state: syringe_base0 + product: CrateTrackingImplants + cost: 1000 + category: Armory + group: market + - type: cargoProduct id: ArmoryLaser icon: @@ -26,4 +36,4 @@ product: CrateArmoryLaser cost: 1600 category: Armory - group: market \ No newline at end of file + group: market diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml b/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml index cf81814d0e..d94fda2292 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml @@ -57,3 +57,23 @@ cost: 1500 category: Fun group: market + +- type: cargoProduct + id: FunSadTromboneImplants + icon: + sprite: Objects/Specific/Chemistry/syringe.rsi + state: syringe_base0 + product: CrateFunSadTromboneImplants + cost: 1000 + category: Fun + group: market + +- type: cargoProduct + id: FunLightImplants + icon: + sprite: Objects/Specific/Chemistry/syringe.rsi + state: syringe_base0 + product: CrateFunLightImplants + cost: 1000 + category: Fun + group: market diff --git a/Resources/Prototypes/Catalog/Fills/Crates/armory.yml b/Resources/Prototypes/Catalog/Fills/Crates/armory.yml index cd07e42e72..fe37c3fb84 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/armory.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/armory.yml @@ -20,6 +20,15 @@ - id: BoxLethalshot amount: 3 +- type: entity + id: CrateTrackingImplants + parent: CrateWeaponSecure + components: + - type: StorageFill + contents: + - id: TrackingImplanter + amount: 5 + - type: entity id: CrateArmoryLaser parent: CrateWeaponSecure @@ -27,4 +36,4 @@ - type: StorageFill contents: - id: WeaponLaserGun - amount: 3 \ No newline at end of file + amount: 3 diff --git a/Resources/Prototypes/Catalog/Fills/Crates/fun.yml b/Resources/Prototypes/Catalog/Fills/Crates/fun.yml index 2d7b6ff33d..3954425b5b 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/fun.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/fun.yml @@ -118,6 +118,24 @@ - id: VehicleKeyATV amount: 1 +- type: entity + id: CrateFunSadTromboneImplants + parent: CrateGenericSteel + components: + - type: StorageFill + contents: + - id: SadTromboneImplanter + amount: 3 + +- type: entity + id: CrateFunLightImplants + parent: CrateGenericSteel + components: + - type: StorageFill + contents: + - id: LightImplanter + amount: 3 + - type: entity id: CrateFunSyndicateSegway parent: CrateLivestock diff --git a/Resources/Prototypes/Catalog/Research/technologies.yml b/Resources/Prototypes/Catalog/Research/technologies.yml index 4da5fbc1b9..e8d0c6fd60 100644 --- a/Resources/Prototypes/Catalog/Research/technologies.yml +++ b/Resources/Prototypes/Catalog/Research/technologies.yml @@ -51,6 +51,7 @@ - ClothingHandsGlovesNitrile - ClothingMaskSterile - DiseaseSwab + - Implanter - type: technology name: technologies-advanced-botany diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 9a1d1ce1ea..150ddeeeda 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -280,6 +280,56 @@ categories: - UplinkUtility +# Utility-Implants + +- type: listing + id: UplinkStorageImplanter + icon: /Textures/Clothing/Back/Backpacks/backpack.rsi/icon.png + name: Storage Implanter + description: Hide goodies inside of yourself with new bluespace technology! + productEntity: StorageImplanter + cost: + Telecrystal: 12 + categories: + - UplinkUtility + conditions: + - !type:StoreWhitelistCondition + blacklist: + tags: + - NukeOpsUplink + +- type: listing + id: UplinkFreedomImplanter + icon: /Textures/Actions/Implants/implants.rsi/freedom.png + name: Freedom Implanter + description: Get away from those nasty sec officers with this three use implant! + productEntity: FreedomImplanter + cost: + Telecrystal: 10 + categories: + - UplinkUtility + conditions: + - !type:StoreWhitelistCondition + blacklist: + tags: + - NukeOpsUplink + +- type: listing + id: UplinkMacroBombImplanter + icon: /Textures/Actions/Implants/implants.rsi/explosive.png + name: Macro Bomb Implanter + description: Inject this and on death you'll create a large explosion. Huge team casualty cost, use at own risk. Replaces internal micro bomb. + productEntity: MacroBombImplanter + cost: + Telecrystal: 20 + categories: + - UplinkUtility + conditions: + - !type:StoreWhitelistCondition + blacklist: + tags: + - TraitorUplink + # Bundles - type: listing diff --git a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml new file mode 100644 index 0000000000..c240be2b51 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml @@ -0,0 +1,146 @@ +# Implanters + +- type: entity + name: implanter + description: a syringe fitted to be used exclusively with implants + id: BaseImplanter + parent: BaseItem + abstract: true + components: + - type: ItemSlots + - type: ContainerContainer + containers: + implanter_slot: !type:ContainerSlot { } + - type: Implanter + currentMode: Draw + implanterSlot: + name: Implant + priority: 0 + whitelist: + tags: + - SubdermalImplant + - type: Sprite + sprite: Objects/Specific/Chemistry/syringe.rsi + netsync: false + state: syringe_base0 + layers: + - state: syringe4 + map: [ "implantFull" ] + color: '#1cd94e' + visible: false + - state: syringe_base0 + map: [ "implantOnly" ] + - type: Item + sprite: Objects/Specific/Chemistry/syringe.rsi + heldPrefix: 0 + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ImplanterVisuals.Full: + implantFull: + True: {visible: true} + False: {visible: false} + enum.ImplanterImplantOnlyVisuals.ImplantOnly: + implantOnly: + True: {state: broken} + False: {state: syringe_base0} + +- type: entity + id: Implanter + parent: BaseImplanter + components: + - type: Tag + tags: + - Trash + +- type: entity + id: BaseImplantOnlyImplanter + parent: Implanter + description: a single use implanter + abstract: true + components: + - type: Sprite + sprite: Objects/Specific/Chemistry/syringe.rsi + netsync: false + state: syringe_base0 + layers: + - state: syringe4 + map: [ "implantFull" ] + color: '#1cd94e' + visible: true + - state: syringe_base0 + map: [ "implantOnly" ] + - type: Implanter + currentMode: Inject + implantOnly: true + +#Fun implanters + +- type: entity + id: SadTromboneImplanter + name: sad trombone implanter + description: a single use implanter, the implant plays a sad tune on death + parent: BaseImplantOnlyImplanter + components: + - type: Implanter + implant: SadTromboneImplant + +- type: entity + id: LightImplanter + name: light implanter + description: a single use implanter, the implant emits light on activation + parent: BaseImplantOnlyImplanter + components: + - type: Implanter + implant: LightImplant + +#Security implanters + +- type: entity + id: TrackingImplanter + name: tracking implanter + description: a single use implanter, the implant tracks + parent: BaseImplantOnlyImplanter + components: + - type: Implanter + implant: TrackingImplant + +#Traitor implanters + +- type: entity + id: StorageImplanter + name: storage implanter + description: a single use implanter, the implant grants hidden storage + parent: BaseImplantOnlyImplanter + components: + - type: Implanter + implant: StorageImplant + +- type: entity + id: FreedomImplanter + name: freedom implanter + description: a single use implanter, the implant lets the user break out of hand restraints three times + parent: BaseImplantOnlyImplanter + components: + - type: Implanter + implant: FreedomImplant + +#Nuclear Operative/Special implanters + +- type: entity + id: MicroBombImplanter + name: micro bomb implanter + description: a single use implanter, the implant is permanent and blows the user up on death + parent: BaseImplantOnlyImplanter + components: + - type: Implanter + implant: MicroBombImplant + +- type: entity + id: MacroBombImplanter + name: macro bomb implanter + description: a single use implanter, the implant creates a large explosion on death after the alloted time + parent: BaseImplantOnlyImplanter + components: + - type: Implanter + implant: MacroBombImplant diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml new file mode 100644 index 0000000000..cd25cdd496 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -0,0 +1,174 @@ +- type: entity + parent: BaseItem + id: BaseSubdermalImplant + name: implant + description: a microscopic chip that's injected under the skin + abstract: true + components: + - type: SubdermalImplant + - type: Tag + tags: + - SubdermalImplant + - HideContextMenu + +#Fun implants + +- type: entity + parent: BaseSubdermalImplant + id: SadTromboneImplant + name: sad trombone implant + description: plays a sad tune when the user dies + noSpawn: true + components: + - type: SubdermalImplant + - type: TriggerOnMobstateChange + mobState: Dead + - type: EmitSoundOnTrigger + sound: + collection: SadTrombone + params: + variation: 0.125 + +- type: entity + parent: BaseSubdermalImplant + id: LightImplant + name: light implant + description: makes your skin emit a faint light + noSpawn: true + components: + - type: SubdermalImplant + implantAction: ToggleLight + - type: PointLight + enabled: false + radius: 2.5 + softness: 5 + mask: /Textures/Effects/LightMasks/cone.png + autoRot: true + - type: Tag + tags: + - SubdermalImplant + - HideContextMenu + - Flashlight + - type: UnpoweredFlashlight + toggleAction: + name: action-name-toggle-light + description: action-description-toggle-light + icon: Objects/Tools/flashlight.rsi/flashlight.png + iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png + event: !type:ToggleActionEvent + +#Security implants + +- type: entity + parent: BaseSubdermalImplant + id: TrackingImplant + name: tracking implant + description: tracks whoever was implanted via the suit sensor network + noSpawn: true + components: + - type: SubdermalImplant + - type: SuitSensor + randomMode: false + controlsLocked: true + mode: SensorCords + activationContainer: "ImplantContainer" + - type: DeviceNetwork + deviceNetId: Wireless + transmitFrequencyId: SuitSensor + - type: WirelessNetworkConnection + range: 500 + +#Traitor implants + +- type: entity + parent: BaseSubdermalImplant + id: StorageImplant + name: storage implant + description: made with bluespace technology, allows the user to fit a few items in hidden storage + noSpawn: true + components: + - type: SubdermalImplant + implantAction: OpenStorageImplant + - type: Item + size: 9999 + - type: Storage + capacity: 20 #10-20 should be more than enough for this + - type: ContainerContainer + containers: + storagebase: !type:Container + ents: [ ] + - type: UserInterface + interfaces: + - key: enum.StorageUiKey.Key + type: StorageBoundUserInterface + +- type: entity + parent: BaseSubdermalImplant + id: FreedomImplant + name: freedom implant + description: you can break these cuffs + noSpawn: true + components: + - type: SubdermalImplant + implantAction: ActivateFreedomImplant + +#Nuclear Operative/Special Exclusive implants + +- type: entity + parent: BaseSubdermalImplant + id: MicroBombImplant + name: micro bomb implant + description: mission failed, user blows up on death to prevent company equipment from being stolen + noSpawn: true + components: + - type: SubdermalImplant + permanent: true + implantAction: ActivateMicroBomb + - type: TriggerOnMobstateChange + mobState: Dead + - type: TriggerImplantAction + - type: ExplodeOnTrigger + - type: GibOnTrigger + deleteItems: true + - type: Explosive + explosionType: MicroBomb + totalIntensity: 120 + intensitySlope: 5 + maxIntensity: 30 + canCreateVacuum: false + - type: Tag + tags: + - SubdermalImplant + - HideContextMenu + - MicroBomb + + +- type: entity + parent: BaseSubdermalImplant + id: MacroBombImplant + name: macro bomb implant + description: a large explosion packed into a small implant, be warned as this could be dangerous for your teammates + noSpawn: true + components: + - type: SubdermalImplant + permanent: true + - type: TriggerOnMobstateChange #Chains with OnUseTimerTrigger + mobState: Dead + - type: OnUseTimerTrigger + delay: 5 + initialBeepDelay: 0 + beepSound: /Audio/Machines/Nuke/general_beep.ogg + - type: ExplodeOnTrigger + - type: GibOnTrigger + deleteItems: true + - type: Explosive + explosionType: Default + totalIntensity: 4000 + intensitySlope: 5 + maxIntensity: 50 + canCreateVacuum: true + - type: Tag + tags: + - SubdermalImplant + - HideContextMenu + - MacroBomb diff --git a/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml b/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml index c64b82fb72..9043ef148d 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/syndicate.yml @@ -70,9 +70,9 @@ key: enum.StoreUiKey.Key - type: Store preset: StorePresetUplink - balance: + balance: Telecrystal: 0 - + - type: entity parent: BaseUplinkRadio id: BaseUplinkRadio20TC @@ -82,6 +82,9 @@ preset: StorePresetUplink balance: Telecrystal: 20 + - type: Tag + tags: + - TraitorUplink - type: entity parent: BaseUplinkRadio @@ -92,6 +95,9 @@ preset: StorePresetUplink balance: Telecrystal: 25 + - type: Tag + tags: + - TraitorUplink #this uplink MUST be used for nukeops, as it has the tag for filtering the listing. - type: entity diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 5c0a40bab3..58a4d1a10b 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -185,6 +185,7 @@ - CryostasisBeaker - Dropper - Syringe + - Implanter - PillCanister - ChemistryEmptyBottle01 - Drone @@ -406,6 +407,7 @@ - CryostasisBeaker - Dropper - Syringe + - Implanter - PillCanister - BodyBag - ChemistryEmptyBottle01 diff --git a/Resources/Prototypes/Recipes/Lathes/medical.yml b/Resources/Prototypes/Recipes/Lathes/medical.yml index 43930f7cfd..1afc88821b 100644 --- a/Resources/Prototypes/Recipes/Lathes/medical.yml +++ b/Resources/Prototypes/Recipes/Lathes/medical.yml @@ -107,3 +107,12 @@ materials: Cloth: 20 Plastic: 20 + +- type: latheRecipe + id: Implanter + icon: Objects/Specific/Chemistry/syringe.rsi/syringe_base0.png + result: Implanter + completetime: 1 + materials: + Glass: 500 + Steel: 500 diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml index 4fe123a717..24d172c7d2 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml @@ -18,6 +18,8 @@ Piercing: 4 groups: Burn: 3 + - !type:AddImplantSpecial + implants: [ SadTromboneImplant ] - type: startingGear id: ClownGear diff --git a/Resources/Prototypes/SoundCollections/sadtrombone.yml b/Resources/Prototypes/SoundCollections/sadtrombone.yml new file mode 100644 index 0000000000..268bb2d0df --- /dev/null +++ b/Resources/Prototypes/SoundCollections/sadtrombone.yml @@ -0,0 +1,4 @@ +- type: soundCollection + id: SadTrombone + files: + - /Audio/Effects/sadtrombone.ogg diff --git a/Resources/Prototypes/explosion.yml b/Resources/Prototypes/explosion.yml index b49037341c..401ca37cee 100644 --- a/Resources/Prototypes/explosion.yml +++ b/Resources/Prototypes/explosion.yml @@ -29,6 +29,22 @@ texturePath: /Textures/Effects/fire.rsi fireStates: 3 +- type: explosion + id: MicroBomb + damagePerIntensity: + types: + Heat: 6 + Blunt: 6 + Piercing: 6 + Structural: 20 + tileBreakChance: [ 0, 0.5, 1 ] + tileBreakIntensity: [ 1, 10, 15 ] + tileBreakRerollReduction: 30 + intensityPerState: 20 + lightColor: Orange + texturePath: /Textures/Effects/fire.rsi + fireStates: 6 + - type: explosion id: Radioactive damagePerIntensity: @@ -39,7 +55,7 @@ Piercing: 3 lightColor: Green fireColor: Green - texturePath: /Textures/Effects/fire_greyscale.rsi + texturePath: /Textures/Effects/fire_greyscale.rsi fireStates: 3 - type: explosion @@ -47,10 +63,10 @@ damagePerIntensity: types: Cold: 5 - Blunt: 2 + Blunt: 2 tileBreakChance: [0] tileBreakIntensity: [0] lightColor: Blue fireColor: Blue - texturePath: /Textures/Effects/fire_greyscale.rsi + texturePath: /Textures/Effects/fire_greyscale.rsi fireStates: 3 diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index fb1bab7c11..eb4fdfd837 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -380,6 +380,9 @@ - type: Tag id: NoSpinOnThrow +- type: Tag + id: TraitorUplink + - type: Tag id: NoBlockAnchoring @@ -395,6 +398,12 @@ - type: Tag id: Payload # for grenade/bomb crafting +- type: Tag + id: MacroBomb + +- type: Tag + id: MicroBomb + - type: Tag id: PaintableAirlock @@ -582,3 +591,5 @@ - type: Tag id: Write +- type: Tag + id: SubdermalImplant diff --git a/Resources/Textures/Actions/Implants/implants.rsi/explosive.png b/Resources/Textures/Actions/Implants/implants.rsi/explosive.png new file mode 100644 index 0000000000..2ed42b3f13 Binary files /dev/null and b/Resources/Textures/Actions/Implants/implants.rsi/explosive.png differ diff --git a/Resources/Textures/Actions/Implants/implants.rsi/freedom.png b/Resources/Textures/Actions/Implants/implants.rsi/freedom.png new file mode 100644 index 0000000000..f4a9b2fc3c Binary files /dev/null and b/Resources/Textures/Actions/Implants/implants.rsi/freedom.png differ diff --git a/Resources/Textures/Actions/Implants/implants.rsi/meta.json b/Resources/Textures/Actions/Implants/implants.rsi/meta.json new file mode 100644 index 0000000000..a3e37f3298 --- /dev/null +++ b/Resources/Textures/Actions/Implants/implants.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Implant icons taken from Citadel Station at commit https://github.com/Citadel-Station-13/Citadel-Station-13/commit/a2f6a7c20763da3d2f3cfb982e9ccd7922df6162", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "freedom" + }, + { + "name": "explosive" + } + ] +}