Merge Injector & Hypospray Systems & Components (#41833)

* Merge Injector & Hyposprays

* Fixes

* Requested Changes

* Preview

* Inclusion of Prototypes

* Fix

* small oversight

* Further fixes

* A few more fixes & Bluespacesyringe buff

Co-Authored-By: āda <177162775+iaada@users.noreply.github.com>

* Final Commit, hopefully

* Merge conflict no more

* YML fix

* Add required changes

Co-Authored-By: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>

* cleanup warnings removal

* Bug fix & Maintainer Requests

Co-Authored-By: āda <177162775+iaada@users.noreply.github.com>

* Adhere to requested changes

Co-Authored-By: āda <177162775+iaada@users.noreply.github.com>

---------

Co-authored-by: āda <177162775+iaada@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
Sir Warock 2025-12-21 00:58:26 +01:00 committed by Vanessa
parent 95cac43684
commit 08dfaf0dd5
28 changed files with 1297 additions and 1349 deletions

View File

@ -1,16 +0,0 @@
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class HyposprayStatusControlSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<HyposprayComponent>(ent => new HyposprayStatusControl(ent, _solutionContainers));
}
}

View File

@ -0,0 +1,20 @@
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class InjectorStatusControlSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<InjectorComponent>(injector => new InjectorStatusControl(injector, _solutionContainers, _prototypeManager));
}
}

View File

@ -1,16 +0,0 @@
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class InjectorSystem : SharedInjectorSystem
{
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
}
}

View File

@ -1,58 +0,0 @@
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.Chemistry.UI;
public sealed class HyposprayStatusControl : Control
{
private readonly Entity<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)));
}
}

View File

@ -2,26 +2,32 @@ using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Chemistry.UI;
public sealed class InjectorStatusControl : Control
{
private readonly IPrototypeManager _prototypeManager;
private readonly Entity<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 } };
@ -32,33 +38,38 @@ public sealed class InjectorStatusControl : Control
{
base.FrameUpdate(args);
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution)
|| !_prototypeManager.Resolve(_parent.Comp.ActiveModeProtoId, out var activeMode))
return;
// only updates the UI if any of the details are different than they previously were
if (PrevVolume == solution.Volume
&& PrevMaxVolume == solution.MaxVolume
&& PrevTransferAmount == _parent.Comp.CurrentTransferAmount
&& PrevToggleState == _parent.Comp.ToggleState)
if (_prevVolume == solution.Volume
&& _prevMaxVolume == solution.MaxVolume
&& _prevTransferAmount == _parent.Comp.CurrentTransferAmount
&& _prevBehavior == activeMode.Behavior)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
PrevToggleState = _parent.Comp.ToggleState;
_prevVolume = solution.Volume;
_prevMaxVolume = solution.MaxVolume;
_prevTransferAmount = _parent.Comp.CurrentTransferAmount;
_prevBehavior = activeMode.Behavior;
// Update current volume and injector state
var modeStringLocalized = Loc.GetString(_parent.Comp.ToggleState switch
// Seeing transfer volume is only important for injectors that can change it.
if (activeMode.TransferAmounts.Count > 1 && _parent.Comp.CurrentTransferAmount.HasValue)
{
InjectorToggleMode.Draw => "injector-draw-text",
InjectorToggleMode.Inject => "injector-inject-text",
_ => "injector-invalid-injector-toggle-mode"
});
_label.SetMarkup(Loc.GetString("injector-volume-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", modeStringLocalized),
("transferVolume", _parent.Comp.CurrentTransferAmount)));
_label.SetMarkup(Loc.GetString("injector-volume-transfer-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", Loc.GetString(activeMode.Name)),
("transferVolume", _parent.Comp.CurrentTransferAmount.Value)));
}
else
{
_label.SetMarkup(Loc.GetString("injector-volume-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", Loc.GetString(activeMode.Name))));
}
}
}

View File

@ -1,5 +0,0 @@
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Server.Chemistry.EntitySystems;
public sealed class InjectorSystem : SharedInjectorSystem;

View File

@ -586,12 +586,12 @@ public sealed partial class DeepFryerSystem : SharedDeepfryerSystem
if (!_solutionContainerSystem.TryGetSolution(component.Owner, component.Solution.Name, out var solution))
return;
_solutionTransferSystem.Transfer(user,
_solutionTransferSystem.Transfer(new SolutionTransferData(user,
uid,
solution.Value,
heldItem.Value,
heldSolution.Value,
transferAmount);
transferAmount));
// UI update is not necessary here, because the solution change event handles it.
}

View File

@ -32,7 +32,7 @@ public sealed class RecruiterPenSystem : SharedRecruiterPenSystem
return;
}
if (_transfer.Transfer(user, user, blood, uid, dest, desired) != desired)
if (_transfer.Transfer(new SolutionTransferData(user, user, blood, uid, dest, desired)) != desired)
return;
// this is why you have to keep the pen safe, it has the dna of everyone you recruited!

View File

@ -1,60 +0,0 @@
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;
}

View File

@ -1,10 +1,10 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Prototypes;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry.Components;
@ -14,11 +14,10 @@ namespace Content.Shared.Chemistry.Components;
/// <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>
@ -34,74 +33,52 @@ public sealed partial class InjectorComponent : Component
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, AutoNetworkedField]
public FixedPoint2? CurrentTransferAmount = FixedPoint2.New(5);
/// <summary>
/// The mode that this injector starts with on MapInit.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public ProtoId<InjectorModePrototype> ActiveModeProtoId;
/// <summary>
/// The possible <see cref="InjectorModePrototype"/> that it can switch between.
/// </summary>
[DataField(required: true)]
public List<ProtoId<InjectorModePrototype>> AllowedModes;
/// <summary>
/// Whether the injector is able to draw from or inject from mobs.
/// </summary>
/// <example>
/// Droppers ignore mobs.
/// </example>
[DataField]
public bool IgnoreMobs;
/// <summary>
/// Whether or not the injector is able to draw from or inject into containers that are closed/sealed.
/// Whether 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>
/// <example>
/// Droppers can't inject into closed cans.
/// </example>
[DataField]
public bool IgnoreClosed = true;
/// <summary>
/// The transfer amounts for the set-transfer verb.
/// </summary>
[DataField]
public List<FixedPoint2> TransferAmounts = new() { 1, 5, 10, 15 };
/// <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.
/// </summary>
[DataField, AutoNetworkedField]
public FixedPoint2 CurrentTransferAmount = FixedPoint2.New(5);
/// <summary>
/// Injection delay (seconds) when the target is a mob.
/// </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>
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(5);
/// <summary>
/// Each additional 1u after first 5u increases the delay by X seconds.
/// </summary>
[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;
/// <summary>
/// Reagents that are allowed to be within this injector.
/// If a solution has both allowed and non-allowed reagents, only allowed reagents will be drawn into this injector.
/// A null ReagentWhitelist indicates all reagents are allowed.
/// </summary>
[DataField]
public List<ProtoId<ReagentPrototype>>? ReagentWhitelist = null;
public List<ProtoId<ReagentPrototype>>? ReagentWhitelist;
/// <summary>
/// DeltaV - If set to true, this injector will only target the smallest reagent in the solution.
@ -112,40 +89,25 @@ public sealed partial class InjectorComponent : Component
#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;

View File

@ -1,329 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Administration.Logs;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Forensics;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Events;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry.EntitySystems;
public sealed class HypospraySystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<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 {}

View File

@ -0,0 +1,772 @@
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.Chemistry.Reagent;
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());
}
// Begin DeltaV Additions - skimmer functionality
else if (injector.Comp.TargetSmallest && applicableTargetSolution.Any())
{
var smallest = applicableTargetSolution.MinBy(soln => soln.Quantity);
ProtoId<ReagentPrototype> smallestReagent = smallest.Reagent.Prototype;
temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, smallestReagent);
}
// End DeltaV Additions - skimmer functionality
// 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
}

View File

@ -1,562 +0,0 @@
using System.Linq;
using Content.Shared._DV.Chemistry.Components; // DeltaV
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.Chemistry.Reagent; // DeltaV - Skimmer
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;
using Robust.Shared.Prototypes; // DeltaV - Skimmer
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)
{
if (HasComp<BlockInjectionComponent>(target)) // DeltaV
{
_popup.PopupClient(Loc.GetString("injector-component-deny-user"), target, user);
return;
}
// 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.ChemicalSolutionName,
ref target.Comp.ChemicalSolution, out var chemSolution))
{
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, chemSolution.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.ChemicalSolution.Value, realTransferAmount);
_blood.TryAddToChemicals(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());
}
// Begin DeltaV Additions - skimmer functionality
else if (injector.Comp.TargetSmallest && applicableTargetSolution.Any())
{
var smallest = applicableTargetSolution.MinBy(soln => soln.Quantity);
temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, (ProtoId<ReagentPrototype>)smallest.Reagent.Prototype);
}
// End DeltaV Additions - skimmer functionality
// 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)
{
var drawAmount = (float)transferAmount;
if (SolutionContainer.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
ref target.Comp.ChemicalSolution))
{
var chemTemp = SolutionContainer.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f);
SolutionContainer.TryAddSolution(injectorSolution, chemTemp);
drawAmount -= (float)chemTemp.Volume;
}
if (SolutionContainer.ResolveSolution(target.Owner, target.Comp.BloodSolutionName,
ref target.Comp.BloodSolution))
{
var bloodTemp = SolutionContainer.SplitSolution(target.Comp.BloodSolution.Value, drawAmount);
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);
}
}

View File

@ -1,38 +0,0 @@
using Content.Shared.Inventory;
namespace Content.Shared.Chemistry.Hypospray.Events;
public abstract partial class BeforeHyposprayInjectsTargetEvent : CancellableEntityEventArgs, IInventoryRelayEvent
{
public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET;
public EntityUid EntityUsingHypospray;
public readonly EntityUid Hypospray;
public EntityUid TargetGettingInjected;
public string? InjectMessageOverride;
public BeforeHyposprayInjectsTargetEvent(EntityUid user, EntityUid hypospray, EntityUid target)
{
EntityUsingHypospray = user;
Hypospray = hypospray;
TargetGettingInjected = target;
InjectMessageOverride = null;
}
}
/// <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) { }
}

View File

@ -0,0 +1,43 @@
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);

View File

@ -0,0 +1,145 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
namespace Content.Shared.Chemistry.Prototypes;
/// <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,
}

View File

@ -1,5 +1,5 @@
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Chemistry.Events;
using Content.Shared.Climbing.Components;
using Content.Shared.Climbing.Events;
using Content.Shared.Damage;
@ -32,7 +32,7 @@ public sealed class ClumsySystem : EntitySystem
public override void Initialize()
{
SubscribeLocalEvent<ClumsyComponent, SelfBeforeHyposprayInjectsEvent>(BeforeHyposprayEvent);
SubscribeLocalEvent<ClumsyComponent, SelfBeforeInjectEvent>(BeforeHyposprayEvent);
SubscribeLocalEvent<ClumsyComponent, SelfBeforeDefibrillatorZapsEvent>(BeforeDefibrillatorZapsEvent);
SubscribeLocalEvent<ClumsyComponent, SelfBeforeGunShotEvent>(BeforeGunShotEvent);
SubscribeLocalEvent<ClumsyComponent, CatchAttemptEvent>(OnCatchAttempt);
@ -41,7 +41,7 @@ public sealed class ClumsySystem : EntitySystem
// If you add more clumsy interactions add them in this section!
#region Clumsy interaction events
private void BeforeHyposprayEvent(Entity<ClumsyComponent> ent, ref SelfBeforeHyposprayInjectsEvent args)
private void BeforeHyposprayEvent(Entity<ClumsyComponent> ent, ref SelfBeforeInjectEvent args)
{
// Clumsy people sometimes inject themselves! Apparently syringes are clumsy proof...
@ -55,9 +55,9 @@ public sealed class ClumsySystem : EntitySystem
if (!rand.Prob(ent.Comp.ClumsyDefaultCheck))
return;
args.TargetGettingInjected = args.EntityUsingHypospray;
args.InjectMessageOverride = Loc.GetString(ent.Comp.HypoFailedMessage);
_audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingHypospray);
args.TargetGettingInjected = args.EntityUsingInjector;
args.OverrideMessage = Loc.GetString(ent.Comp.HypoFailedMessage);
_audio.PlayPredicted(ent.Comp.ClumsySound, ent, args.EntityUsingInjector);
}
private void BeforeDefibrillatorZapsEvent(Entity<ClumsyComponent> ent, ref SelfBeforeDefibrillatorZapsEvent args)

View File

@ -2,6 +2,7 @@ using Content.Shared._DV.Chemistry.Systems; // DeltaV - Beer Goggles Safe Throw
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;
@ -24,6 +25,7 @@ namespace Content.Shared.Fluids;
public abstract partial class SharedPuddleSystem
{
private static readonly FixedPoint2 MeleeHitTransferProportion = 0.25;
[Dependency] private readonly InjectorSystem _injectorSystem = default!;
[Dependency] protected readonly SafeSolutionThrowerSystem _safeSolutionThrower = default!; // DeltaV - Beer Goggles Safe Throw
@ -72,6 +74,7 @@ public abstract partial class SharedPuddleSystem
if (entity.Comp.SpillDelay == null)
{
var target = args.Target;
var user = args.User;
verb.Act = () =>
{
var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
@ -79,9 +82,21 @@ public abstract partial class SharedPuddleSystem
// TODO: Make this an event subscription once spilling puddles is predicted.
// Injectors should not be hardcoded here.
if (TryComp<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);
}
};

View File

@ -3,7 +3,7 @@ using Content.Shared.Armor;
using Content.Shared.Atmos;
using Content.Shared.Chat;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Chemistry.Events;
using Content.Shared.Climbing.Events;
using Content.Shared.Contraband;
using Content.Shared.Damage;
@ -32,6 +32,7 @@ using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Wieldable;
using Content.Shared.Zombies;
namespace Content.Shared.Inventory;
public partial class InventorySystem
@ -48,8 +49,8 @@ public partial class InventorySystem
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);

View File

@ -7,4 +7,8 @@ namespace Content.Shared._DV.Chemistry.Components;
/// Hyposprays are unaffected.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class BlockInjectionComponent : Component;
public sealed partial class BlockInjectionComponent : Component
{
[DataField]
public LocId ReasonLocId = "injector-component-deny-user-chitnid";
}

View File

@ -0,0 +1 @@
injector-component-deny-user-chitnid = Exoskeleton too thick!

View File

@ -1,20 +0,0 @@
## UI
hypospray-all-mode-text = Only Injects
hypospray-mobs-only-mode-text = Draws and Injects
hypospray-invalid-text = Invalid
hypospray-volume-label = Volume: [color=white]{$currentVolume}/{$totalVolume}u[/color]
Mode: [color=white]{$modeString}[/color]
## Entity
hypospray-component-inject-other-message = You inject {THE($other)}.
hypospray-component-inject-self-message = You inject yourself.
hypospray-component-empty-message = Nothing to inject.
hypospray-component-feel-prick-message = You feel a tiny prick!
hypospray-component-transfer-already-full-message = {$owner} is already full!
hypospray-cant-inject = Can't inject into {$target}!
hypospray-verb-mode-label = Toggle Container Draw
hypospray-verb-mode-inject-all = You cannot draw from containers anymore.
hypospray-verb-mode-inject-mobs-only = You can now draw from containers.

View File

@ -1,38 +1,49 @@
## 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-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!
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-deny-user = Exoskeleton too thick!
## Target Popup Success messages
injector-component-feel-prick-message = You feel a tiny prick!

View File

@ -0,0 +1,118 @@
## Abstracts
- type: injectorMode
abstract: true
id: BaseInjectMode
name: injector-component-inject-mode-name
behavior: Inject
popupUserAttempt: injector-component-needle-injecting-user
popupTargetAttempt: injector-component-needle-injecting-target
- type: injectorMode
abstract: true
id: BaseDrawMode
name: injector-component-draw-mode-name
behavior: Draw
popupUserAttempt: injector-component-needle-drawing-user
popupTargetAttempt: injector-component-needle-drawing-target
- type: injectorMode
abstract: true
id: BaseDynamicMode
name: injector-component-dynamic-mode-name
behavior: Dynamic
popupUserAttempt: injector-component-needle-injecting-user
popupTargetAttempt: injector-component-needle-injecting-target
## Syringes
- type: injectorMode
abstract: true
id: BaseSyringeMode
transferAmounts:
- 5
- 10
- 15
- type: injectorMode
abstract: true
id: BaseCryostasisSyringeMode
transferAmounts:
- 5
- 10
- type: injectorMode
abstract: true
id: BaseBluespaceSyringeMode
mobTime: 2.5
transferAmounts:
- 5
- 10
- 15
- 50
- type: injectorMode
parent: [ BaseSyringeMode, BaseInjectMode ]
id: SyringeInjectMode
- type: injectorMode
parent: [ BaseSyringeMode, BaseDrawMode ]
id: SyringeDrawMode
- type: injectorMode
parent: [ BaseBluespaceSyringeMode, BaseInjectMode ]
id: BluespaceSyringeInjectMode
- type: injectorMode
parent: [ BaseBluespaceSyringeMode, BaseDrawMode ]
id: BluespaceSyringeDrawMode
- type: injectorMode
parent: [ BaseCryostasisSyringeMode, BaseInjectMode ]
id: CryostasisSyringeInjectMode
- type: injectorMode
parent: [ BaseCryostasisSyringeMode, BaseDrawMode ]
id: CryostasisSyringeDrawMode
## Dropper
- type: injectorMode
abstract: true
id: BaseDropperMode
transferAmounts:
- 1
- 2
- 3
- 4
- 5
- type: injectorMode
parent: [ BaseDropperMode, BaseInjectMode ]
id: DropperInjectMode
- type: injectorMode
parent: [ BaseDropperMode, BaseDrawMode ]
id: DropperDrawMode
## Hyposprays
- type: injectorMode
abstract: true
id: HyposprayBaseMode
injectSound: /Audio/Items/hypospray.ogg
injectPopupTarget: injector-component-feel-prick-message
injectOnUse: true
mobTime: 0
delayPerVolume: 0
transferAmounts:
- 5
- type: injectorMode
parent: [ HyposprayBaseMode, BaseInjectMode ]
id: HyposprayInjectMode
- type: injectorMode
parent: [ HyposprayBaseMode, BaseDynamicMode ]
id: HyposprayDynamicMode
- type: injectorMode
parent: HyposprayDynamicMode
id: HypopenDynamicMode
containerDrawTime: 0.75

View File

@ -186,7 +186,6 @@
- Vial # DeltaV
- Syringe # DeltaV
components:
- Hypospray
- Injector
- Pill
- HandLabeler
@ -200,7 +199,7 @@
- Bottle
hypo:
whitelist:
components:
tags:
- Hypospray
pill:
whitelist:

View File

@ -1,4 +1,19 @@
- type: entity
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
parent: [BaseItem, BaseGrandTheftContraband]
description: A sterile injector for rapid administration of drugs to patients.
@ -7,11 +22,11 @@
- type: Sprite
sprite: Objects/Specific/Medical/hypospray.rsi
layers:
- state: hypo
map: ["enum.SolutionContainerLayers.Base"]
- state: hypo_fill1
map: ["enum.SolutionContainerLayers.Fill"]
visible: false
- state: hypo
map: ["enum.SolutionContainerLayers.Base"]
- state: hypo_fill1
map: ["enum.SolutionContainerLayers.Fill"]
visible: false
- type: Item
sprite: Objects/Specific/Medical/hypospray.rsi
- type: SolutionContainerManager
@ -23,8 +38,6 @@
- type: ExaminableSolution
solution: hypospray
exactVolume: true
- type: Hypospray
onlyAffectsMobs: false
- type: UseDelay
delay: 0.5
- type: StaticPrice
@ -41,6 +54,8 @@
solutionName: hypospray
- type: entity
parent: [BaseHypospray, BaseSyndicateContraband]
id: SyndiHypo
name: gorlex hypospray
parent: BaseItem
description: Using reverse engineered designs from NT, Cybersun produced these in limited quantities for Gorlex Marauder operatives.
@ -60,22 +75,14 @@
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: BaseHypospray
id: BorgHypo
name: borghypo
parent: BaseItem
description: A sterile injector for rapid administration of drugs to patients. A cheaper and more specialised version for medical borgs.
@ -94,8 +101,6 @@
solution: hypospray
- type: ExaminableSolution
solution: hypospray
- type: Hypospray
onlyAffectsMobs: false
- type: UseDelay
delay: 0.5
@ -114,6 +119,8 @@
delay: 0.0
- type: entity
parent: BaseHypospray
id: ChemicalMedipen
name: chemical medipen
parent: BaseItem
description: A sterile injector for rapid administration of drugs to patients. This one can't be refilled.
@ -145,11 +152,12 @@
- type: ExaminableSolution
solution: pen
exactVolume: true
- type: Hypospray
- type: Injector
solutionName: pen
transferAmount: 15
onlyAffectsMobs: false
injectOnly: true
currentTransferAmount: null
activeModeProtoId: HyposprayInjectMode
allowedModes:
- HyposprayInjectMode
- type: Appearance
- type: SolutionContainerVisuals
maxFillLevels: 1
@ -256,11 +264,6 @@
maxFillLevels: 1
changeColor: false
emptySpriteName: bicpen_empty
- type: Hypospray
solutionName: pen
transferAmount: 20
onlyAffectsMobs: false
injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
@ -298,11 +301,6 @@
maxFillLevels: 1
changeColor: false
emptySpriteName: dermpen_empty
- type: Hypospray
solutionName: pen
transferAmount: 20
onlyAffectsMobs: false
injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
@ -340,11 +338,6 @@
maxFillLevels: 1
changeColor: false
emptySpriteName: arithpen_empty
- type: Hypospray
solutionName: pen
transferAmount: 20
onlyAffectsMobs: false
injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
@ -382,11 +375,6 @@
maxFillLevels: 1
changeColor: false
emptySpriteName: punctpen_empty
- type: Hypospray
solutionName: pen
transferAmount: 15
onlyAffectsMobs: false
injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
@ -424,11 +412,6 @@
maxFillLevels: 1
changeColor: false
emptySpriteName: pyrapen_empty
- type: Hypospray
solutionName: pen
transferAmount: 20
onlyAffectsMobs: false
injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
@ -466,11 +449,6 @@
maxFillLevels: 1
changeColor: false
emptySpriteName: dexpen_empty
- type: Hypospray
solutionName: pen
transferAmount: 40
onlyAffectsMobs: false
injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
@ -511,11 +489,6 @@
maxFillLevels: 1
changeColor: false
emptySpriteName: hypovolemic_empty
- type: Hypospray
solutionName: pen
transferAmount: 30
onlyAffectsMobs: false
injectOnly: true
- type: SolutionContainerManager
solutions:
pen:
@ -562,11 +535,6 @@
maxFillLevels: 1
changeColor: false
emptySpriteName: stimpen_empty
- type: Hypospray
solutionName: pen
transferAmount: 30
onlyAffectsMobs: false
injectOnly: true
- type: StaticPrice
price: 1500
@ -645,11 +613,6 @@
Quantity: 25
- ReagentId: TranexamicAcid
Quantity: 5
- type: Hypospray
solutionName: pen
transferAmount: 30
onlyAffectsMobs: false
injectOnly: true
- type: StaticPrice
price: 1500
@ -672,9 +635,12 @@
solution: hypospray
heldOnly: true # Allow examination only when held in hand.
exactVolume: true
- type: Hypospray
onlyAffectsMobs: false
drawTime: 1.25
- type: Injector
solutionName: hypospray
activeModeProtoId: HypopenDynamicMode
allowedModes:
- HyposprayInjectMode
- HypopenDynamicMode
- type: UseDelay
delay: 0.5
- type: StaticPrice # A new shitcurity meta
@ -721,8 +687,3 @@
reagents:
- ReagentId: JuiceThatMakesYouWeh
Quantity: 60
- type: Hypospray
solutionName: pen
transferAmount: 1
onlyAffectsMobs: false
injectOnly: true

View File

@ -294,16 +294,12 @@
injector:
maxVol: 5
- type: Injector
injectOnly: false
ignoreMobs: true
ignoreClosed: false
transferAmounts:
- 1
- 2
- 3
- 4
- 5
currentTransferAmount: 1
activeModeProtoId: DropperDrawMode
allowedModes:
- DropperDrawMode
- DropperInjectMode
- type: ExaminableSolution
solution: dropper
exactVolume: true
@ -376,11 +372,10 @@
injector:
maxVol: 15
- type: Injector
injectOnly: false
transferAmounts:
- 5
- 10
- 15
activeModeProtoId: SyringeDrawMode
allowedModes:
- SyringeDrawMode
- SyringeInjectMode
- type: ExaminableSolution
solution: injector
exactVolume: true
@ -402,8 +397,6 @@
parent: BaseSyringe
id: Syringe
components:
- type: Injector
currentTransferAmount: 15
- type: Tag
tags:
- Syringe
@ -426,14 +419,6 @@
solutions:
injector:
maxVol: 5
- type: Injector
transferAmounts:
- 1
- 2
- 3
- 4
- 5
currentTransferAmount: 5
- type: SolutionContainerVisuals
maxFillLevels: 3
fillBaseName: minisyringe
@ -487,7 +472,7 @@
id: PrefilledSyringe
components:
- type: Injector
toggleState: Inject
activeModeProtoId: SyringeInjectMode
- type: entity
id: SyringeBluespace
@ -510,8 +495,10 @@
injector:
maxVol: 100
- type: Injector
delay: 2.5
injectOnly: false
activeModeProtoId: BluespaceSyringeDrawMode
allowedModes:
- BluespaceSyringeInjectMode
- BluespaceSyringeDrawMode
- type: SolutionContainerVisuals
maxFillLevels: 2
fillBaseName: syringe
@ -546,11 +533,10 @@
maxVol: 10
canReact: false
- type: Injector
injectOnly: false
transferAmounts:
- 5
- 10
currentTransferAmount: 10
activeModeProtoId: CryostasisSyringeDrawMode
allowedModes:
- CryostasisSyringeInjectMode
- CryostasisSyringeDrawMode
- type: Tag
tags:
- Syringe

View File

@ -821,6 +821,9 @@
- type: Tag
id: HudSecurity # ConstructionGraph: HudMedSec, GlassesSecHUD
- type: Tag
id: Hypospray # ItemMapper: ClothingBeltMedical
## I ##
- type: Tag