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:
parent
95cac43684
commit
08dfaf0dd5
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
using Content.Shared.Chemistry.EntitySystems;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems;
|
||||
|
||||
public sealed class InjectorSystem : SharedInjectorSystem;
|
||||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) { }
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
injector-component-deny-user-chitnid = Exoskeleton too thick!
|
||||
|
|
@ -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.
|
||||
|
|
@ -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!
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -821,6 +821,9 @@
|
|||
- type: Tag
|
||||
id: HudSecurity # ConstructionGraph: HudMedSec, GlassesSecHUD
|
||||
|
||||
- type: Tag
|
||||
id: Hypospray # ItemMapper: ClothingBeltMedical
|
||||
|
||||
## I ##
|
||||
|
||||
- type: Tag
|
||||
|
|
|
|||
Loading…
Reference in New Issue