From 6932f2819136e570e8e763f53c05d1920071d9fa Mon Sep 17 00:00:00 2001 From: Sir Warock <67167466+SirWarock@users.noreply.github.com> Date: Sun, 21 Dec 2025 00:58:26 +0100 Subject: [PATCH] Merge Injector & Hypospray Systems & Components (#41833) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * Merge Injector & Hyposprays * Fixes * Requested Changes * Preview * Inclusion of Prototypes * Fix * small oversight * Further fixes * A few more fixes & Bluespacesyringe buff Co-Authored-By: āda <177162775+iaada@users.noreply.github.com> * Final Commit, hopefully * Merge conflict no more * YML fix * Add required changes Co-Authored-By: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> * cleanup warnings removal * Bug fix & Maintainer Requests Co-Authored-By: āda <177162775+iaada@users.noreply.github.com> * Adhere to requested changes Co-Authored-By: āda <177162775+iaada@users.noreply.github.com> --------- Co-authored-by: āda <177162775+iaada@users.noreply.github.com> Co-authored-by: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../HyposprayStatusControlSystem.cs | 16 - .../InjectorStatusControlSystem.cs | 20 + .../Chemistry/EntitySystems/InjectorSystem.cs | 16 - .../Chemistry/UI/HyposprayStatusControl.cs | 58 -- .../Chemistry/UI/InjectorStatusControl.cs | 61 +- .../Chemistry/EntitySystems/InjectorSystem.cs | 6 - .../Components/HyposprayComponent.cs | 60 -- .../Chemistry/Components/InjectorComponent.cs | 104 +-- .../EntitySystems/HypospraySystem.cs | 329 -------- .../Chemistry/EntitySystems/InjectorSystem.cs | 762 ++++++++++++++++++ .../EntitySystems/SharedInjectorSystem.cs | 536 ------------ .../Chemistry/Events/HyposprayEvents.cs | 38 - .../Chemistry/Events/InjectorEvents.cs | 43 + .../Prototypes/InjectorModePrototype.cs | 145 ++++ Content.Shared/Clumsy/ClumsySystem.cs | 12 +- .../Fluids/SharedPuddleSystem.Spillable.cs | 19 +- .../Inventory/InventorySystem.Relay.cs | 7 +- .../components/hypospray-component.ftl | 20 - .../components/injector-component.ftl | 45 +- .../Prototypes/Chemistry/injector_modes.yml | 118 +++ .../Prototypes/Entities/Clothing/Belt/job.yml | 3 +- .../Objects/Specific/Medical/hypospray.yml | 113 +-- .../Entities/Objects/Specific/chemistry.yml | 48 +- Resources/Prototypes/tags.yml | 3 + 24 files changed, 1266 insertions(+), 1316 deletions(-) delete mode 100644 Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs create mode 100644 Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs delete mode 100644 Content.Client/Chemistry/EntitySystems/InjectorSystem.cs delete mode 100644 Content.Client/Chemistry/UI/HyposprayStatusControl.cs delete mode 100644 Content.Server/Chemistry/EntitySystems/InjectorSystem.cs delete mode 100644 Content.Shared/Chemistry/Components/HyposprayComponent.cs delete mode 100644 Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs create mode 100644 Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs delete mode 100644 Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs delete mode 100644 Content.Shared/Chemistry/Events/HyposprayEvents.cs create mode 100644 Content.Shared/Chemistry/Events/InjectorEvents.cs create mode 100644 Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs delete mode 100644 Resources/Locale/en-US/chemistry/components/hypospray-component.ftl create mode 100644 Resources/Prototypes/Chemistry/injector_modes.yml diff --git a/Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs b/Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs deleted file mode 100644 index 4dfc8506d2..0000000000 --- a/Content.Client/Chemistry/EntitySystems/HyposprayStatusControlSystem.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Client.Chemistry.UI; -using Content.Client.Items; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.EntitySystems; - -namespace Content.Client.Chemistry.EntitySystems; - -public sealed class HyposprayStatusControlSystem : EntitySystem -{ - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!; - public override void Initialize() - { - base.Initialize(); - Subs.ItemStatus(ent => new HyposprayStatusControl(ent, _solutionContainers)); - } -} diff --git a/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs b/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs new file mode 100644 index 0000000000..ca8685eb3e --- /dev/null +++ b/Content.Client/Chemistry/EntitySystems/InjectorStatusControlSystem.cs @@ -0,0 +1,20 @@ +using Content.Client.Chemistry.UI; +using Content.Client.Items; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Robust.Shared.Prototypes; + +namespace Content.Client.Chemistry.EntitySystems; + +public sealed class InjectorStatusControlSystem : EntitySystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + + public override void Initialize() + { + base.Initialize(); + Subs.ItemStatus(injector => new InjectorStatusControl(injector, _solutionContainers, _prototypeManager)); + } +} diff --git a/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs deleted file mode 100644 index 58cb5330a2..0000000000 --- a/Content.Client/Chemistry/EntitySystems/InjectorSystem.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Content.Client.Chemistry.UI; -using Content.Client.Items; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.EntitySystems; - -namespace Content.Client.Chemistry.EntitySystems; - -public sealed class InjectorSystem : SharedInjectorSystem -{ - public override void Initialize() - { - base.Initialize(); - - Subs.ItemStatus(ent => new InjectorStatusControl(ent, SolutionContainer)); - } -} diff --git a/Content.Client/Chemistry/UI/HyposprayStatusControl.cs b/Content.Client/Chemistry/UI/HyposprayStatusControl.cs deleted file mode 100644 index a564bcefc6..0000000000 --- a/Content.Client/Chemistry/UI/HyposprayStatusControl.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Content.Client.Message; -using Content.Client.Stylesheets; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.EntitySystems; -using Content.Shared.FixedPoint; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; -using Robust.Shared.Timing; - -namespace Content.Client.Chemistry.UI; - -public sealed class HyposprayStatusControl : Control -{ - private readonly Entity _parent; - private readonly RichTextLabel _label; - private readonly SharedSolutionContainerSystem _solutionContainers; - - private FixedPoint2 PrevVolume; - private FixedPoint2 PrevMaxVolume; - private bool PrevOnlyAffectsMobs; - - public HyposprayStatusControl(Entity parent, SharedSolutionContainerSystem solutionContainers) - { - _parent = parent; - _solutionContainers = solutionContainers; - _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } }; - AddChild(_label); - } - - protected override void FrameUpdate(FrameEventArgs args) - { - base.FrameUpdate(args); - - if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution)) - return; - - // only updates the UI if any of the details are different than they previously were - if (PrevVolume == solution.Volume - && PrevMaxVolume == solution.MaxVolume - && PrevOnlyAffectsMobs == _parent.Comp.OnlyAffectsMobs) - return; - - PrevVolume = solution.Volume; - PrevMaxVolume = solution.MaxVolume; - PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs; - - var modeStringLocalized = Loc.GetString((_parent.Comp.OnlyAffectsMobs && _parent.Comp.CanContainerDraw) switch - { - false => "hypospray-all-mode-text", - true => "hypospray-mobs-only-mode-text", - }); - - _label.SetMarkup(Loc.GetString("hypospray-volume-label", - ("currentVolume", solution.Volume), - ("totalVolume", solution.MaxVolume), - ("modeString", modeStringLocalized))); - } -} diff --git a/Content.Client/Chemistry/UI/InjectorStatusControl.cs b/Content.Client/Chemistry/UI/InjectorStatusControl.cs index 24f988bd35..0c57da7813 100644 --- a/Content.Client/Chemistry/UI/InjectorStatusControl.cs +++ b/Content.Client/Chemistry/UI/InjectorStatusControl.cs @@ -2,26 +2,32 @@ using Content.Client.Message; using Content.Client.Stylesheets; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Prototypes; using Content.Shared.FixedPoint; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Client.Chemistry.UI; public sealed class InjectorStatusControl : Control { + private readonly IPrototypeManager _prototypeManager; + private readonly Entity _parent; private readonly SharedSolutionContainerSystem _solutionContainers; private readonly RichTextLabel _label; - private FixedPoint2 PrevVolume; - private FixedPoint2 PrevMaxVolume; - private FixedPoint2 PrevTransferAmount; - private InjectorToggleMode PrevToggleState; + private FixedPoint2 _prevVolume; + private FixedPoint2 _prevMaxVolume; + private FixedPoint2? _prevTransferAmount; + private InjectorBehavior _prevBehavior; - public InjectorStatusControl(Entity parent, SharedSolutionContainerSystem solutionContainers) + public InjectorStatusControl(Entity parent, SharedSolutionContainerSystem solutionContainers, IPrototypeManager prototypeManager) { + _prototypeManager = prototypeManager; + _parent = parent; _solutionContainers = solutionContainers; _label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } }; @@ -32,33 +38,38 @@ public sealed class InjectorStatusControl : Control { base.FrameUpdate(args); - if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution)) + if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution) + || !_prototypeManager.Resolve(_parent.Comp.ActiveModeProtoId, out var activeMode)) return; // only updates the UI if any of the details are different than they previously were - if (PrevVolume == solution.Volume - && PrevMaxVolume == solution.MaxVolume - && PrevTransferAmount == _parent.Comp.CurrentTransferAmount - && PrevToggleState == _parent.Comp.ToggleState) + if (_prevVolume == solution.Volume + && _prevMaxVolume == solution.MaxVolume + && _prevTransferAmount == _parent.Comp.CurrentTransferAmount + && _prevBehavior == activeMode.Behavior) return; - PrevVolume = solution.Volume; - PrevMaxVolume = solution.MaxVolume; - PrevTransferAmount = _parent.Comp.CurrentTransferAmount; - PrevToggleState = _parent.Comp.ToggleState; + _prevVolume = solution.Volume; + _prevMaxVolume = solution.MaxVolume; + _prevTransferAmount = _parent.Comp.CurrentTransferAmount; + _prevBehavior = activeMode.Behavior; // Update current volume and injector state - var modeStringLocalized = Loc.GetString(_parent.Comp.ToggleState switch + // Seeing transfer volume is only important for injectors that can change it. + if (activeMode.TransferAmounts.Count > 1 && _parent.Comp.CurrentTransferAmount.HasValue) { - InjectorToggleMode.Draw => "injector-draw-text", - InjectorToggleMode.Inject => "injector-inject-text", - _ => "injector-invalid-injector-toggle-mode" - }); - - _label.SetMarkup(Loc.GetString("injector-volume-label", - ("currentVolume", solution.Volume), - ("totalVolume", solution.MaxVolume), - ("modeString", modeStringLocalized), - ("transferVolume", _parent.Comp.CurrentTransferAmount))); + _label.SetMarkup(Loc.GetString("injector-volume-transfer-label", + ("currentVolume", solution.Volume), + ("totalVolume", solution.MaxVolume), + ("modeString", Loc.GetString(activeMode.Name)), + ("transferVolume", _parent.Comp.CurrentTransferAmount.Value))); + } + else + { + _label.SetMarkup(Loc.GetString("injector-volume-label", + ("currentVolume", solution.Volume), + ("totalVolume", solution.MaxVolume), + ("modeString", Loc.GetString(activeMode.Name)))); + } } } diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs deleted file mode 100644 index 6088d01c59..0000000000 --- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs +++ /dev/null @@ -1,6 +0,0 @@ - -using Content.Shared.Chemistry.EntitySystems; - -namespace Content.Server.Chemistry.EntitySystems; - -public sealed class InjectorSystem : SharedInjectorSystem; diff --git a/Content.Shared/Chemistry/Components/HyposprayComponent.cs b/Content.Shared/Chemistry/Components/HyposprayComponent.cs deleted file mode 100644 index e1e4f21101..0000000000 --- a/Content.Shared/Chemistry/Components/HyposprayComponent.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Content.Shared.FixedPoint; -using Robust.Shared.GameStates; -using Robust.Shared.Audio; - -namespace Content.Shared.Chemistry.Components; - -/// -/// Component that allows an entity instantly transfer liquids by interacting with objects that have solutions. -/// -[RegisterComponent, NetworkedComponent] -[AutoGenerateComponentState] -public sealed partial class HyposprayComponent : Component -{ - /// - /// Solution that will be used by hypospray for injections. - /// - [DataField] - public string SolutionName = "hypospray"; - - /// - /// Amount of the units that will be transfered. - /// - [AutoNetworkedField] - [DataField] - public FixedPoint2 TransferAmount = FixedPoint2.New(5); - - /// - /// The delay to draw reagents using the hypospray. - /// If set, RefillTime should probably have the same value. - /// - [DataField] - public float DrawTime = 0f; - - /// - /// Sound that will be played when injecting. - /// - [DataField] - public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Items/hypospray.ogg"); - - /// - /// Decides whether you can inject everything or just mobs. - /// - [AutoNetworkedField] - [DataField(required: true)] - public bool OnlyAffectsMobs = false; - - /// - /// If this can draw from containers in mob-only mode. - /// - [AutoNetworkedField] - [DataField] - public bool CanContainerDraw = true; - - /// - /// Whether or not the hypospray is able to draw from containers or if it's a single use - /// device that can only inject. - /// - [DataField] - public bool InjectOnly = false; -} diff --git a/Content.Shared/Chemistry/Components/InjectorComponent.cs b/Content.Shared/Chemistry/Components/InjectorComponent.cs index d3a0503c3c..e31d8d3503 100644 --- a/Content.Shared/Chemistry/Components/InjectorComponent.cs +++ b/Content.Shared/Chemistry/Components/InjectorComponent.cs @@ -1,10 +1,10 @@ using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Prototypes; using Content.Shared.Chemistry.Reagent; using Content.Shared.DoAfter; using Content.Shared.FixedPoint; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization; namespace Content.Shared.Chemistry.Components; @@ -14,11 +14,10 @@ namespace Content.Shared.Chemistry.Components; /// /// Can optionally support both /// injection and drawing or just injection. Can inject/draw reagents from solution -/// containers, and can directly inject into a mobs bloodstream. +/// containers, and can directly inject into a mob's bloodstream. /// -/// -/// -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(InjectorSystem))] public sealed partial class InjectorComponent : Component { /// @@ -34,66 +33,44 @@ public sealed partial class InjectorComponent : Component public Entity? Solution = null; /// - /// Whether or not the injector is able to draw from containers or if it's a single use - /// device that can only inject. - /// - [DataField] - public bool InjectOnly; - - /// - /// Whether or not the injector is able to draw from or inject from mobs. + /// Amount to inject or draw on each usage. /// /// - /// For example: droppers would ignore mobs. + /// If its set null, this injector is marked to inject its entire contents upon usage. /// - [DataField] - public bool IgnoreMobs; + [DataField, AutoNetworkedField] + public FixedPoint2? CurrentTransferAmount = FixedPoint2.New(5); - /// - /// Whether or not the injector is able to draw from or inject into containers that are closed/sealed. - /// - /// - /// For example: droppers can not inject into cans, but syringes can. - /// - [DataField] - public bool IgnoreClosed = true; /// - /// The transfer amounts for the set-transfer verb. + /// The mode that this injector starts with on MapInit. /// - [DataField] - public List TransferAmounts = new() { 1, 5, 10, 15 }; + [DataField(required: true), AutoNetworkedField] + public ProtoId ActiveModeProtoId; /// - /// Amount to inject or draw on each usage. If the injector is inject only, it will - /// attempt to inject it's entire contents upon use. + /// The possible that it can switch between. /// - [DataField, AutoNetworkedField] - public FixedPoint2 CurrentTransferAmount = FixedPoint2.New(5); + [DataField(required: true)] + public List> AllowedModes; /// - /// Injection delay (seconds) when the target is a mob. + /// Whether the injector is able to draw from or inject from mobs. /// - /// - /// The base delay has a minimum of 1 second, but this will still be modified if the target is incapacitated or - /// in combat mode. - /// + /// + /// Droppers ignore mobs. + /// [DataField] - public TimeSpan Delay = TimeSpan.FromSeconds(5); + public bool IgnoreMobs; /// - /// Each additional 1u after first 5u increases the delay by X seconds. + /// Whether the injector is able to draw from or inject into containers that are closed/sealed. /// + /// + /// Droppers can't inject into closed cans. + /// [DataField] - public TimeSpan DelayPerVolume = TimeSpan.FromSeconds(0.1); - - /// - /// The state of the injector. Determines it's attack behavior. Containers must have the - /// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should - /// only ever be set to Inject - /// - [DataField, AutoNetworkedField] - public InjectorToggleMode ToggleState = InjectorToggleMode.Draw; + public bool IgnoreClosed = true; /// /// Reagents that are allowed to be within this injector. @@ -101,44 +78,29 @@ public sealed partial class InjectorComponent : Component /// A null ReagentWhitelist indicates all reagents are allowed. /// [DataField] - public List>? ReagentWhitelist = null; + public List>? ReagentWhitelist; #region Arguments for injection doafter - /// + /// [DataField] public bool NeedHand = true; - /// + /// [DataField] public bool BreakOnHandChange = true; - /// + /// [DataField] public float MovementThreshold = 0.1f; #endregion } -/// -/// Possible modes for an . -/// -[Serializable, NetSerializable] -public enum InjectorToggleMode : byte +internal static class InjectorToggleModeExtensions { - /// - /// The injector will try to inject reagent into things. - /// - Inject, - - /// - /// The injector will try to draw reagent from things. - /// - Draw, + public static bool HasAnyFlag(this InjectorBehavior s1, InjectorBehavior s2) + { + return (s1 & s2) != 0; + } } - -/// -/// Raised on the injector when the doafter has finished. -/// -[Serializable, NetSerializable] -public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs deleted file mode 100644 index e179fb5f43..0000000000 --- a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Shared.Administration.Logs; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.Chemistry.Hypospray.Events; -using Content.Shared.Database; -using Content.Shared.DoAfter; -using Content.Shared.FixedPoint; -using Content.Shared.Forensics; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction.Events; -using Content.Shared.Interaction; -using Content.Shared.Mobs.Components; -using Content.Shared.Popups; -using Content.Shared.Timing; -using Content.Shared.Verbs; -using Content.Shared.Weapons.Melee.Events; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Serialization; - -namespace Content.Shared.Chemistry.EntitySystems; - -public sealed class HypospraySystem : EntitySystem -{ - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!; - [Dependency] private readonly UseDelaySystem _useDelay = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnAttack); - SubscribeLocalEvent(OnUseInHand); - SubscribeLocalEvent>(AddToggleModeVerb); - SubscribeLocalEvent(OnDrawDoAfter); - } - - #region Ref events - private void OnUseInHand(Entity entity, ref UseInHandEvent args) - { - if (args.Handled) - return; - - args.Handled = TryDoInject(entity, args.User, args.User); - } - - private void OnAfterInteract(Entity entity, ref AfterInteractEvent args) - { - if (args.Handled || !args.CanReach || args.Target == null) - return; - - args.Handled = TryUseHypospray(entity, args.Target.Value, args.User); - } - - private void OnAttack(Entity entity, ref MeleeHitEvent args) - { - if (args.HitEntities is []) - return; - - TryDoInject(entity, args.HitEntities[0], args.User); - } - - private void OnDrawDoAfter(Entity entity, ref HyposprayDrawDoAfterEvent args) - { - if (args.Cancelled) - return; - - if (entity.Comp.CanContainerDraw - && args.Target.HasValue - && !EligibleEntity(args.Target.Value, entity) - && _solutionContainers.TryGetDrawableSolution(args.Target.Value, out var drawableSolution, out _)) - { - TryDraw(entity, args.Target.Value, drawableSolution.Value, args.User); - } - } - - #endregion - - #region Draw/Inject - private bool TryUseHypospray(Entity entity, EntityUid target, EntityUid user) - { - // if target is ineligible but is a container, try to draw from the container if allowed - if (entity.Comp.CanContainerDraw - && !EligibleEntity(target, entity) - && _solutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _)) - { - return TryStartDraw(entity, target, drawableSolution.Value, user); - } - - return TryDoInject(entity, target, user); - } - - public bool TryDoInject(Entity entity, EntityUid target, EntityUid user) - { - var (uid, component) = entity; - - if (!EligibleEntity(target, component)) - return false; - - if (TryComp(uid, out UseDelayComponent? delayComp)) - { - if (_useDelay.IsDelayed((uid, delayComp))) - return false; - } - - string? msgFormat = null; - - // Self event - var selfEvent = new SelfBeforeHyposprayInjectsEvent(user, entity.Owner, target); - RaiseLocalEvent(user, selfEvent); - - if (selfEvent.Cancelled) - { - _popup.PopupClient(Loc.GetString(selfEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user); - return false; - } - - target = selfEvent.TargetGettingInjected; - - if (!EligibleEntity(target, component)) - return false; - - // Target event - var targetEvent = new TargetBeforeHyposprayInjectsEvent(user, entity.Owner, target); - RaiseLocalEvent(target, targetEvent); - - if (targetEvent.Cancelled) - { - _popup.PopupClient(Loc.GetString(targetEvent.InjectMessageOverride ?? "hypospray-cant-inject", ("owner", Identity.Entity(target, EntityManager))), target, user); - return false; - } - - target = targetEvent.TargetGettingInjected; - - if (!EligibleEntity(target, component)) - return false; - - // The target event gets priority for the overriden message. - if (targetEvent.InjectMessageOverride != null) - msgFormat = targetEvent.InjectMessageOverride; - else if (selfEvent.InjectMessageOverride != null) - msgFormat = selfEvent.InjectMessageOverride; - else if (target == user) - msgFormat = "hypospray-component-inject-self-message"; - - if (!_solutionContainers.TryGetSolution(uid, component.SolutionName, out var hypoSpraySoln, out var hypoSpraySolution) || hypoSpraySolution.Volume == 0) - { - _popup.PopupClient(Loc.GetString("hypospray-component-empty-message"), target, user); - return true; - } - - if (!_solutionContainers.TryGetInjectableSolution(target, out var targetSoln, out var targetSolution)) - { - _popup.PopupClient(Loc.GetString("hypospray-cant-inject", ("target", Identity.Entity(target, EntityManager))), target, user); - return false; - } - - _popup.PopupClient(Loc.GetString(msgFormat ?? "hypospray-component-inject-other-message", ("other", Identity.Entity(target, EntityManager))), target, user); - - if (target != user) - { - _popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target); - // TODO: This should just be using melee attacks... - // meleeSys.SendLunge(angle, user); - } - - _audio.PlayPredicted(component.InjectSound, target, user); - - // Medipens and such use this system and don't have a delay, requiring extra checks - // BeginDelay function returns if item is already on delay - if (delayComp != null) - _useDelay.TryResetDelay((uid, delayComp)); - - // Get transfer amount. May be smaller than component.TransferAmount if not enough room - var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetSolution.AvailableVolume); - - if (realTransferAmount <= 0) - { - _popup.PopupClient(Loc.GetString("hypospray-component-transfer-already-full-message", ("owner", target)), target, user); - return true; - } - - // Move units from attackSolution to targetSolution - var removedSolution = _solutionContainers.SplitSolution(hypoSpraySoln.Value, realTransferAmount); - - if (!targetSolution.CanAddSolution(removedSolution)) - return true; - _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); - _solutionContainers.TryAddSolution(targetSoln.Value, removedSolution); - - var ev = new TransferDnaEvent { Donor = target, Recipient = uid }; - RaiseLocalEvent(target, ref ev); - - // same LogType as syringes... - _adminLogger.Add(LogType.ForceFeed, $"{ToPrettyString(user):user} injected {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {ToPrettyString(uid):using}"); - - return true; - } - - public bool TryStartDraw(Entity entity, EntityUid target, Entity targetSolution, EntityUid user) - { - if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln)) - return false; - - if (!TryGetDrawAmount(entity, target, targetSolution, user, soln.Value, out _)) - return false; - - var doAfterArgs = new DoAfterArgs(EntityManager, user, entity.Comp.DrawTime, new HyposprayDrawDoAfterEvent(), entity, target) - { - BreakOnDamage = true, - BreakOnMove = true, - NeedHand = true, - Hidden = true, - }; - - return _doAfter.TryStartDoAfter(doAfterArgs, out _); - } - - private bool TryGetDrawAmount(Entity entity, EntityUid target, Entity targetSolution, EntityUid user, Entity solutionEntity, [NotNullWhen(true)] out FixedPoint2? amount) - { - amount = null; - - if (solutionEntity.Comp.Solution.AvailableVolume == 0) - { - return false; - } - - // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector - var realTransferAmount = FixedPoint2.Min(entity.Comp.TransferAmount, targetSolution.Comp.Solution.Volume, - solutionEntity.Comp.Solution.AvailableVolume); - - if (realTransferAmount <= 0) - { - _popup.PopupClient( - Loc.GetString("injector-component-target-is-empty-message", - ("target", Identity.Entity(target, EntityManager))), - entity.Owner, user); - return false; - } - - amount = realTransferAmount; - return true; - } - - private bool TryDraw(Entity entity, EntityUid target, Entity targetSolution, EntityUid user) - { - if (!_solutionContainers.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln)) - return false; - - if (!TryGetDrawAmount(entity, target, targetSolution, user, soln.Value, out var amount)) - return false; - - var removedSolution = _solutionContainers.Draw(target, targetSolution, amount.Value); - - if (!_solutionContainers.TryAddSolution(soln.Value, removedSolution)) - { - return false; - } - - _popup.PopupClient(Loc.GetString("injector-component-draw-success-message", - ("amount", removedSolution.Volume), - ("target", Identity.Entity(target, EntityManager))), entity.Owner, user); - return true; - } - - private bool EligibleEntity(EntityUid entity, HyposprayComponent component) - { - // TODO: Does checking for BodyComponent make sense as a "can be hypospray'd" tag? - // In SS13 the hypospray ONLY works on mobs, NOT beakers or anything else. - // But this is 14, we dont do what SS13 does just because SS13 does it. - return component.OnlyAffectsMobs - ? HasComp(entity) && - HasComp(entity) - : HasComp(entity); - } - - #endregion - - #region Verbs - - // - // Uses the OnlyMobs field as a check to implement the ability - // to draw from jugs and containers with the hypospray - // Toggleable to allow people to inject containers if they prefer it over drawing - // - private void AddToggleModeVerb(Entity entity, ref GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || args.Hands == null || entity.Comp.InjectOnly) - return; - - var user = args.User; - var verb = new AlternativeVerb - { - Text = Loc.GetString("hypospray-verb-mode-label"), - Act = () => - { - ToggleMode(entity, user); - } - }; - args.Verbs.Add(verb); - } - - private void ToggleMode(Entity entity, EntityUid user) - { - SetMode(entity, !entity.Comp.OnlyAffectsMobs); - var msg = (entity.Comp.OnlyAffectsMobs && entity.Comp.CanContainerDraw) ? "hypospray-verb-mode-inject-mobs-only" : "hypospray-verb-mode-inject-all"; - _popup.PopupClient(Loc.GetString(msg), entity, user); - } - - public void SetMode(Entity entity, bool onlyAffectsMobs) - { - if (entity.Comp.OnlyAffectsMobs == onlyAffectsMobs) - return; - - entity.Comp.OnlyAffectsMobs = onlyAffectsMobs; - Dirty(entity); - } - - #endregion -} - -[Serializable, NetSerializable] -public sealed partial class HyposprayDrawDoAfterEvent : SimpleDoAfterEvent {} diff --git a/Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs new file mode 100644 index 0000000000..438da65293 --- /dev/null +++ b/Content.Shared/Chemistry/EntitySystems/InjectorSystem.cs @@ -0,0 +1,762 @@ +using System.Linq; +using Content.Shared.Administration.Logs; +using Content.Shared.Body.Components; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.Events; +using Content.Shared.Chemistry.Prototypes; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.FixedPoint; +using Content.Shared.Forensics.Systems; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Popups; +using Content.Shared.Stacks; +using Content.Shared.Standing; +using Content.Shared.Timing; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Melee.Events; +using JetBrains.Annotations; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Chemistry.EntitySystems; + +/// +/// This handles toggling injection modes, injections and drawings for all kinds of injectors. +/// +/// +/// +public sealed partial class InjectorSystem : EntitySystem +{ + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedForensicsSystem _forensics = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly OpenableSystem _openable = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly StandingStateSystem _standingState = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnInjectorUse); + SubscribeLocalEvent(OnInjectorAfterInteract); + SubscribeLocalEvent(OnInjectDoAfter); + SubscribeLocalEvent(OnAttack); + SubscribeLocalEvent>(AddVerbs); + } + + #region Events Handling + private void OnInjectorUse(Entity injector, ref UseInHandEvent args) + { + if (args.Handled + || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeProto)) + return; + + if (activeProto.InjectOnUse) // Injectors that can't toggle transferAmounts will be used. + TryMobsDoAfter(injector, args.User, args.User); + else // Syringes toggle Draw/Inject. + ToggleMode(injector, args.User); + + args.Handled = true; + } + + private void OnInjectorAfterInteract(Entity injector, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target is not { Valid: true } target) + return; + + // Is the target a mob? If yes, use a do-after to give them time to respond. + if (HasComp(target)) + { + // Are use using an injector capable of targeting a mob? + if (injector.Comp.IgnoreMobs) + { + _popup.PopupClient(Loc.GetString("injector-component-ignore-mobs"), args.Target.Value, args.User); + return; + } + + args.Handled = TryMobsDoAfter(injector, args.User, target); + return; + } + + // Draw from or inject into jugs, bottles, etc. + args.Handled = ContainerDoAfter(injector, args.User, target); + } + + private void OnInjectDoAfter(Entity injector, ref InjectorDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Target == null) + return; + + args.Handled = TryUseInjector(injector, args.Args.User, args.Args.Target.Value); + } + + private void OnAttack(Entity injector, ref MeleeHitEvent args) + { + if (args.HitEntities is []) + return; + + TryMobsDoAfter(injector, args.User, args.HitEntities[0]); + } + + /// + /// Give the user interaction verbs for their injector. + /// + /// + /// + /// + /// If they have multiple transferAmounts, they'll be able to switch between them via the verbs. + /// If they have multiple injector modes and don't toggle when used in hand, they can toggle the mode with the verbs too. + /// + private void AddVerbs(Entity injector, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands == null + || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return; + + var user = args.User; + var min = activeMode.TransferAmounts.Min(); + var max = activeMode.TransferAmounts.Max(); + var cur = injector.Comp.CurrentTransferAmount; + var toggleAmount = cur == max ? min : max; + + var priority = 0; + + if (activeMode.TransferAmounts.Count > 1) + { + AlternativeVerb toggleVerb = new() + { + Text = Loc.GetString("comp-solution-transfer-verb-toggle", ("amount", toggleAmount)), + Category = VerbCategory.SetTransferAmount, + Act = () => + { + injector.Comp.CurrentTransferAmount = toggleAmount; + _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user); + Dirty(injector); + }, + + Priority = priority + }; + args.Verbs.Add(toggleVerb); + + priority -= 1; + + // Add specific transfer verbs for amounts defined in the component + foreach (var amount in activeMode.TransferAmounts) + { + AlternativeVerb verb = new() + { + Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)), + Category = VerbCategory.SetTransferAmount, + Act = () => + { + injector.Comp.CurrentTransferAmount = amount; + _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user); + Dirty(injector); + }, + + // we want to sort by size, not alphabetically by the verb text. + Priority = priority + }; + args.Verbs.Add(verb); + } + } + + // If the injector cannot toggle via using in hand, allow toggling via verb. + if (!activeMode.InjectOnUse || injector.Comp.AllowedModes.Count <= 1) + return; + + var toggleModeVerb = new AlternativeVerb + { + Text = Loc.GetString("injector-toggle-verb-text"), + Act = () => + { + ToggleMode(injector, user); + }, + Priority = priority, + }; + + args.Verbs.Add(toggleModeVerb); + } + #endregion Events Handling + + #region Mob Interaction + /// + /// Send informative pop-up messages and wait for a do-after to complete. + /// + private bool TryMobsDoAfter(Entity injector, EntityUid user, EntityUid target) + { + if (_useDelay.IsDelayed(injector.Owner) // Check for Delay. + || !GetMobsDoAfterTime(injector, user, target, out var doAfterTime, out var amount)) // Get the DoAfter time. + return false; + + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, doAfterTime, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner) + { + BreakOnMove = true, + BreakOnWeightlessMove = false, + BreakOnDamage = true, + NeedHand = injector.Comp.NeedHand, + BreakOnHandChange = injector.Comp.BreakOnHandChange, + MovementThreshold = injector.Comp.MovementThreshold, + }); + + // If the DoAfter was instant, don't send popups and logs indicating an attempt. + if (doAfterTime == TimeSpan.Zero) + return true; + + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var injectorSolution) + || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + // Create a pop-up for the user. + _popup.PopupClient(Loc.GetString(activeMode.PopupUserAttempt), target, user); + + if (user == target) + { + if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw)) + { + _adminLogger.Add(LogType.ForceFeed, + $"{ToPrettyString(user):user} is attempting to draw {amount} units from themselves."); + } + else + { + _adminLogger.Add(LogType.Ingestion, + $"{ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(injectorSolution):solution}."); + } + } + else + { + // Create a popup to the target. + var userName = Identity.Entity(user, EntityManager); + var popup = Loc.GetString(activeMode.PopupTargetAttempt, ("user", userName)); + _popup.PopupEntity(popup, user, target); + + if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw)) + { + _adminLogger.Add(LogType.ForceFeed, + $"{ToPrettyString(user):user} is attempting to draw {amount} units from {ToPrettyString(target):target}"); + } + else + { + _adminLogger.Add(LogType.ForceFeed, + $"{ToPrettyString(user):user} is attempting to inject {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(injectorSolution):solution}"); + } + } + + return true; + } + + /// + /// Get the DoAfter Time for Mobs. + /// + /// The injector that is interacting with the mob. + /// The user using the injector. + /// The target mob. + /// The duration of the resulting doAfter. + /// The amount of the reagents transferred. + /// True if calculating the time was successful, false if not. + private bool GetMobsDoAfterTime(Entity injector, EntityUid user, EntityUid target, out TimeSpan doAfterTime, out FixedPoint2 amount) + { + doAfterTime = TimeSpan.Zero; + amount = FixedPoint2.Zero; + + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var injectorSolution) + || !_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + doAfterTime = activeMode.MobTime; + + // Can only draw blood with a draw mode and a transferAmount. + if (activeMode.Behavior.HasFlag(InjectorBehavior.Draw) && injector.Comp.CurrentTransferAmount != null) + { + // additional delay is based on actual volume left to draw in syringe when smaller than transfer amount + amount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount.Value, injectorSolution.AvailableVolume); + } + else + { + // additional delay is based on actual volume left to inject in syringe when smaller than transfer amount + // If CurrentTransferAmount is null, it'll want to inject its entire contents, e.g., epipens. + amount = injector.Comp.CurrentTransferAmount ?? injectorSolution.Volume; + amount = FixedPoint2.Min(amount, injectorSolution.Volume); + } + + // Transfers over the IgnoreDelayForVolume amount take Xu times DelayPerVolume longer. + doAfterTime += activeMode.DelayPerVolume * FixedPoint2.Max(0, amount - activeMode.IgnoreDelayForVolume).Double(); + + // Check if the target is either the user or downed. + if (user == target) // Self-injections take priority. + doAfterTime *= activeMode.SelfModifier; + // Technically, both can be true, but that is probably a balance nightmare. + else if (_standingState.IsDown(target)) + doAfterTime *= activeMode.DownedModifier; + + return true; + } + #endregion Mob Interaction + + #region Container Interaction + private bool ContainerDoAfter(Entity injector, EntityUid user, EntityUid target) + { + if (!GetContainerDoAfterTime(injector, user, target, out var doAfterTime)) + return false; + + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, doAfterTime, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner) + { + BreakOnMove = true, + BreakOnWeightlessMove = false, + BreakOnDamage = true, + NeedHand = injector.Comp.NeedHand, + BreakOnHandChange = injector.Comp.BreakOnHandChange, + MovementThreshold = injector.Comp.MovementThreshold, + }); + + return true; + } + + /// + /// Get the DoAfter Time for Containers and check if it is possible. + /// + /// The injector that is interacting with the container. + /// The user using the injector. + /// The target container, + /// The duration of the resulting DoAfter. + /// True if calculating the time was successful, false if not. + private bool GetContainerDoAfterTime(Entity injector, EntityUid user, EntityUid target, out TimeSpan doAfterTime) + { + doAfterTime = TimeSpan.Zero; + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + // Check if the Injector has a draw time, but only when drawing. + if (!activeMode.Behavior.HasAnyFlag(InjectorBehavior.Draw | InjectorBehavior.Dynamic)) + return true; + + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) + || solution.AvailableVolume == 0) + { + _popup.PopupClient(Loc.GetString("injector-component-cannot-toggle-draw-message"), user, user); + return false; // If already full, fail drawing. + } + + if (!_solutionContainer.TryGetDrawableSolution(target, out _, out var drawableSol)) + { + _popup.PopupClient(Loc.GetString("injector-component-cannot-transfer-message", ("target", Identity.Entity(target, EntityManager))), injector, user); + return false; + } + + if (drawableSol.Volume == 0) + { + _popup.PopupClient(Loc.GetString("injector-component-target-is-empty-message", ("target", Identity.Entity(target, EntityManager))), injector, user); + return false; + } + + doAfterTime = activeMode.ContainerDrawTime; + return true; + } + #endregion Container Interaction + + #region Injecting/Drawing + /// + /// Depending on the , this will deal with the result of the DoAfter and draw/inject accordingly. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + /// True if the injection/drawing was successful, false if not. + /// The injector has a different . + private bool TryUseInjector(Entity injector, EntityUid user, EntityUid target) + { + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target); + + LocId msg = target == user ? "injector-component-cannot-transfer-message-self" : "injector-component-cannot-transfer-message"; + + switch (activeMode.Behavior) + { + // Handle injecting/drawing for solutions + case InjectorBehavior.Inject: + { + if (isOpenOrIgnored && _solutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _)) + return TryInject(injector, user, target, injectableSolution.Value, false); + + if (isOpenOrIgnored && _solutionContainer.TryGetRefillableSolution(target, out var refillableSolution, out _)) + return TryInject(injector, user, target, refillableSolution.Value, true); + break; + } + case InjectorBehavior.Draw: + { + // Draw from a bloodstream if the target has that + if (TryComp(target, out var stream) && + _solutionContainer.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution)) + { + return TryDraw(injector, user, (target, stream), stream.BloodSolution.Value); + } + + // Draw from an object (food, beaker, etc) + if (isOpenOrIgnored && _solutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _)) + return TryDraw(injector, user, target, drawableSolution.Value); + + msg = target == user ? "injector-component-cannot-draw-message-self" : "injector-component-cannot-draw-message"; + _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user); + break; + } + case InjectorBehavior.Dynamic: + { + // If it's a mob, inject. We're using injectableSolution so I don't have to code a sole method for injecting into bloodstreams. + if (HasComp(target) + && _solutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _)) + { + return TryInject(injector, user, target, injectableSolution.Value, false); + } + + // Draw from an object (food, beaker, etc.) + if (isOpenOrIgnored && _solutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _)) + return TryDraw(injector, user, target, drawableSolution.Value); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user); + return false; + } + + /// + /// Attempt to inject the solution of the injector into the target. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + /// The solution of the target. + /// Whether or not the solution is refillable or injectable. + /// True if the injection was successful, false if not. + private bool TryInject(Entity injector, EntityUid user, EntityUid target, Entity targetSolution, bool asRefill) + { + if (!_solutionContainer.ResolveSolution(injector.Owner, + injector.Comp.SolutionName, + ref injector.Comp.Solution, + out var injectorSolution) || injectorSolution.Volume == 0) + { + // If empty, show a popup. + _popup.PopupClient(Loc.GetString("injector-component-empty-message", ("injector", injector)), user, user); + return false; + } + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode)) + return false; + + var selfEv = new SelfBeforeInjectEvent(user, injector, target); + RaiseLocalEvent(user, selfEv); + + if (selfEv.Cancelled) + { + // Clowns will now also fumble Syringes. + if (selfEv.OverrideMessage != null) + _popup.PopupPredicted(selfEv.OverrideMessage, user, user); + return true; + } + + target = selfEv.TargetGettingInjected; + + var ev = new TargetBeforeInjectEvent(user, injector, target); + RaiseLocalEvent(target, ref ev); + + // Jugsuit blocking Hyposprays when + if (ev.Cancelled) + { + var userMessage = Loc.GetString("injector-component-blocked-user"); + var otherMessage = Loc.GetString("injector-component-blocked-other", ("target", target), ("user", user)); + _popup.PopupPredicted(userMessage, otherMessage, target, user, PopupType.SmallCaution); + return true; + } + + // Get transfer amount. It may be smaller than _transferAmount if not enough room + var plannedTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount ?? injectorSolution.Volume, injectorSolution.Volume); + var realTransferAmount = FixedPoint2.Min(plannedTransferAmount, targetSolution.Comp.Solution.AvailableVolume); + + if (realTransferAmount <= 0) + { + LocId msg = target == user ? "injector-component-target-already-full-message-self" : "injector-component-target-already-full-message"; + _popup.PopupClient( + Loc.GetString(msg, + ("target", Identity.Entity(target, EntityManager))), + injector.Owner, + user); + return false; + } + + // Move units from attackSolution to targetSolution + Solution removedSolution; + if (TryComp(target, out var stack)) + removedSolution = _solutionContainer.SplitStackSolution(injector.Comp.Solution.Value, realTransferAmount, stack.Count); + else + removedSolution = _solutionContainer.SplitSolution(injector.Comp.Solution.Value, realTransferAmount); + + _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); + + if (!asRefill) + _solutionContainer.Inject(target, targetSolution, removedSolution); + else + _solutionContainer.Refill(target, targetSolution, removedSolution); + + LocId msgSuccess = target == user ? "injector-component-transfer-success-message-self" : "injector-component-transfer-success-message"; + + if (selfEv.OverrideMessage != null) + msgSuccess = selfEv.OverrideMessage; + else if (ev.OverrideMessage != null) + msgSuccess = ev.OverrideMessage; + + _popup.PopupClient(Loc.GetString(msgSuccess, ("amount", removedSolution.Volume), ("target", Identity.Entity(target, EntityManager))), target, user); + + // it is IMPERATIVE that when an injector is instant, that it has a pop-up. + if (activeMode.InjectPopupTarget != null && target != user) + _popup.PopupClient(Loc.GetString(activeMode.InjectPopupTarget), target, target); + + // Some injectors like hyposprays have sound, some like syringes have not. + if (activeMode.InjectSound != null) + _audio.PlayPredicted(activeMode.InjectSound, injector, user); + + // Log what happened. + _adminLogger.Add(LogType.ForceFeed, $"{ToPrettyString(user):user} injected {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(removedSolution):removedSolution} using a {ToPrettyString(injector):using}"); + + AfterInject(injector, user, target); + return true; + } + + /// + /// Attempt to draw reagents from a container. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + /// The solution of the target. + /// True if the drawing was successful, false if not. + private bool TryDraw(Entity injector, EntityUid user, Entity target, Entity targetSolution) + { + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) || solution.AvailableVolume == 0) + { + _popup.PopupClient("injector-component-cannot-toggle-draw-message", user, user); + return false; + } + + var applicableTargetSolution = targetSolution.Comp.Solution; + // If a whitelist exists, remove all non-whitelisted reagents from the target solution temporarily + var temporarilyRemovedSolution = new Solution(); + if (injector.Comp.ReagentWhitelist is { } reagentWhitelist) + { + temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentWhitelist.ToArray()); + } + + // If transferAmount is null, fallback to 5 units. + var plannedTransferAmount = injector.Comp.CurrentTransferAmount ?? FixedPoint2.New(5); + // Get transfer amount. It may be smaller than _transferAmount if not enough room, also make sure there's room in the injector + var realTransferAmount = FixedPoint2.Min(plannedTransferAmount, + applicableTargetSolution.Volume, + solution.AvailableVolume); + + if (realTransferAmount <= 0) + { + LocId msg = target.Owner == user ? "injector-component-target-is-empty-message-self" : "injector-component-target-is-empty-message"; + var targetIdentity = Identity.Entity(target, EntityManager); + _popup.PopupClient(Loc.GetString(msg, ("target", targetIdentity)), injector.Owner, user); + return false; + } + + // We have some snowflaked behavior for streams. + if (target.Comp != null) + { + DrawFromBlood(injector, user, (target.Owner, target.Comp), injector.Comp.Solution.Value, realTransferAmount); + return true; + } + + // Move units from attackSolution to targetSolution + var removedSolution = _solutionContainer.Draw(target.Owner, targetSolution, realTransferAmount); + + // Add back non-whitelisted reagents to the target solution + _solutionContainer.TryAddSolution(targetSolution, temporarilyRemovedSolution); + + if (!_solutionContainer.TryAddSolution(injector.Comp.Solution.Value, removedSolution)) + { + return false; + } + + LocId msgSuccess = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message"; + var targetIdentitySuccess = Identity.Entity(target, EntityManager); + _popup.PopupClient( + Loc.GetString(msgSuccess, ("amount", removedSolution.Volume), ("target", targetIdentitySuccess)), + target, + user); + + AfterDraw(injector, user, target); + return true; + } + + /// + /// Attempt to draw blood from a mob. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + /// The solution of the injector. + /// The amount of blood to draw. + private void DrawFromBlood(Entity injector, + EntityUid user, + Entity target, + Entity injectorSolution, + FixedPoint2 transferAmount) + { + if (_solutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName, ref target.Comp.BloodSolution)) + { + var bloodTemp = _solutionContainer.SplitSolution(target.Comp.BloodSolution.Value, transferAmount); + _solutionContainer.TryAddSolution(injectorSolution, bloodTemp); + } + + LocId msg = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message"; + var targetIdentity = Identity.Entity(target, EntityManager); + var finalMessage = Loc.GetString(msg, ("amount", transferAmount), ("target", targetIdentity)); + _popup.PopupClient(finalMessage, target, user); + + AfterDraw(injector, user, target); + } + + /// + /// This handles logic like DNA and Delays after injection. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + private void AfterInject(Entity injector, EntityUid user, EntityUid target) + { + // Leave some DNA from the injectee on it + _forensics.TransferDna(injector, target); + // Reset the delay, if present. + + _useDelay.TryResetDelay(injector); + + // Automatically set syringe to draw after completely draining it. + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) + || solution.Volume != 0) + return; + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode) + || activeMode.Behavior.HasFlag(InjectorBehavior.Dynamic)) + return; + + foreach (var mode in injector.Comp.AllowedModes) + { + if (!_prototypeManager.Resolve(mode, out var proto) + || !proto.Behavior.HasFlag(InjectorBehavior.Draw)) + continue; + + ToggleMode(injector, user, proto); + return; + } + } + + /// + /// This handles logic like DNA after drawing. + /// + /// The injector used. + /// The entity using the injector. + /// The entity targeted by the user. + private void AfterDraw(Entity injector, EntityUid user, EntityUid target) + { + // Leave some DNA from the drawee on it + _forensics.TransferDna(injector, target); + + // Automatically set the syringe to inject after completely filling it. + if (!_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution) + || solution.AvailableVolume != 0) + return; + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeMode) + || activeMode.Behavior.HasFlag(InjectorBehavior.Dynamic)) + return; + + foreach (var mode in injector.Comp.AllowedModes) + { + if (!_prototypeManager.Resolve(mode, out var proto) + || !proto.Behavior.HasFlag(InjectorBehavior.Inject)) + continue; + + ToggleMode(injector, user, proto); + return; + } + } + #endregion Injecting/Drawing + + #region Mode Toggling + /// + /// Toggle modes of the injector if possible. + /// + /// The injector whose mode is to be toggled. + /// The user toggling the mode. + /// The desired mode. + /// This will still check if the injector can use that mode. + [PublicAPI] + public void ToggleMode(Entity injector, EntityUid user, InjectorModePrototype mode) + { + var index = injector.Comp.AllowedModes.FindIndex(nextMode => mode == nextMode); + + injector.Comp.ActiveModeProtoId = injector.Comp.AllowedModes[index]; + + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var newMode)) + return; + + var modeName = Loc.GetString(newMode.Name); + var message = Loc.GetString("injector-component-mode-changed-text", ("mode", modeName)); + _popup.PopupClient(message, user, user); + Dirty(injector); + } + + /// + /// Toggle the mode of the injector to the next allowed mode. + /// + /// The injector whose mode is to be toggled. + /// The user toggling the mode. + [PublicAPI] + public void ToggleMode(Entity injector, EntityUid user) + { + if (!_prototypeManager.Resolve(injector.Comp.ActiveModeProtoId, out var activeProto)) + return; + + string? errorMessage = null; + + foreach (var allowedMode in injector.Comp.AllowedModes) + { + if (!_prototypeManager.Resolve(allowedMode, out var proto) + || proto.Behavior.HasFlag(activeProto.Behavior) + || !_solutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)) + continue; + + if (proto.Behavior.HasFlag(InjectorBehavior.Inject) && solution.Volume == 0) + { + errorMessage = "injector-component-cannot-toggle-inject-message"; + continue; + } + + if (proto.Behavior.HasFlag(InjectorBehavior.Draw) && solution.AvailableVolume == 0) + { + errorMessage = "injector-component-cannot-toggle-draw-message"; + continue; + } + + ToggleMode(injector, user, proto); + return; + } + if (errorMessage != null) + _popup.PopupClient(Loc.GetString(errorMessage), user, user); + } + #endregion Mode Toggling +} diff --git a/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs deleted file mode 100644 index e20045fc78..0000000000 --- a/Content.Shared/Chemistry/EntitySystems/SharedInjectorSystem.cs +++ /dev/null @@ -1,536 +0,0 @@ -using System.Linq; -using Content.Shared.Administration.Logs; -using Content.Shared.Body.Components; -using Content.Shared.Body.Systems; -using Content.Shared.Chemistry.Components; -using Content.Shared.Chemistry.Components.SolutionManager; -using Content.Shared.CombatMode; -using Content.Shared.Database; -using Content.Shared.DoAfter; -using Content.Shared.FixedPoint; -using Content.Shared.Forensics.Systems; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Interaction.Events; -using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; -using Content.Shared.Nutrition.EntitySystems; -using Content.Shared.Popups; -using Content.Shared.Stacks; -using Content.Shared.Verbs; - -namespace Content.Shared.Chemistry.EntitySystems; - -public abstract class SharedInjectorSystem : EntitySystem -{ - [Dependency] private readonly SharedBloodstreamSystem _blood = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly MobStateSystem _mobState = default!; - [Dependency] private readonly OpenableSystem _openable = default!; - [Dependency] private readonly ReactiveSystem _reactiveSystem = default!; - [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedForensicsSystem _forensics = default!; - [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainer = default!; - - public override void Initialize() - { - SubscribeLocalEvent>(AddSetTransferVerbs); - SubscribeLocalEvent(OnInjectorUse); - SubscribeLocalEvent(OnInjectorAfterInteract); - SubscribeLocalEvent(OnInjectDoAfter); - } - - private void AddSetTransferVerbs(Entity ent, ref GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || args.Hands == null) - return; - - if (ent.Comp.TransferAmounts.Count <= 1) - return; // No options to cycle between - - var user = args.User; - - var min = ent.Comp.TransferAmounts.Min(); - var max = ent.Comp.TransferAmounts.Max(); - var cur = ent.Comp.CurrentTransferAmount; - var toggleAmount = cur == max ? min : max; - - var priority = 0; - AlternativeVerb toggleVerb = new() - { - Text = Loc.GetString("comp-solution-transfer-verb-toggle", ("amount", toggleAmount)), - Category = VerbCategory.SetTransferAmount, - Act = () => - { - ent.Comp.CurrentTransferAmount = toggleAmount; - _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user); - Dirty(ent); - }, - - Priority = priority - }; - args.Verbs.Add(toggleVerb); - - priority -= 1; - - // Add specific transfer verbs for amounts defined in the component - foreach (var amount in ent.Comp.TransferAmounts) - { - AlternativeVerb verb = new() - { - Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)), - Category = VerbCategory.SetTransferAmount, - Act = () => - { - ent.Comp.CurrentTransferAmount = amount; - _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user); - Dirty(ent); - }, - - // we want to sort by size, not alphabetically by the verb text. - Priority = priority - }; - - priority -= 1; - - args.Verbs.Add(verb); - } - } - - private void OnInjectorUse(Entity ent, ref UseInHandEvent args) - { - if (args.Handled) - return; - - Toggle(ent, args.User); - args.Handled = true; - } - - private void OnInjectorAfterInteract(Entity ent, ref AfterInteractEvent args) - { - if (args.Handled || !args.CanReach) - return; - - //Make sure we have the attacking entity - if (args.Target is not { Valid: true } target || !HasComp(ent)) - return; - - // Is the target a mob? If yes, use a do-after to give them time to respond. - if (HasComp(target) || HasComp(target)) - { - // Are use using an injector capable of targeting a mob? - if (ent.Comp.IgnoreMobs) - return; - - InjectDoAfter(ent, target, args.User); - args.Handled = true; - return; - } - - // Instantly draw from or inject into jugs, bottles etc. - args.Handled = TryUseInjector(ent, target, args.User); - } - - private void OnInjectDoAfter(Entity ent, ref InjectorDoAfterEvent args) - { - if (args.Cancelled || args.Handled || args.Args.Target == null) - return; - - args.Handled = TryUseInjector(ent, args.Args.Target.Value, args.Args.User); - } - - /// - /// Send informative pop-up messages and wait for a do-after to complete. - /// - private void InjectDoAfter(Entity injector, EntityUid target, EntityUid user) - { - // Create a pop-up for the user - if (injector.Comp.ToggleState == InjectorToggleMode.Draw) - { - _popup.PopupClient(Loc.GetString("injector-component-drawing-user"), target, user); - } - else - { - _popup.PopupClient(Loc.GetString("injector-component-injecting-user"), target, user); - } - - if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)) - return; - - var actualDelay = injector.Comp.Delay; - FixedPoint2 amountToInject; - if (injector.Comp.ToggleState == InjectorToggleMode.Draw) - { - // additional delay is based on actual volume left to draw in syringe when smaller than transfer amount - amountToInject = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, solution.MaxVolume - solution.Volume); - } - else - { - // additional delay is based on actual volume left to inject in syringe when smaller than transfer amount - amountToInject = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, solution.Volume); - } - - // Injections take 0.5 seconds longer per 5u of possible space/content - // First 5u(MinimumTransferAmount) doesn't incur delay - actualDelay += injector.Comp.DelayPerVolume * FixedPoint2.Max(0, amountToInject - injector.Comp.TransferAmounts.Min()).Double(); - - // Ensure that minimum delay before incapacitation checks is 1 seconds - actualDelay = MathHelper.Max(actualDelay, TimeSpan.FromSeconds(1)); - - if (user != target) // injecting someone else - { - // Create a pop-up for the target - var userName = Identity.Entity(user, EntityManager); - if (injector.Comp.ToggleState == InjectorToggleMode.Draw) - { - _popup.PopupEntity(Loc.GetString("injector-component-drawing-target", - ("user", userName)), user, target); - } - else - { - _popup.PopupEntity(Loc.GetString("injector-component-injecting-target", - ("user", userName)), user, target); - } - - - // Check if the target is incapacitated or in combat mode and modify time accordingly. - if (_mobState.IsIncapacitated(target)) - { - actualDelay /= 2.5f; - } - else if (_combatMode.IsInCombatMode(target)) - { - // Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in - // combat with fast syringes & lag. - actualDelay += TimeSpan.FromSeconds(1); - } - - // Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same. - if (injector.Comp.ToggleState == InjectorToggleMode.Inject) - { - _adminLogger.Add(LogType.ForceFeed, - $"{ToPrettyString(user):user} is attempting to inject {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}"); - } - else - { - _adminLogger.Add(LogType.ForceFeed, - $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.CurrentTransferAmount.ToString()} units from {ToPrettyString(target):target}"); - } - } - else // injecting yourself - { - // Self-injections take half as long. - actualDelay /= 2; - - if (injector.Comp.ToggleState == InjectorToggleMode.Inject) - { - _adminLogger.Add(LogType.Ingestion, - $"{ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}."); - } - else - { - _adminLogger.Add(LogType.ForceFeed, - $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.CurrentTransferAmount.ToString()} units from themselves."); - } - } - - _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner) - { - BreakOnMove = true, - BreakOnWeightlessMove = false, - BreakOnDamage = true, - NeedHand = injector.Comp.NeedHand, - BreakOnHandChange = injector.Comp.BreakOnHandChange, - MovementThreshold = injector.Comp.MovementThreshold, - }); - } - - private bool TryUseInjector(Entity injector, EntityUid target, EntityUid user) - { - var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target); - // Handle injecting/drawing for solutions - if (injector.Comp.ToggleState == InjectorToggleMode.Inject) - { - if (isOpenOrIgnored && SolutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _)) - return TryInject(injector, target, injectableSolution.Value, user, false); - - if (isOpenOrIgnored && SolutionContainer.TryGetRefillableSolution(target, out var refillableSolution, out _)) - return TryInject(injector, target, refillableSolution.Value, user, true); - - if (TryComp(target, out var bloodstream)) - return TryInjectIntoBloodstream(injector, (target, bloodstream), user); - - LocId msg = target == user ? "injector-component-cannot-transfer-message-self" : "injector-component-cannot-transfer-message"; - _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user); - } - else if (injector.Comp.ToggleState == InjectorToggleMode.Draw) - { - // Draw from a bloodstream, if the target has that - if (TryComp(target, out var stream) && - SolutionContainer.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution)) - { - return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user); - } - - // Draw from an object (food, beaker, etc) - if (isOpenOrIgnored && SolutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _)) - return TryDraw(injector, target, drawableSolution.Value, user); - - LocId msg = target == user ? "injector-component-cannot-draw-message-self" : "injector-component-cannot-draw-message"; - _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector.Owner, user); - } - return false; - } - - private bool TryInject(Entity injector, EntityUid target, - Entity targetSolution, EntityUid user, bool asRefill) - { - if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, - out var solution) || solution.Volume == 0) - return false; - - // Get transfer amount. May be smaller than _transferAmount if not enough room - var realTransferAmount = - FixedPoint2.Min(injector.Comp.CurrentTransferAmount, targetSolution.Comp.Solution.AvailableVolume); - - if (realTransferAmount <= 0) - { - LocId msg = target == user ? "injector-component-target-already-full-message-self" : "injector-component-target-already-full-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, - user); - return false; - } - - // Move units from attackSolution to targetSolution - Solution removedSolution; - if (TryComp(target, out var stack)) - removedSolution = SolutionContainer.SplitStackSolution(injector.Comp.Solution.Value, realTransferAmount, stack.Count); - else - removedSolution = SolutionContainer.SplitSolution(injector.Comp.Solution.Value, realTransferAmount); - - _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); - - if (!asRefill) - SolutionContainer.Inject(target, targetSolution, removedSolution); - else - SolutionContainer.Refill(target, targetSolution, removedSolution); - - LocId msgSuccess = target == user ? "injector-component-transfer-success-message-self" : "injector-component-transfer-success-message"; - _popup.PopupClient( - Loc.GetString(msgSuccess, - ("amount", removedSolution.Volume), - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - - AfterInject(injector, target); - return true; - } - - private bool TryInjectIntoBloodstream(Entity injector, Entity target, - EntityUid user) - { - // Get transfer amount. May be smaller than _transferAmount if not enough room - if (!SolutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName, - ref target.Comp.BloodSolution, out var bloodSolution)) - { - LocId msg = target.Owner == user ? "injector-component-cannot-inject-message-self" : "injector-component-cannot-inject-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - return false; - } - - var realTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, bloodSolution.AvailableVolume); - if (realTransferAmount <= 0) - { - LocId msg = target.Owner == user ? "injector-component-cannot-inject-message-self" : "injector-component-cannot-inject-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - return false; - } - - // Move units from attackSolution to targetSolution - var removedSolution = SolutionContainer.SplitSolution(target.Comp.BloodSolution.Value, realTransferAmount); - - _blood.TryAddToBloodstream(target.AsNullable(), removedSolution); - - _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection); - - LocId msgSuccess = target.Owner == user ? "injector-component-inject-success-message-self" : "injector-component-inject-success-message"; - _popup.PopupClient( - Loc.GetString(msgSuccess, - ("amount", removedSolution.Volume), - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - - AfterInject(injector, target); - return true; - } - - private bool TryDraw(Entity injector, Entity target, - Entity targetSolution, EntityUid user) - { - if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, - out var solution) || solution.AvailableVolume == 0) - { - return false; - } - - var applicableTargetSolution = targetSolution.Comp.Solution; - // If a whitelist exists, remove all non-whitelisted reagents from the target solution temporarily - var temporarilyRemovedSolution = new Solution(); - if (injector.Comp.ReagentWhitelist is { } reagentWhitelist) - { - temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentWhitelist.ToArray()); - } - - // Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector - var realTransferAmount = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, applicableTargetSolution.Volume, - solution.AvailableVolume); - - if (realTransferAmount <= 0) - { - LocId msg = target.Owner == user ? "injector-component-target-is-empty-message-self" : "injector-component-target-is-empty-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - return false; - } - - // We have some snowflaked behavior for streams. - if (target.Comp != null) - { - DrawFromBlood(injector, (target.Owner, target.Comp), injector.Comp.Solution.Value, realTransferAmount, user); - return true; - } - - // Move units from attackSolution to targetSolution - var removedSolution = SolutionContainer.Draw(target.Owner, targetSolution, realTransferAmount); - - // Add back non-whitelisted reagents to the target solution - SolutionContainer.TryAddSolution(targetSolution, temporarilyRemovedSolution); - - if (!SolutionContainer.TryAddSolution(injector.Comp.Solution.Value, removedSolution)) - { - return false; - } - - LocId msgSuccess = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message"; - _popup.PopupClient( - Loc.GetString(msgSuccess, - ("amount", removedSolution.Volume), - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - - AfterDraw(injector, target); - return true; - } - - private void DrawFromBlood(Entity injector, Entity target, - Entity injectorSolution, FixedPoint2 transferAmount, EntityUid user) - { - if (SolutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName, - ref target.Comp.BloodSolution)) - { - var bloodTemp = SolutionContainer.SplitSolution(target.Comp.BloodSolution.Value, transferAmount); - SolutionContainer.TryAddSolution(injectorSolution, bloodTemp); - } - - LocId msg = target.Owner == user ? "injector-component-draw-success-message-self" : "injector-component-draw-success-message"; - _popup.PopupClient( - Loc.GetString(msg, - ("amount", transferAmount), - ("target", Identity.Entity(target, EntityManager))), - injector.Owner, user); - - AfterDraw(injector, target); - } - - private void AfterInject(Entity injector, EntityUid target) - { - // Automatically set syringe to draw after completely draining it. - if (SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, - out var solution) && solution.Volume == 0) - { - SetMode(injector, InjectorToggleMode.Draw); - } - - // Leave some DNA from the injectee on it - _forensics.TransferDna(injector, target); - } - - private void AfterDraw(Entity injector, EntityUid target) - { - // Automatically set syringe to inject after completely filling it. - if (SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, - out var solution) && solution.AvailableVolume == 0) - { - SetMode(injector, InjectorToggleMode.Inject); - } - - // Leave some DNA from the drawee on it - _forensics.TransferDna(injector, target); - } - - /// - /// Toggle the injector between draw/inject state if applicable. - /// - public void Toggle(Entity injector, EntityUid user) - { - if (injector.Comp.InjectOnly) - return; - - if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution)) - return; - - string msg; - - switch (injector.Comp.ToggleState) - { - case InjectorToggleMode.Inject: - if (solution.AvailableVolume > 0) // If solution has empty space to fill up, allow toggling to draw - { - SetMode(injector, InjectorToggleMode.Draw); - msg = "injector-component-drawing-text"; - } - else - { - msg = "injector-component-cannot-toggle-draw-message"; - } - break; - case InjectorToggleMode.Draw: - if (solution.Volume > 0) // If solution has anything in it, allow toggling to inject - { - SetMode(injector, InjectorToggleMode.Inject); - msg = "injector-component-injecting-text"; - } - else - { - msg = "injector-component-cannot-toggle-inject-message"; - } - break; - default: - throw new ArgumentOutOfRangeException(); - } - - _popup.PopupClient(Loc.GetString(msg), injector, user); - } - - /// - /// Set the mode of the injector to draw or inject. - /// - public void SetMode(Entity injector, InjectorToggleMode mode) - { - injector.Comp.ToggleState = mode; - Dirty(injector); - } -} diff --git a/Content.Shared/Chemistry/Events/HyposprayEvents.cs b/Content.Shared/Chemistry/Events/HyposprayEvents.cs deleted file mode 100644 index 33293a4049..0000000000 --- a/Content.Shared/Chemistry/Events/HyposprayEvents.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Content.Shared.Inventory; - -namespace Content.Shared.Chemistry.Hypospray.Events; - -public abstract partial class BeforeHyposprayInjectsTargetEvent : CancellableEntityEventArgs, IInventoryRelayEvent -{ - public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET; - public EntityUid EntityUsingHypospray; - public readonly EntityUid Hypospray; - public EntityUid TargetGettingInjected; - public string? InjectMessageOverride; - - public BeforeHyposprayInjectsTargetEvent(EntityUid user, EntityUid hypospray, EntityUid target) - { - EntityUsingHypospray = user; - Hypospray = hypospray; - TargetGettingInjected = target; - InjectMessageOverride = null; - } -} - -/// -/// This event is raised on the user using the hypospray before the hypospray is injected. -/// The event is triggered on the user and all their clothing. -/// -public sealed class SelfBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent -{ - public SelfBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { } -} - -/// -/// This event is raised on the target before the hypospray is injected. -/// The event is triggered on the target itself and all its clothing. -/// -public sealed class TargetBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent -{ - public TargetBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { } -} diff --git a/Content.Shared/Chemistry/Events/InjectorEvents.cs b/Content.Shared/Chemistry/Events/InjectorEvents.cs new file mode 100644 index 0000000000..ce4ccb88f1 --- /dev/null +++ b/Content.Shared/Chemistry/Events/InjectorEvents.cs @@ -0,0 +1,43 @@ +using Content.Shared.DoAfter; +using Content.Shared.Inventory; +using Robust.Shared.Serialization; + +namespace Content.Shared.Chemistry.Events; + +/// +/// Raised on the injector when the doafter has finished. +/// +[Serializable, NetSerializable] +public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent; + +/// +/// The base injection attempt event. It'll be raised on the user and target when attempting to inject the target. +/// +/// The user who is trying to inject the target. +/// The injector being used by the user. +/// The target who the user is trying to inject. +/// The resulting message that gets displayed per popup. +public abstract partial class BeforeInjectTargetEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null) + : CancellableEntityEventArgs, IInventoryRelayEvent +{ + public EntityUid EntityUsingInjector = user; + public readonly EntityUid UsedInjector = usedInjector; + public EntityUid TargetGettingInjected = target; + public string? OverrideMessage = overrideMessage; + public SlotFlags TargetSlots => SlotFlags.WITHOUT_POCKET; +} + +/// +/// This event is raised on the user using the injector before the injector is injected. +/// The event is triggered on the user and all their clothing. +/// +public sealed class SelfBeforeInjectEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null) + : BeforeInjectTargetEvent(user, usedInjector, target, overrideMessage); + +/// +/// This event is raised on the target before the injector is injected. +/// The event is triggered on the target itself and all its clothing. +/// +[ByRefEvent] +public sealed class TargetBeforeInjectEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null) + : BeforeInjectTargetEvent(user, usedInjector, target, overrideMessage); diff --git a/Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs b/Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs new file mode 100644 index 0000000000..8434309b26 --- /dev/null +++ b/Content.Shared/Chemistry/Prototypes/InjectorModePrototype.cs @@ -0,0 +1,145 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.FixedPoint; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; + +namespace Content.Shared.Chemistry.Prototypes; + +/// +/// This defines the behavior of an injector. +/// Every injector requires this and it defines how much an injector injects, what transferamounts they can switch between, etc. +/// +[Prototype] +public sealed partial class InjectorModePrototype : IPrototype, IInheritingPrototype +{ + /// + [IdDataField] + public string ID { get; private set; } = default!; + + /// + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] + public string[]? Parents { get; } + + /// + [AbstractDataField, NeverPushInheritance] + public bool Abstract { get; } + + /// + /// The name of the mode that will be shown on the label UI. + /// + [DataField(required: true)] + public LocId Name; + + /// + /// If true, it'll inject the user when used in hand (Default Key: Y/Z) + /// + [DataField] + public bool InjectOnUse; + + /// + /// The transfer amounts for the set-transfer verb. + /// + [DataField] + public List TransferAmounts = new() { 1, 5, 10, 15 }; + + /// + /// Injection/Drawing delay (seconds) when the target is a mob. + /// + [DataField] + public TimeSpan MobTime = TimeSpan.FromSeconds(5); + + /// + /// The delay to draw Reagents from Containers. + /// If set, RefillTime should probably have the same value. + /// + [DataField] + public TimeSpan ContainerDrawTime = TimeSpan.Zero; + + + /// + /// The number to multiply and if the target is the downed. + /// Downed counts as crouching, buckled on a bed or critical. + /// + [DataField] + public float DownedModifier = 0.5f; + + /// + /// The number to multiply and if the target is the user. + /// + [DataField] + public float SelfModifier = 0.5f; + + /// + /// This delay will increase the DoAfter time for each Xu above . + /// + [DataField] + public TimeSpan DelayPerVolume = TimeSpan.FromSeconds(0.1); + + /// + /// This works in tandem with . + /// + [DataField] + public FixedPoint2 IgnoreDelayForVolume = FixedPoint2.New(5); + + /// + /// What message will be displayed to the user when attempting to inject someone. + /// + /// + /// This is used for when you aren't injecting with a needle or an instant hypospray. + /// It would be weird if someone injects with a spray, but the popup says "needle". + /// + [DataField] + public LocId PopupUserAttempt = "injector-component-needle-injecting-user"; + + /// + /// What message will be displayed to the target when someone attempts to inject into them. + /// + [DataField] + public LocId PopupTargetAttempt = "injector-component-needle-injecting-target"; + + /// + /// The state of the injector. Determines its attack behavior. Containers must have the + /// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should + /// only ever be set to Inject + /// + [DataField] + public InjectorBehavior Behavior = InjectorBehavior.Inject; + + /// + /// Sound that will be played when injecting. + /// + [DataField] + public SoundSpecifier? InjectSound; + + /// + /// A popup for the target upon a successful injection. + /// It's imperative that this is not null when is instant. + /// + [DataField] + public LocId? InjectPopupTarget; + +} + +/// +/// Possible modes for an . +/// +[Serializable, NetSerializable, Flags] +public enum InjectorBehavior +{ + /// + /// The injector will try to inject reagent into things. + /// + Inject = 1 << 0, + + /// + /// The injector will try to draw reagent from things. + /// + Draw = 1 << 1, + + /// + /// The injector will draw from containers and inject into mobs. + /// + Dynamic = 1 << 2, +} diff --git a/Content.Shared/Clumsy/ClumsySystem.cs b/Content.Shared/Clumsy/ClumsySystem.cs index 35866b155a..4650065ad6 100644 --- a/Content.Shared/Clumsy/ClumsySystem.cs +++ b/Content.Shared/Clumsy/ClumsySystem.cs @@ -1,5 +1,5 @@ using Content.Shared.CCVar; -using Content.Shared.Chemistry.Hypospray.Events; +using Content.Shared.Chemistry.Events; using Content.Shared.Climbing.Components; using Content.Shared.Climbing.Events; using Content.Shared.Damage.Systems; @@ -31,7 +31,7 @@ public sealed class ClumsySystem : EntitySystem public override void Initialize() { - SubscribeLocalEvent(BeforeHyposprayEvent); + SubscribeLocalEvent(BeforeHyposprayEvent); SubscribeLocalEvent(BeforeDefibrillatorZapsEvent); SubscribeLocalEvent(BeforeGunShotEvent); SubscribeLocalEvent(OnCatchAttempt); @@ -40,7 +40,7 @@ public sealed class ClumsySystem : EntitySystem // If you add more clumsy interactions add them in this section! #region Clumsy interaction events - private void BeforeHyposprayEvent(Entity ent, ref SelfBeforeHyposprayInjectsEvent args) + private void BeforeHyposprayEvent(Entity ent, ref SelfBeforeInjectEvent args) { // Clumsy people sometimes inject themselves! Apparently syringes are clumsy proof... @@ -54,9 +54,9 @@ public sealed class ClumsySystem : EntitySystem if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) return; - args.TargetGettingInjected = args.EntityUsingHypospray; - args.InjectMessageOverride = Loc.GetString(ent.Comp.HypoFailedMessage); - _audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingHypospray); + args.TargetGettingInjected = args.EntityUsingInjector; + args.OverrideMessage = Loc.GetString(ent.Comp.HypoFailedMessage); + _audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingInjector); } private void BeforeDefibrillatorZapsEvent(Entity ent, ref SelfBeforeDefibrillatorZapsEvent args) diff --git a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs index 05062aed2e..9dcd965492 100644 --- a/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs +++ b/Content.Shared/Fluids/SharedPuddleSystem.Spillable.cs @@ -1,6 +1,7 @@ using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Prototypes; using Content.Shared.Chemistry.Reaction; using Content.Shared.CombatMode.Pacification; using Content.Shared.Database; @@ -22,6 +23,7 @@ namespace Content.Shared.Fluids; public abstract partial class SharedPuddleSystem { private static readonly FixedPoint2 MeleeHitTransferProportion = 0.25; + [Dependency] private readonly InjectorSystem _injectorSystem = default!; protected virtual void InitializeSpillable() { @@ -68,6 +70,7 @@ public abstract partial class SharedPuddleSystem if (entity.Comp.SpillDelay == null) { var target = args.Target; + var user = args.User; verb.Act = () => { var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume); @@ -75,9 +78,21 @@ public abstract partial class SharedPuddleSystem // TODO: Make this an event subscription once spilling puddles is predicted. // Injectors should not be hardcoded here. - if (TryComp(entity, out var injectorComp)) + if (TryComp(entity, out var injectorComp) + && _prototypeManager.Resolve(injectorComp.ActiveModeProtoId, out var activeMode) + && !activeMode.Behavior.HasFlag(InjectorBehavior.Draw)) { - injectorComp.ToggleState = InjectorToggleMode.Draw; + foreach (var mode in injectorComp.AllowedModes) + { + if (!_prototypeManager.Resolve(mode, out var protoMode)) + continue; + + if (protoMode.Behavior.HasAnyFlag(InjectorBehavior.Draw | InjectorBehavior.Dynamic)) + { + _injectorSystem.ToggleMode((entity, injectorComp), user, protoMode); + break; + } + } Dirty(entity, injectorComp); } }; diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index 11b7f61130..242e8d0de9 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -2,10 +2,9 @@ using Content.Shared.Armor; using Content.Shared.Atmos; using Content.Shared.Chat; using Content.Shared.Chemistry; -using Content.Shared.Chemistry.Hypospray.Events; +using Content.Shared.Chemistry.Events; using Content.Shared.Climbing.Events; using Content.Shared.Contraband; -using Content.Shared.Damage; using Content.Shared.Damage.Events; using Content.Shared.Damage.Systems; using Content.Shared.Electrocution; @@ -48,8 +47,8 @@ public partial class InventorySystem SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); - SubscribeLocalEvent(RelayInventoryEvent); - SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); diff --git a/Resources/Locale/en-US/chemistry/components/hypospray-component.ftl b/Resources/Locale/en-US/chemistry/components/hypospray-component.ftl deleted file mode 100644 index 36d229e78f..0000000000 --- a/Resources/Locale/en-US/chemistry/components/hypospray-component.ftl +++ /dev/null @@ -1,20 +0,0 @@ -## UI - -hypospray-all-mode-text = Only Injects -hypospray-mobs-only-mode-text = Draws and Injects -hypospray-invalid-text = Invalid -hypospray-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}u[/color] - Mode: [color=white]{$modeString}[/color] - -## Entity - -hypospray-component-inject-other-message = You inject {THE($other)}. -hypospray-component-inject-self-message = You inject yourself. -hypospray-component-empty-message = Nothing to inject. -hypospray-component-feel-prick-message = You feel a tiny prick! -hypospray-component-transfer-already-full-message = {$owner} is already full! -hypospray-cant-inject = Can't inject into {$target}! - -hypospray-verb-mode-label = Toggle Container Draw -hypospray-verb-mode-inject-all = You cannot draw from containers anymore. -hypospray-verb-mode-inject-mobs-only = You can now draw from containers. diff --git a/Resources/Locale/en-US/chemistry/components/injector-component.ftl b/Resources/Locale/en-US/chemistry/components/injector-component.ftl index 53387ea1a4..5dc12a1f7d 100644 --- a/Resources/Locale/en-US/chemistry/components/injector-component.ftl +++ b/Resources/Locale/en-US/chemistry/components/injector-component.ftl @@ -1,37 +1,50 @@ ## UI -injector-draw-text = Draw -injector-inject-text = Inject -injector-invalid-injector-toggle-mode = Invalid -injector-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}[/color] +injector-volume-transfer-label = Volume: [color=white]{$currentVolume}/{$totalVolume}u[/color] Mode: [color=white]{$modeString}[/color] ([color=white]{$transferVolume}u[/color]) +injector-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}u[/color] + Mode: [color=white]{$modeString}[/color] +injector-toggle-verb-text = Toggle Injector Mode ## Entity -injector-component-drawing-text = Now drawing -injector-component-injecting-text = Now injecting -injector-component-cannot-transfer-message = You aren't able to transfer into {THE($target)}! -injector-component-cannot-transfer-message-self = You aren't able to transfer into yourself! -injector-component-cannot-draw-message = You aren't able to draw from {THE($target)}! -injector-component-cannot-draw-message-self = You aren't able to draw from yourself! -injector-component-cannot-inject-message = You aren't able to inject into {THE($target)}! -injector-component-cannot-inject-message-self = You aren't able to inject into yourself! +injector-component-inject-mode-name = Inject +injector-component-draw-mode-name = Draw +injector-component-dynamic-mode-name = Dynamic +injector-component-mode-changed-text = Now {$mode} injector-component-inject-success-message = You inject {$amount}u into {THE($target)}! injector-component-inject-success-message-self = You inject {$amount}u into yourself! injector-component-transfer-success-message = You transfer {$amount}u into {THE($target)}. injector-component-transfer-success-message-self = You transfer {$amount}u into yourself. injector-component-draw-success-message = You draw {$amount}u from {THE($target)}. injector-component-draw-success-message-self = You draw {$amount}u from youself. + +## Fail Messages + injector-component-target-already-full-message = {CAPITALIZE(THE($target))} is already full! injector-component-target-already-full-message-self = You are already full! injector-component-target-is-empty-message = {CAPITALIZE(THE($target))} is empty! injector-component-target-is-empty-message-self = You are empty! injector-component-cannot-toggle-draw-message = Too full to draw! injector-component-cannot-toggle-inject-message = Nothing to inject! +injector-component-cannot-toggle-dynamic-message = Can't toggle dynamic! +injector-component-empty-message = {CAPITALIZE(THE($injector))} is empty! +injector-component-blocked-user = Protective gear blocked your injection! +injector-component-blocked-other = {CAPITALIZE(THE(POSS-ADJ($target)))} armor blocked {THE($user)}'s injection! +injector-component-cannot-transfer-message = You aren't able to transfer into {THE($target)}! +injector-component-cannot-transfer-message-self = You aren't able to transfer into yourself! +injector-component-cannot-draw-message = You aren't able to draw from {THE($target)}! +injector-component-cannot-draw-message-self = You aren't able to draw from yourself! +injector-component-cannot-inject-message = You aren't able to inject into {THE($target)}! +injector-component-cannot-inject-message-self = You aren't able to inject into yourself! +injector-component-ignore-mobs = This injector can only interact with containers! ## mob-inject doafter messages -injector-component-drawing-user = You start drawing the needle. -injector-component-injecting-user = You start injecting the needle. -injector-component-drawing-target = {CAPITALIZE(THE($user))} is trying to use a needle to draw from you! -injector-component-injecting-target = {CAPITALIZE(THE($user))} is trying to inject a needle into you! +injector-component-needle-injecting-user = You start injecting the needle. +injector-component-needle-injecting-target = {CAPITALIZE(THE($user))} is trying to inject a needle into you! +injector-component-needle-drawing-user = You start drawing the needle. +injector-component-needle-drawing-target = {CAPITALIZE(THE($user))} is trying to use a needle to draw from you! + +## Target Popup Success messages +injector-component-feel-prick-message = You feel a tiny prick! diff --git a/Resources/Prototypes/Chemistry/injector_modes.yml b/Resources/Prototypes/Chemistry/injector_modes.yml new file mode 100644 index 0000000000..2a791eb0ac --- /dev/null +++ b/Resources/Prototypes/Chemistry/injector_modes.yml @@ -0,0 +1,118 @@ +## Abstracts +- type: injectorMode + abstract: true + id: BaseInjectMode + name: injector-component-inject-mode-name + behavior: Inject + popupUserAttempt: injector-component-needle-injecting-user + popupTargetAttempt: injector-component-needle-injecting-target + +- type: injectorMode + abstract: true + id: BaseDrawMode + name: injector-component-draw-mode-name + behavior: Draw + popupUserAttempt: injector-component-needle-drawing-user + popupTargetAttempt: injector-component-needle-drawing-target + +- type: injectorMode + abstract: true + id: BaseDynamicMode + name: injector-component-dynamic-mode-name + behavior: Dynamic + popupUserAttempt: injector-component-needle-injecting-user + popupTargetAttempt: injector-component-needle-injecting-target + +## Syringes +- type: injectorMode + abstract: true + id: BaseSyringeMode + transferAmounts: + - 5 + - 10 + - 15 + +- type: injectorMode + abstract: true + id: BaseCryostasisSyringeMode + transferAmounts: + - 5 + - 10 + +- type: injectorMode + abstract: true + id: BaseBluespaceSyringeMode + mobTime: 2.5 + transferAmounts: + - 5 + - 10 + - 15 + - 50 + +- type: injectorMode + parent: [ BaseSyringeMode, BaseInjectMode ] + id: SyringeInjectMode + +- type: injectorMode + parent: [ BaseSyringeMode, BaseDrawMode ] + id: SyringeDrawMode + +- type: injectorMode + parent: [ BaseBluespaceSyringeMode, BaseInjectMode ] + id: BluespaceSyringeInjectMode + +- type: injectorMode + parent: [ BaseBluespaceSyringeMode, BaseDrawMode ] + id: BluespaceSyringeDrawMode + +- type: injectorMode + parent: [ BaseCryostasisSyringeMode, BaseInjectMode ] + id: CryostasisSyringeInjectMode + +- type: injectorMode + parent: [ BaseCryostasisSyringeMode, BaseDrawMode ] + id: CryostasisSyringeDrawMode + +## Dropper +- type: injectorMode + abstract: true + id: BaseDropperMode + transferAmounts: + - 1 + - 2 + - 3 + - 4 + - 5 + +- type: injectorMode + parent: [ BaseDropperMode, BaseInjectMode ] + id: DropperInjectMode + +- type: injectorMode + parent: [ BaseDropperMode, BaseDrawMode ] + id: DropperDrawMode + +## Hyposprays +- type: injectorMode + abstract: true + id: HyposprayBaseMode + injectSound: /Audio/Items/hypospray.ogg + injectPopupTarget: injector-component-feel-prick-message + injectOnUse: true + mobTime: 0 + delayPerVolume: 0 + transferAmounts: + - 5 + +- type: injectorMode + parent: [ HyposprayBaseMode, BaseInjectMode ] + id: HyposprayInjectMode + +- type: injectorMode + parent: [ HyposprayBaseMode, BaseDynamicMode ] + id: HyposprayDynamicMode + +- type: injectorMode + parent: HyposprayDynamicMode + id: HypopenDynamicMode + containerDrawTime: 0.75 diff --git a/Resources/Prototypes/Entities/Clothing/Belt/job.yml b/Resources/Prototypes/Entities/Clothing/Belt/job.yml index 820a59e019..a40740f46a 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/job.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/job.yml @@ -185,7 +185,6 @@ - SurgeryTool - Dropper components: - - Hypospray - Injector - Pill - HandLabeler @@ -198,7 +197,7 @@ - Bottle hypo: whitelist: - components: + tags: - Hypospray pill: whitelist: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml index 343113e2f0..9b76330d98 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml @@ -1,5 +1,18 @@ - type: entity - parent: [BaseItem, BaseGrandTheftContraband] + abstract: true + parent: BaseItem + id: BaseHypospray + components: + - type: Injector + solutionName: hypospray + ignoreClosed: false + activeModeProtoId: HyposprayDynamicMode + allowedModes: + - HyposprayDynamicMode + - HyposprayInjectMode + +- type: entity + parent: [BaseHypospray, BaseGrandTheftContraband] id: Hypospray name: hypospray description: A sterile injector for rapid administration of drugs to patients. @@ -7,11 +20,11 @@ - type: Sprite sprite: Objects/Specific/Medical/hypospray.rsi layers: - - state: hypo - map: ["enum.SolutionContainerLayers.Base"] - - state: hypo_fill1 - map: ["enum.SolutionContainerLayers.Fill"] - visible: false + - state: hypo + map: ["enum.SolutionContainerLayers.Base"] + - state: hypo_fill1 + map: ["enum.SolutionContainerLayers.Fill"] + visible: false - type: Item sprite: Objects/Specific/Medical/hypospray.rsi - type: SolutionContainerManager @@ -23,8 +36,6 @@ - type: ExaminableSolution solution: hypospray exactVolume: true - - type: Hypospray - onlyAffectsMobs: false - type: UseDelay delay: 0.5 - type: StaticPrice @@ -41,7 +52,7 @@ solutionName: hypospray - type: entity - parent: BaseItem + parent: [BaseHypospray, BaseSyndicateContraband] id: SyndiHypo name: gorlex hypospray description: A sterile injector for rapid administration of drugs. Reverse-engineered from Nanotrasen designs, Cybersun produces these in limited quantities for Gorlex Marauders' corpsmen. @@ -60,23 +71,13 @@ solutions: hypospray: maxVol: 20 - - type: RefillableSolution - solution: hypospray - - type: ExaminableSolution - solution: hypospray - exactVolume: true - - type: Hypospray - onlyAffectsMobs: false - - type: UseDelay - delay: 0.5 - - type: Appearance - type: SolutionContainerVisuals maxFillLevels: 4 fillBaseName: hypo_fill solutionName: hypospray - type: entity - parent: BaseItem + parent: BaseHypospray id: BorgHypo name: borghypo description: A sterile injector for rapid administration of drugs to patients. This integrated model is specialized for use by medical borgs. @@ -94,8 +95,6 @@ solution: hypospray - type: ExaminableSolution solution: hypospray - - type: Hypospray - onlyAffectsMobs: false - type: UseDelay delay: 0.5 @@ -114,7 +113,7 @@ delay: 0.0 - type: entity - parent: BaseItem + parent: BaseHypospray id: ChemicalMedipen name: chemical medipen description: A single-dose, non-refillable medipen. @@ -145,11 +144,12 @@ - type: ExaminableSolution solution: pen exactVolume: true - - type: Hypospray + - type: Injector solutionName: pen - transferAmount: 15 - onlyAffectsMobs: false - injectOnly: true + currentTransferAmount: null + activeModeProtoId: HyposprayInjectMode + allowedModes: + - HyposprayInjectMode - type: Appearance - type: SolutionContainerVisuals maxFillLevels: 1 @@ -253,11 +253,6 @@ maxFillLevels: 1 changeColor: false emptySpriteName: bicpen_empty - - type: Hypospray - solutionName: pen - transferAmount: 20 - onlyAffectsMobs: false - injectOnly: true - type: SolutionContainerManager solutions: pen: @@ -295,11 +290,6 @@ maxFillLevels: 1 changeColor: false emptySpriteName: dermpen_empty - - type: Hypospray - solutionName: pen - transferAmount: 20 - onlyAffectsMobs: false - injectOnly: true - type: SolutionContainerManager solutions: pen: @@ -337,11 +327,6 @@ maxFillLevels: 1 changeColor: false emptySpriteName: arithpen_empty - - type: Hypospray - solutionName: pen - transferAmount: 20 - onlyAffectsMobs: false - injectOnly: true - type: SolutionContainerManager solutions: pen: @@ -379,11 +364,6 @@ maxFillLevels: 1 changeColor: false emptySpriteName: punctpen_empty - - type: Hypospray - solutionName: pen - transferAmount: 15 - onlyAffectsMobs: false - injectOnly: true - type: SolutionContainerManager solutions: pen: @@ -421,11 +401,6 @@ maxFillLevels: 1 changeColor: false emptySpriteName: pyrapen_empty - - type: Hypospray - solutionName: pen - transferAmount: 20 - onlyAffectsMobs: false - injectOnly: true - type: SolutionContainerManager solutions: pen: @@ -463,11 +438,6 @@ maxFillLevels: 1 changeColor: false emptySpriteName: dexpen_empty - - type: Hypospray - solutionName: pen - transferAmount: 40 - onlyAffectsMobs: false - injectOnly: true - type: SolutionContainerManager solutions: pen: @@ -506,11 +476,6 @@ maxFillLevels: 1 changeColor: false emptySpriteName: hypovolemic_empty - - type: Hypospray - solutionName: pen - transferAmount: 30 - onlyAffectsMobs: false - injectOnly: true - type: SolutionContainerManager solutions: pen: @@ -557,11 +522,6 @@ maxFillLevels: 1 changeColor: false emptySpriteName: stimpen_empty - - type: Hypospray - solutionName: pen - transferAmount: 30 - onlyAffectsMobs: false - injectOnly: true - type: StaticPrice price: 1500 @@ -640,11 +600,6 @@ Quantity: 25 - ReagentId: TranexamicAcid Quantity: 5 - - type: Hypospray - solutionName: pen - transferAmount: 30 - onlyAffectsMobs: false - injectOnly: true - type: StaticPrice price: 1500 @@ -665,9 +620,12 @@ solution: hypospray heldOnly: true # Allow examination only when held in hand. exactVolume: true - - type: Hypospray - onlyAffectsMobs: false - drawTime: 0.75 + - type: Injector + solutionName: hypospray + activeModeProtoId: HypopenDynamicMode + allowedModes: + - HyposprayInjectMode + - HypopenDynamicMode - type: UseDelay delay: 0.5 - type: StaticPrice # A new shitcurity meta @@ -714,8 +672,3 @@ reagents: - ReagentId: JuiceThatMakesYouWeh Quantity: 60 - - type: Hypospray - solutionName: pen - transferAmount: 1 - onlyAffectsMobs: false - injectOnly: true diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml index 82cf3dd16b..9548224440 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml @@ -379,16 +379,12 @@ injector: maxVol: 5 - type: Injector - injectOnly: false ignoreMobs: true ignoreClosed: false - transferAmounts: - - 1 - - 2 - - 3 - - 4 - - 5 - currentTransferAmount: 1 + activeModeProtoId: DropperDrawMode + allowedModes: + - DropperDrawMode + - DropperInjectMode - type: ExaminableSolution solution: dropper exactVolume: true @@ -461,11 +457,10 @@ injector: maxVol: 15 - type: Injector - injectOnly: false - transferAmounts: - - 5 - - 10 - - 15 + activeModeProtoId: SyringeDrawMode + allowedModes: + - SyringeDrawMode + - SyringeInjectMode - type: ExaminableSolution solution: injector exactVolume: true @@ -487,8 +482,6 @@ parent: BaseSyringe id: Syringe components: - - type: Injector - currentTransferAmount: 15 - type: Tag tags: - Syringe @@ -511,14 +504,6 @@ solutions: injector: maxVol: 5 - - type: Injector - transferAmounts: - - 1 - - 2 - - 3 - - 4 - - 5 - currentTransferAmount: 5 - type: SolutionContainerVisuals maxFillLevels: 3 fillBaseName: minisyringe @@ -572,7 +557,7 @@ id: PrefilledSyringe components: - type: Injector - toggleState: Inject + activeModeProtoId: SyringeInjectMode - type: entity id: SyringeBluespace @@ -595,8 +580,10 @@ injector: maxVol: 100 - type: Injector - delay: 2.5 - injectOnly: false + activeModeProtoId: BluespaceSyringeDrawMode + allowedModes: + - BluespaceSyringeInjectMode + - BluespaceSyringeDrawMode - type: SolutionContainerVisuals maxFillLevels: 2 fillBaseName: syringe @@ -627,11 +614,10 @@ maxVol: 10 canReact: false - type: Injector - injectOnly: false - transferAmounts: - - 5 - - 10 - currentTransferAmount: 10 + activeModeProtoId: CryostasisSyringeDrawMode + allowedModes: + - CryostasisSyringeInjectMode + - CryostasisSyringeDrawMode - type: Tag tags: - Syringe diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index a1e01a14a6..ab7bc3c3ab 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -787,6 +787,9 @@ - type: Tag id: HudSecurity # ConstructionGraph: HudMedSec, GlassesSecHUD +- type: Tag + id: Hypospray # ItemMapper: ClothingBeltMedical + ## I ## - type: Tag -- 2.52.0