+++ /dev/null
-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<HyposprayComponent>(ent => new HyposprayStatusControl(ent, _solutionContainers));
- }
-}
--- /dev/null
+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<InjectorComponent>(injector => new InjectorStatusControl(injector, _solutionContainers, _prototypeManager));
+ }
+}
+++ /dev/null
-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<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
- }
-}
+++ /dev/null
-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<HyposprayComponent> _parent;
- private readonly RichTextLabel _label;
- private readonly SharedSolutionContainerSystem _solutionContainers;
-
- private FixedPoint2 PrevVolume;
- private FixedPoint2 PrevMaxVolume;
- private bool PrevOnlyAffectsMobs;
-
- public HyposprayStatusControl(Entity<HyposprayComponent> 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)));
- }
-}
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<InjectorComponent> _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<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers)
+ public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers, IPrototypeManager prototypeManager)
{
+ _prototypeManager = prototypeManager;
+
_parent = parent;
_solutionContainers = solutionContainers;
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
{
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))));
+ }
}
}
+++ /dev/null
-
-using Content.Shared.Chemistry.EntitySystems;
-
-namespace Content.Server.Chemistry.EntitySystems;
-
-public sealed class InjectorSystem : SharedInjectorSystem;
+++ /dev/null
-using Content.Shared.FixedPoint;
-using Robust.Shared.GameStates;
-using Robust.Shared.Audio;
-
-namespace Content.Shared.Chemistry.Components;
-
-/// <summary>
-/// Component that allows an entity instantly transfer liquids by interacting with objects that have solutions.
-/// </summary>
-[RegisterComponent, NetworkedComponent]
-[AutoGenerateComponentState]
-public sealed partial class HyposprayComponent : Component
-{
- /// <summary>
- /// Solution that will be used by hypospray for injections.
- /// </summary>
- [DataField]
- public string SolutionName = "hypospray";
-
- /// <summary>
- /// Amount of the units that will be transfered.
- /// </summary>
- [AutoNetworkedField]
- [DataField]
- public FixedPoint2 TransferAmount = FixedPoint2.New(5);
-
- /// <summary>
- /// The delay to draw reagents using the hypospray.
- /// If set, <see cref="RefillableSolutionComponent"/> RefillTime should probably have the same value.
- /// </summary>
- [DataField]
- public float DrawTime = 0f;
-
- /// <summary>
- /// Sound that will be played when injecting.
- /// </summary>
- [DataField]
- public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Items/hypospray.ogg");
-
- /// <summary>
- /// Decides whether you can inject everything or just mobs.
- /// </summary>
- [AutoNetworkedField]
- [DataField(required: true)]
- public bool OnlyAffectsMobs = false;
-
- /// <summary>
- /// If this can draw from containers in mob-only mode.
- /// </summary>
- [AutoNetworkedField]
- [DataField]
- public bool CanContainerDraw = true;
-
- /// <summary>
- /// Whether or not the hypospray is able to draw from containers or if it's a single use
- /// device that can only inject.
- /// </summary>
- [DataField]
- public bool InjectOnly = false;
-}
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;
/// <remarks>
/// 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.
/// </remarks>
-/// <seealso cref="SharedInjectorSystem"/>
-/// <seealso cref="InjectorToggleMode"/>
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+/// <seealso cref="InjectorModePrototype"/>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(InjectorSystem))]
public sealed partial class InjectorComponent : Component
{
/// <summary>
public Entity<SolutionComponent>? Solution = null;
/// <summary>
- /// Whether or not the injector is able to draw from containers or if it's a single use
- /// device that can only inject.
- /// </summary>
- [DataField]
- public bool InjectOnly;
-
- /// <summary>
- /// Whether or not the injector is able to draw from or inject from mobs.
+ /// Amount to inject or draw on each usage.
/// </summary>
/// <remarks>
- /// For example: droppers would ignore mobs.
+ /// If its set null, this injector is marked to inject its entire contents upon usage.
/// </remarks>
- [DataField]
- public bool IgnoreMobs;
+ [DataField, AutoNetworkedField]
+ public FixedPoint2? CurrentTransferAmount = FixedPoint2.New(5);
- /// <summary>
- /// Whether or not the injector is able to draw from or inject into containers that are closed/sealed.
- /// </summary>
- /// <remarks>
- /// For example: droppers can not inject into cans, but syringes can.
- /// </remarks>
- [DataField]
- public bool IgnoreClosed = true;
/// <summary>
- /// The transfer amounts for the set-transfer verb.
+ /// The mode that this injector starts with on MapInit.
/// </summary>
- [DataField]
- public List<FixedPoint2> TransferAmounts = new() { 1, 5, 10, 15 };
+ [DataField(required: true), AutoNetworkedField]
+ public ProtoId<InjectorModePrototype> ActiveModeProtoId;
/// <summary>
- /// 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 <see cref="InjectorModePrototype"/> that it can switch between.
/// </summary>
- [DataField, AutoNetworkedField]
- public FixedPoint2 CurrentTransferAmount = FixedPoint2.New(5);
+ [DataField(required: true)]
+ public List<ProtoId<InjectorModePrototype>> AllowedModes;
/// <summary>
- /// Injection delay (seconds) when the target is a mob.
+ /// Whether the injector is able to draw from or inject from mobs.
/// </summary>
- /// <remarks>
- /// The base delay has a minimum of 1 second, but this will still be modified if the target is incapacitated or
- /// in combat mode.
- /// </remarks>
+ /// <example>
+ /// Droppers ignore mobs.
+ /// </example>
[DataField]
- public TimeSpan Delay = TimeSpan.FromSeconds(5);
+ public bool IgnoreMobs;
/// <summary>
- /// 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.
/// </summary>
+ /// <example>
+ /// Droppers can't inject into closed cans.
+ /// </example>
[DataField]
- public TimeSpan DelayPerVolume = TimeSpan.FromSeconds(0.1);
-
- /// <summary>
- /// 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
- /// </summary>
- [DataField, AutoNetworkedField]
- public InjectorToggleMode ToggleState = InjectorToggleMode.Draw;
+ public bool IgnoreClosed = true;
/// <summary>
/// Reagents that are allowed to be within this injector.
/// A null ReagentWhitelist indicates all reagents are allowed.
/// </summary>
[DataField]
- public List<ProtoId<ReagentPrototype>>? ReagentWhitelist = null;
+ public List<ProtoId<ReagentPrototype>>? ReagentWhitelist;
#region Arguments for injection doafter
- /// <inheritdoc cref=DoAfterArgs.NeedHand>
+ /// <inheritdoc cref="DoAfterArgs.NeedHand"/>
[DataField]
public bool NeedHand = true;
- /// <inheritdoc cref=DoAfterArgs.BreakOnHandChange>
+ /// <inheritdoc cref="DoAfterArgs.BreakOnHandChange"/>
[DataField]
public bool BreakOnHandChange = true;
- /// <inheritdoc cref=DoAfterArgs.MovementThreshold>
+ /// <inheritdoc cref="DoAfterArgs.MovementThreshold"/>
[DataField]
public float MovementThreshold = 0.1f;
#endregion
}
-/// <summary>
-/// Possible modes for an <see cref="InjectorComponent"/>.
-/// </summary>
-[Serializable, NetSerializable]
-public enum InjectorToggleMode : byte
+internal static class InjectorToggleModeExtensions
{
- /// <summary>
- /// The injector will try to inject reagent into things.
- /// </summary>
- Inject,
-
- /// <summary>
- /// The injector will try to draw reagent from things.
- /// </summary>
- Draw,
+ public static bool HasAnyFlag(this InjectorBehavior s1, InjectorBehavior s2)
+ {
+ return (s1 & s2) != 0;
+ }
}
-
-/// <summary>
-/// Raised on the injector when the doafter has finished.
-/// </summary>
-[Serializable, NetSerializable]
-public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent;
+++ /dev/null
-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<HyposprayComponent, AfterInteractEvent>(OnAfterInteract);
- SubscribeLocalEvent<HyposprayComponent, MeleeHitEvent>(OnAttack);
- SubscribeLocalEvent<HyposprayComponent, UseInHandEvent>(OnUseInHand);
- SubscribeLocalEvent<HyposprayComponent, GetVerbsEvent<AlternativeVerb>>(AddToggleModeVerb);
- SubscribeLocalEvent<HyposprayComponent, HyposprayDrawDoAfterEvent>(OnDrawDoAfter);
- }
-
- #region Ref events
- private void OnUseInHand(Entity<HyposprayComponent> entity, ref UseInHandEvent args)
- {
- if (args.Handled)
- return;
-
- args.Handled = TryDoInject(entity, args.User, args.User);
- }
-
- private void OnAfterInteract(Entity<HyposprayComponent> 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<HyposprayComponent> entity, ref MeleeHitEvent args)
- {
- if (args.HitEntities is [])
- return;
-
- TryDoInject(entity, args.HitEntities[0], args.User);
- }
-
- private void OnDrawDoAfter(Entity<HyposprayComponent> 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<HyposprayComponent> 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<HyposprayComponent> 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<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> 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<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> targetSolution, EntityUid user, Entity<SolutionComponent> 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<HyposprayComponent> entity, EntityUid target, Entity<SolutionComponent> 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<SolutionContainerManagerComponent>(entity) &&
- HasComp<MobStateComponent>(entity)
- : HasComp<SolutionContainerManagerComponent>(entity);
- }
-
- #endregion
-
- #region Verbs
-
- // <summary>
- // 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
- // </summary>
- private void AddToggleModeVerb(Entity<HyposprayComponent> entity, ref GetVerbsEvent<AlternativeVerb> 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<HyposprayComponent> 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<HyposprayComponent> entity, bool onlyAffectsMobs)
- {
- if (entity.Comp.OnlyAffectsMobs == onlyAffectsMobs)
- return;
-
- entity.Comp.OnlyAffectsMobs = onlyAffectsMobs;
- Dirty(entity);
- }
-
- #endregion
-}
-
-[Serializable, NetSerializable]
-public sealed partial class HyposprayDrawDoAfterEvent : SimpleDoAfterEvent {}
--- /dev/null
+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;
+
+/// <summary>
+/// This handles toggling injection modes, injections and drawings for all kinds of injectors.
+/// </summary>
+/// <seealso cref="InjectorComponent"/>
+/// <seealso cref="InjectorModePrototype"/>
+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<InjectorComponent, UseInHandEvent>(OnInjectorUse);
+ SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
+ SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
+ SubscribeLocalEvent<InjectorComponent, MeleeHitEvent>(OnAttack);
+ SubscribeLocalEvent<InjectorComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
+ }
+
+ #region Events Handling
+ private void OnInjectorUse(Entity<InjectorComponent> 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<InjectorComponent> 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<BloodstreamComponent>(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<InjectorComponent> 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<InjectorComponent> injector, ref MeleeHitEvent args)
+ {
+ if (args.HitEntities is [])
+ return;
+
+ TryMobsDoAfter(injector, args.User, args.HitEntities[0]);
+ }
+
+ /// <summary>
+ /// Give the user interaction verbs for their injector.
+ /// </summary>
+ /// <param name="injector"></param>
+ /// <param name="args"></param>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ private void AddVerbs(Entity<InjectorComponent> injector, ref GetVerbsEvent<AlternativeVerb> 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
+ /// <summary>
+ /// Send informative pop-up messages and wait for a do-after to complete.
+ /// </summary>
+ private bool TryMobsDoAfter(Entity<InjectorComponent> 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;
+ }
+
+ /// <summary>
+ /// Get the DoAfter Time for Mobs.
+ /// </summary>
+ /// <param name="injector">The injector that is interacting with the mob.</param>
+ /// <param name="user">The user using the injector.</param>
+ /// <param name="target">The target mob.</param>
+ /// <param name="doAfterTime">The duration of the resulting doAfter.</param>
+ /// <param name="amount">The amount of the reagents transferred.</param>
+ /// <returns>True if calculating the time was successful, false if not.</returns>
+ private bool GetMobsDoAfterTime(Entity<InjectorComponent> 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<InjectorComponent> 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;
+ }
+
+ /// <summary>
+ /// Get the DoAfter Time for Containers and check if it is possible.
+ /// </summary>
+ /// <param name="injector">The injector that is interacting with the container.</param>
+ /// <param name="user">The user using the injector.</param>
+ /// <param name="target">The target container,</param>
+ /// <param name="doAfterTime">The duration of the resulting DoAfter.</param>
+ /// <returns>True if calculating the time was successful, false if not.</returns>
+ private bool GetContainerDoAfterTime(Entity<InjectorComponent> 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
+ /// <summary>
+ /// Depending on the <see cref="InjectorBehavior"/>, this will deal with the result of the DoAfter and draw/inject accordingly.
+ /// </summary>
+ /// <param name="injector">The injector used.</param>
+ /// <param name="user">The entity using the injector.</param>
+ /// <param name="target">The entity targeted by the user.</param>
+ /// <returns>True if the injection/drawing was successful, false if not.</returns>
+ /// <exception cref="ArgumentOutOfRangeException">The injector has a different <see cref="InjectorBehavior"/>.</exception>
+ private bool TryUseInjector(Entity<InjectorComponent> 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<BloodstreamComponent>(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<BloodstreamComponent>(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;
+ }
+
+ /// <summary>
+ /// Attempt to inject the solution of the injector into the target.
+ /// </summary>
+ /// <param name="injector">The injector used.</param>
+ /// <param name="user">The entity using the injector.</param>
+ /// <param name="target">The entity targeted by the user.</param>
+ /// <param name="targetSolution">The solution of the target.</param>
+ /// <param name="asRefill">Whether or not the solution is refillable or injectable.</param>
+ /// <returns>True if the injection was successful, false if not.</returns>
+ private bool TryInject(Entity<InjectorComponent> injector, EntityUid user, EntityUid target, Entity<SolutionComponent> 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<StackComponent>(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;
+ }
+
+ /// <summary>
+ /// Attempt to draw reagents from a container.
+ /// </summary>
+ /// <param name="injector">The injector used.</param>
+ /// <param name="user">The entity using the injector.</param>
+ /// <param name="target">The entity targeted by the user.</param>
+ /// <param name="targetSolution">The solution of the target.</param>
+ /// <returns>True if the drawing was successful, false if not.</returns>
+ private bool TryDraw(Entity<InjectorComponent> injector, EntityUid user, Entity<BloodstreamComponent?> target, Entity<SolutionComponent> 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;
+ }
+
+ /// <summary>
+ /// Attempt to draw blood from a mob.
+ /// </summary>
+ /// <param name="injector">The injector used.</param>
+ /// <param name="user">The entity using the injector.</param>
+ /// <param name="target">The entity targeted by the user.</param>
+ /// <param name="injectorSolution">The solution of the injector.</param>
+ /// <param name="transferAmount">The amount of blood to draw.</param>
+ private void DrawFromBlood(Entity<InjectorComponent> injector,
+ EntityUid user,
+ Entity<BloodstreamComponent> target,
+ Entity<SolutionComponent> 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);
+ }
+
+ /// <summary>
+ /// This handles logic like DNA and Delays after injection.
+ /// </summary>
+ /// <param name="injector">The injector used.</param>
+ /// <param name="user">The entity using the injector.</param>
+ /// <param name="target">The entity targeted by the user.</param>
+ private void AfterInject(Entity<InjectorComponent> 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;
+ }
+ }
+
+ /// <summary>
+ /// This handles logic like DNA after drawing.
+ /// </summary>
+ /// <param name="injector">The injector used.</param>
+ /// <param name="user">The entity using the injector.</param>
+ /// <param name="target">The entity targeted by the user.</param>
+ private void AfterDraw(Entity<InjectorComponent> 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
+ /// <summary>
+ /// Toggle modes of the injector if possible.
+ /// </summary>
+ /// <param name="injector">The injector whose mode is to be toggled.</param>
+ /// <param name="user">The user toggling the mode.</param>
+ /// <param name="mode">The desired mode.</param>
+ /// <remarks>This will still check if the injector can use that mode.</remarks>
+ [PublicAPI]
+ public void ToggleMode(Entity<InjectorComponent> 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);
+ }
+
+ /// <summary>
+ /// Toggle the mode of the injector to the next allowed mode.
+ /// </summary>
+ /// <param name="injector">The injector whose mode is to be toggled.</param>
+ /// <param name="user">The user toggling the mode.</param>
+ [PublicAPI]
+ public void ToggleMode(Entity<InjectorComponent> 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
+}
+++ /dev/null
-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<InjectorComponent, GetVerbsEvent<AlternativeVerb>>(AddSetTransferVerbs);
- SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
- SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
- SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
- }
-
- private void AddSetTransferVerbs(Entity<InjectorComponent> ent, ref GetVerbsEvent<AlternativeVerb> 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<InjectorComponent> ent, ref UseInHandEvent args)
- {
- if (args.Handled)
- return;
-
- Toggle(ent, args.User);
- args.Handled = true;
- }
-
- private void OnInjectorAfterInteract(Entity<InjectorComponent> 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<SolutionContainerManagerComponent>(ent))
- return;
-
- // Is the target a mob? If yes, use a do-after to give them time to respond.
- if (HasComp<MobStateComponent>(target) || HasComp<BloodstreamComponent>(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<InjectorComponent> 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);
- }
-
- /// <summary>
- /// Send informative pop-up messages and wait for a do-after to complete.
- /// </summary>
- private void InjectDoAfter(Entity<InjectorComponent> 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<InjectorComponent> 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<BloodstreamComponent>(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<BloodstreamComponent>(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<InjectorComponent> injector, EntityUid target,
- Entity<SolutionComponent> 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<StackComponent>(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<InjectorComponent> injector, Entity<BloodstreamComponent> 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<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
- Entity<SolutionComponent> 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<InjectorComponent> injector, Entity<BloodstreamComponent> target,
- Entity<SolutionComponent> 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<InjectorComponent> 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<InjectorComponent> 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);
- }
-
- /// <summary>
- /// Toggle the injector between draw/inject state if applicable.
- /// </summary>
- public void Toggle(Entity<InjectorComponent> 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);
- }
-
- /// <summary>
- /// Set the mode of the injector to draw or inject.
- /// </summary>
- public void SetMode(Entity<InjectorComponent> injector, InjectorToggleMode mode)
- {
- injector.Comp.ToggleState = mode;
- Dirty(injector);
- }
-}
+++ /dev/null
-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;
- }
-}
-
-/// <summary>
-/// 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.
-/// </summary>
-public sealed class SelfBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent
-{
- public SelfBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { }
-}
-
-/// <summary>
-/// This event is raised on the target before the hypospray is injected.
-/// The event is triggered on the target itself and all its clothing.
-/// </summary>
-public sealed class TargetBeforeHyposprayInjectsEvent : BeforeHyposprayInjectsTargetEvent
-{
- public TargetBeforeHyposprayInjectsEvent(EntityUid user, EntityUid hypospray, EntityUid target) : base(user, hypospray, target) { }
-}
--- /dev/null
+using Content.Shared.DoAfter;
+using Content.Shared.Inventory;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Chemistry.Events;
+
+/// <summary>
+/// Raised on the injector when the doafter has finished.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent;
+
+/// <summary>
+/// The base injection attempt event. It'll be raised on the user and target when attempting to inject the target.
+/// </summary>
+/// <param name="user">The user who is trying to inject the target.</param>
+/// <param name="usedInjector">The injector being used by the user.</param>
+/// <param name="target">The target who the user is trying to inject.</param>
+/// <param name="overrideMessage">The resulting message that gets displayed per popup.</param>
+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;
+}
+
+/// <summary>
+/// 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.
+/// </summary>
+public sealed class SelfBeforeInjectEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null)
+ : BeforeInjectTargetEvent(user, usedInjector, target, overrideMessage);
+
+/// <summary>
+/// This event is raised on the target before the injector is injected.
+/// The event is triggered on the target itself and all its clothing.
+/// </summary>
+[ByRefEvent]
+public sealed class TargetBeforeInjectEvent(EntityUid user, EntityUid usedInjector, EntityUid target, string? overrideMessage = null)
+ : BeforeInjectTargetEvent(user, usedInjector, target, overrideMessage);
--- /dev/null
+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;
+
+/// <summary>
+/// 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.
+/// </summary>
+[Prototype]
+public sealed partial class InjectorModePrototype : IPrototype, IInheritingPrototype
+{
+ /// <inheritdoc/>
+ [IdDataField]
+ public string ID { get; private set; } = default!;
+
+ /// <inheritdoc/>
+ [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<InjectorModePrototype>))]
+ public string[]? Parents { get; }
+
+ /// <inheritdoc/>
+ [AbstractDataField, NeverPushInheritance]
+ public bool Abstract { get; }
+
+ /// <summary>
+ /// The name of the mode that will be shown on the label UI.
+ /// </summary>
+ [DataField(required: true)]
+ public LocId Name;
+
+ /// <summary>
+ /// If true, it'll inject the user when used in hand (Default Key: Y/Z)
+ /// </summary>
+ [DataField]
+ public bool InjectOnUse;
+
+ /// <summary>
+ /// The transfer amounts for the set-transfer verb.
+ /// </summary>
+ [DataField]
+ public List<FixedPoint2> TransferAmounts = new() { 1, 5, 10, 15 };
+
+ /// <summary>
+ /// Injection/Drawing delay (seconds) when the target is a mob.
+ /// </summary>
+ [DataField]
+ public TimeSpan MobTime = TimeSpan.FromSeconds(5);
+
+ /// <summary>
+ /// The delay to draw Reagents from Containers.
+ /// If set, <see cref="RefillableSolutionComponent"/> RefillTime should probably have the same value.
+ /// </summary>
+ [DataField]
+ public TimeSpan ContainerDrawTime = TimeSpan.Zero;
+
+
+ /// <summary>
+ /// The number to multiply <see cref="MobTime"/> and <see cref="DelayPerVolume"/> if the target is the downed.
+ /// Downed counts as crouching, buckled on a bed or critical.
+ /// </summary>
+ [DataField]
+ public float DownedModifier = 0.5f;
+
+ /// <summary>
+ /// The number to multiply <see cref="MobTime"/> and <see cref="DelayPerVolume"/> if the target is the user.
+ /// </summary>
+ [DataField]
+ public float SelfModifier = 0.5f;
+
+ /// <summary>
+ /// This delay will increase the DoAfter time for each Xu above <see cref="IgnoreDelayForVolume"/>.
+ /// </summary>
+ [DataField]
+ public TimeSpan DelayPerVolume = TimeSpan.FromSeconds(0.1);
+
+ /// <summary>
+ /// This works in tandem with <see cref="DelayPerVolume"/>.
+ /// </summary>
+ [DataField]
+ public FixedPoint2 IgnoreDelayForVolume = FixedPoint2.New(5);
+
+ /// <summary>
+ /// What message will be displayed to the user when attempting to inject someone.
+ /// </summary>
+ /// <remarks>
+ /// 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".
+ /// </remarks>
+ [DataField]
+ public LocId PopupUserAttempt = "injector-component-needle-injecting-user";
+
+ /// <summary>
+ /// What message will be displayed to the target when someone attempts to inject into them.
+ /// </summary>
+ [DataField]
+ public LocId PopupTargetAttempt = "injector-component-needle-injecting-target";
+
+ /// <summary>
+ /// 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
+ /// </summary>
+ [DataField]
+ public InjectorBehavior Behavior = InjectorBehavior.Inject;
+
+ /// <summary>
+ /// Sound that will be played when injecting.
+ /// </summary>
+ [DataField]
+ public SoundSpecifier? InjectSound;
+
+ /// <summary>
+ /// A popup for the target upon a successful injection.
+ /// It's imperative that this is not null when <see cref="MobTime"/> is instant.
+ /// </summary>
+ [DataField]
+ public LocId? InjectPopupTarget;
+
+}
+
+/// <summary>
+/// Possible modes for an <see cref="InjectorModePrototype"/>.
+/// </summary>
+[Serializable, NetSerializable, Flags]
+public enum InjectorBehavior
+{
+ /// <summary>
+ /// The injector will try to inject reagent into things.
+ /// </summary>
+ Inject = 1 << 0,
+
+ /// <summary>
+ /// The injector will try to draw reagent from things.
+ /// </summary>
+ Draw = 1 << 1,
+
+ /// <summary>
+ /// The injector will draw from containers and inject into mobs.
+ /// </summary>
+ Dynamic = 1 << 2,
+}
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;
public override void Initialize()
{
- SubscribeLocalEvent<ClumsyComponent, SelfBeforeHyposprayInjectsEvent>(BeforeHyposprayEvent);
+ SubscribeLocalEvent<ClumsyComponent, SelfBeforeInjectEvent>(BeforeHyposprayEvent);
SubscribeLocalEvent<ClumsyComponent, SelfBeforeDefibrillatorZapsEvent>(BeforeDefibrillatorZapsEvent);
SubscribeLocalEvent<ClumsyComponent, SelfBeforeGunShotEvent>(BeforeGunShotEvent);
SubscribeLocalEvent<ClumsyComponent, CatchAttemptEvent>(OnCatchAttempt);
// If you add more clumsy interactions add them in this section!
#region Clumsy interaction events
- private void BeforeHyposprayEvent(Entity<ClumsyComponent> ent, ref SelfBeforeHyposprayInjectsEvent args)
+ private void BeforeHyposprayEvent(Entity<ClumsyComponent> ent, ref SelfBeforeInjectEvent args)
{
// Clumsy people sometimes inject themselves! Apparently syringes are clumsy proof...
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<ClumsyComponent> ent, ref SelfBeforeDefibrillatorZapsEvent args)
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;
public abstract partial class SharedPuddleSystem
{
private static readonly FixedPoint2 MeleeHitTransferProportion = 0.25;
+ [Dependency] private readonly InjectorSystem _injectorSystem = default!;
protected virtual void InitializeSpillable()
{
if (entity.Comp.SpillDelay == null)
{
var target = args.Target;
+ var user = args.User;
verb.Act = () =>
{
var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
// TODO: Make this an event subscription once spilling puddles is predicted.
// Injectors should not be hardcoded here.
- if (TryComp<InjectorComponent>(entity, out var injectorComp))
+ if (TryComp<InjectorComponent>(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);
}
};
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;
SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshNameModifiersEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, TransformSpeakerNameEvent>(RelayInventoryEvent);
- SubscribeLocalEvent<InventoryComponent, SelfBeforeHyposprayInjectsEvent>(RelayInventoryEvent);
- SubscribeLocalEvent<InventoryComponent, TargetBeforeHyposprayInjectsEvent>(RelayInventoryEvent);
+ SubscribeLocalEvent<InventoryComponent, SelfBeforeInjectEvent>(RelayInventoryEvent);
+ SubscribeLocalEvent<InventoryComponent, BeforeInjectTargetEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, SelfBeforeGunShotEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, SelfBeforeClimbEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, CoefficientQueryEvent>(RelayInventoryEvent);
+++ /dev/null
-## 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.
## 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!
--- /dev/null
+## 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
- SurgeryTool
- Dropper
components:
- - Hypospray
- Injector
- Pill
- HandLabeler
- Bottle
hypo:
whitelist:
- components:
+ tags:
- Hypospray
pill:
whitelist:
- 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.
- 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
- type: ExaminableSolution
solution: hypospray
exactVolume: true
- - type: Hypospray
- onlyAffectsMobs: false
- type: UseDelay
delay: 0.5
- type: StaticPrice
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.
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.
solution: hypospray
- type: ExaminableSolution
solution: hypospray
- - type: Hypospray
- onlyAffectsMobs: false
- type: UseDelay
delay: 0.5
delay: 0.0
- type: entity
- parent: BaseItem
+ parent: BaseHypospray
id: ChemicalMedipen
name: chemical medipen
description: A single-dose, non-refillable medipen.
- 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
maxFillLevels: 1
changeColor: false
emptySpriteName: bicpen_empty
- - type: Hypospray
- solutionName: pen
- transferAmount: 20
- onlyAffectsMobs: false
- injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
maxFillLevels: 1
changeColor: false
emptySpriteName: dermpen_empty
- - type: Hypospray
- solutionName: pen
- transferAmount: 20
- onlyAffectsMobs: false
- injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
maxFillLevels: 1
changeColor: false
emptySpriteName: arithpen_empty
- - type: Hypospray
- solutionName: pen
- transferAmount: 20
- onlyAffectsMobs: false
- injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
maxFillLevels: 1
changeColor: false
emptySpriteName: punctpen_empty
- - type: Hypospray
- solutionName: pen
- transferAmount: 15
- onlyAffectsMobs: false
- injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
maxFillLevels: 1
changeColor: false
emptySpriteName: pyrapen_empty
- - type: Hypospray
- solutionName: pen
- transferAmount: 20
- onlyAffectsMobs: false
- injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
maxFillLevels: 1
changeColor: false
emptySpriteName: dexpen_empty
- - type: Hypospray
- solutionName: pen
- transferAmount: 40
- onlyAffectsMobs: false
- injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
maxFillLevels: 1
changeColor: false
emptySpriteName: hypovolemic_empty
- - type: Hypospray
- solutionName: pen
- transferAmount: 30
- onlyAffectsMobs: false
- injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
maxFillLevels: 1
changeColor: false
emptySpriteName: stimpen_empty
- - type: Hypospray
- solutionName: pen
- transferAmount: 30
- onlyAffectsMobs: false
- injectOnly: true
- type: StaticPrice
price: 1500
Quantity: 25
- ReagentId: TranexamicAcid
Quantity: 5
- - type: Hypospray
- solutionName: pen
- transferAmount: 30
- onlyAffectsMobs: false
- injectOnly: true
- type: StaticPrice
price: 1500
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
reagents:
- ReagentId: JuiceThatMakesYouWeh
Quantity: 60
- - type: Hypospray
- solutionName: pen
- transferAmount: 1
- onlyAffectsMobs: false
- injectOnly: true
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
injector:
maxVol: 15
- type: Injector
- injectOnly: false
- transferAmounts:
- - 5
- - 10
- - 15
+ activeModeProtoId: SyringeDrawMode
+ allowedModes:
+ - SyringeDrawMode
+ - SyringeInjectMode
- type: ExaminableSolution
solution: injector
exactVolume: true
parent: BaseSyringe
id: Syringe
components:
- - type: Injector
- currentTransferAmount: 15
- type: Tag
tags:
- Syringe
solutions:
injector:
maxVol: 5
- - type: Injector
- transferAmounts:
- - 1
- - 2
- - 3
- - 4
- - 5
- currentTransferAmount: 5
- type: SolutionContainerVisuals
maxFillLevels: 3
fillBaseName: minisyringe
id: PrefilledSyringe
components:
- type: Injector
- toggleState: Inject
+ activeModeProtoId: SyringeInjectMode
- type: entity
id: SyringeBluespace
injector:
maxVol: 100
- type: Injector
- delay: 2.5
- injectOnly: false
+ activeModeProtoId: BluespaceSyringeDrawMode
+ allowedModes:
+ - BluespaceSyringeInjectMode
+ - BluespaceSyringeDrawMode
- type: SolutionContainerVisuals
maxFillLevels: 2
fillBaseName: syringe
maxVol: 10
canReact: false
- type: Injector
- injectOnly: false
- transferAmounts:
- - 5
- - 10
- currentTransferAmount: 10
+ activeModeProtoId: CryostasisSyringeDrawMode
+ allowedModes:
+ - CryostasisSyringeInjectMode
+ - CryostasisSyringeDrawMode
- type: Tag
tags:
- Syringe
- type: Tag
id: HudSecurity # ConstructionGraph: HudMedSec, GlassesSecHUD
+- type: Tag
+ id: Hypospray # ItemMapper: ClothingBeltMedical
+
## I ##
- type: Tag