using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
-using Robust.Shared.GameStates;
namespace Content.Client.Chemistry.EntitySystems;
public override void Initialize()
{
base.Initialize();
- Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainers));
+
+ Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
}
}
// 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.TransferAmount
+ && PrevTransferAmount == _parent.Comp.CurrentTransferAmount
&& PrevToggleState == _parent.Comp.ToggleState)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
- PrevTransferAmount = _parent.Comp.TransferAmount;
+ PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
PrevToggleState = _parent.Comp.ToggleState;
// Update current volume and injector state
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", modeStringLocalized),
- ("transferVolume", _parent.Comp.TransferAmount)));
+ ("transferVolume", _parent.Comp.CurrentTransferAmount)));
}
}
-using Content.Server.Body.Systems;
-using Content.Shared.Chemistry;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Components.SolutionManager;
+
using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Body.Components;
-using Content.Shared.Database;
-using Content.Shared.DoAfter;
-using Content.Shared.FixedPoint;
-using Content.Shared.Forensics;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction;
-using Content.Shared.Mobs.Components;
-using Content.Shared.Stacks;
-using Content.Shared.Nutrition.EntitySystems;
namespace Content.Server.Chemistry.EntitySystems;
-public sealed class InjectorSystem : SharedInjectorSystem
-{
- [Dependency] private readonly BloodstreamSystem _blood = default!;
- [Dependency] private readonly ReactiveSystem _reactiveSystem = default!;
- [Dependency] private readonly OpenableSystem _openable = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
- SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
- }
-
- 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 && SolutionContainers.TryGetInjectableSolution(target, out var injectableSolution, out _))
- return TryInject(injector, target, injectableSolution.Value, user, false);
-
- if (isOpenOrIgnored && SolutionContainers.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);
-
- Popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
- ("target", Identity.Entity(target, EntityManager))), injector, user);
- return false;
- }
-
- if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
- {
- // Draw from a bloodstream, if the target has that
- if (TryComp<BloodstreamComponent>(target, out var stream) &&
- SolutionContainers.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 && SolutionContainers.TryGetDrawableSolution(target, out var drawableSolution, out _))
- return TryDraw(injector, target, drawableSolution.Value, user);
-
- Popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
- ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
- return false;
- }
- return false;
- }
-
- private void OnInjectDoAfter(Entity<InjectorComponent> entity, ref InjectorDoAfterEvent args)
- {
- if (args.Cancelled || args.Handled || args.Args.Target == null)
- return;
-
- args.Handled = TryUseInjector(entity, args.Args.Target.Value, args.Args.User);
- }
-
- private void OnInjectorAfterInteract(Entity<InjectorComponent> entity, 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>(entity))
- 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 (entity.Comp.IgnoreMobs)
- return;
-
- InjectDoAfter(entity, target, args.User);
- args.Handled = true;
- return;
- }
-
- args.Handled = TryUseInjector(entity, target, args.User);
- }
-
- /// <summary>
- /// Send informative pop-up messages and wait for a do-after to complete.
- /// </summary>
- private void InjectDoAfter(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
- {
- // Create a pop-up for the user
- if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
- {
- Popup.PopupEntity(Loc.GetString("injector-component-drawing-user"), target, user);
- }
- else
- {
- Popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
- }
-
- if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _, 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.TransferAmount, (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.TransferAmount, 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.MinimumTransferAmount).Double();
-
- // Ensure that minimum delay before incapacitation checks is 1 seconds
- actualDelay = MathHelper.Max(actualDelay, TimeSpan.FromSeconds(1));
-
-
- var isTarget = user != target;
-
- if (isTarget)
- {
- // 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 (Combat.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.TransferAmount.ToString()} units from {ToPrettyString(target):target}");
- }
- }
- else
- {
- // 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.TransferAmount.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 TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
- EntityUid user)
- {
- // Get transfer amount. May be smaller than _transferAmount if not enough room
- if (!SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
- ref target.Comp.ChemicalSolution, out var chemSolution))
- {
- Popup.PopupEntity(
- Loc.GetString("injector-component-cannot-inject-message",
- ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
- return false;
- }
-
- var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, chemSolution.AvailableVolume);
- if (realTransferAmount <= 0)
- {
- Popup.PopupEntity(
- Loc.GetString("injector-component-cannot-inject-message",
- ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
- return false;
- }
-
- // Move units from attackSolution to targetSolution
- var removedSolution = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount);
-
- _blood.TryAddToChemicals(target.AsNullable(), removedSolution);
-
- _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
-
- Popup.PopupEntity(Loc.GetString("injector-component-inject-success-message",
- ("amount", removedSolution.Volume),
- ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-
- Dirty(injector);
- AfterInject(injector, target);
- return true;
- }
-
- private bool TryInject(Entity<InjectorComponent> injector, EntityUid targetEntity,
- Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
- {
- if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
- 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.TransferAmount, targetSolution.Comp.Solution.AvailableVolume);
-
- if (realTransferAmount <= 0)
- {
- Popup.PopupEntity(
- Loc.GetString("injector-component-target-already-full-message",
- ("target", Identity.Entity(targetEntity, EntityManager))),
- injector.Owner, user);
- return false;
- }
-
- // Move units from attackSolution to targetSolution
- Solution removedSolution;
- if (TryComp<StackComponent>(targetEntity, out var stack))
- removedSolution = SolutionContainers.SplitStackSolution(soln.Value, realTransferAmount, stack.Count);
- else
- removedSolution = SolutionContainers.SplitSolution(soln.Value, realTransferAmount);
-
- _reactiveSystem.DoEntityReaction(targetEntity, removedSolution, ReactionMethod.Injection);
-
- if (!asRefill)
- SolutionContainers.Inject(targetEntity, targetSolution, removedSolution);
- else
- SolutionContainers.Refill(targetEntity, targetSolution, removedSolution);
-
- Popup.PopupEntity(Loc.GetString("injector-component-transfer-success-message",
- ("amount", removedSolution.Volume),
- ("target", Identity.Entity(targetEntity, EntityManager))), injector.Owner, user);
-
- Dirty(injector);
- AfterInject(injector, targetEntity);
- return true;
- }
-
- private void AfterInject(Entity<InjectorComponent> injector, EntityUid target)
- {
- // Automatically set syringe to draw after completely draining it.
- if (SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _,
- out var solution) && solution.Volume == 0)
- {
- SetMode(injector, InjectorToggleMode.Draw);
- }
-
- // Leave some DNA from the injectee on it
- var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
- RaiseLocalEvent(target, ref ev);
- }
-
- private void AfterDraw(Entity<InjectorComponent> injector, EntityUid target)
- {
- // Automatically set syringe to inject after completely filling it.
- if (SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out _,
- out var solution) && solution.AvailableVolume == 0)
- {
- SetMode(injector, InjectorToggleMode.Inject);
- }
-
- // Leave some DNA from the drawee on it
- var ev = new TransferDnaEvent { Donor = target, Recipient = injector };
- RaiseLocalEvent(target, ref ev);
- }
-
- private bool TryDraw(Entity<InjectorComponent> injector, Entity<BloodstreamComponent?> target,
- Entity<SolutionComponent> targetSolution, EntityUid user)
- {
- if (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var soln,
- 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)
- {
- string[] reagentPrototypeWhitelistArray = new string[reagentWhitelist.Count];
- var i = 0;
- foreach (var reagent in reagentWhitelist)
- {
- reagentPrototypeWhitelistArray[i] = reagent;
- ++i;
- }
- temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentPrototypeWhitelistArray);
- }
-
- // 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.TransferAmount, applicableTargetSolution.Volume,
- solution.AvailableVolume);
-
- if (realTransferAmount <= 0)
- {
- Popup.PopupEntity(
- Loc.GetString("injector-component-target-is-empty-message",
- ("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), soln.Value, realTransferAmount, user);
- return true;
- }
-
- // Move units from attackSolution to targetSolution
- var removedSolution = SolutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
-
- // Add back non-whitelisted reagents to the target solution
- SolutionContainers.TryAddSolution(targetSolution, temporarilyRemovedSolution);
-
- if (!SolutionContainers.TryAddSolution(soln.Value, removedSolution))
- {
- return false;
- }
-
- Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
- ("amount", removedSolution.Volume),
- ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-
- Dirty(injector);
- 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 (SolutionContainers.ResolveSolution(target.Owner, target.Comp.ChemicalSolutionName,
- ref target.Comp.ChemicalSolution))
- {
- var chemTemp = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, drawAmount * 0.15f);
- SolutionContainers.TryAddSolution(injectorSolution, chemTemp);
- drawAmount -= (float) chemTemp.Volume;
- }
-
- if (SolutionContainers.ResolveSolution(target.Owner, target.Comp.BloodSolutionName,
- ref target.Comp.BloodSolution))
- {
- var bloodTemp = SolutionContainers.SplitSolution(target.Comp.BloodSolution.Value, drawAmount);
- SolutionContainers.TryAddSolution(injectorSolution, bloodTemp);
- }
-
- Popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
- ("amount", transferAmount),
- ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
-
- Dirty(injector);
- AfterDraw(injector, target);
- }
-}
+public sealed class InjectorSystem : SharedInjectorSystem;
component.Fingerprints.Add(fingerprint.Fingerprint ?? "");
}
+ // TODO: Delete this. A lot of systems are manually raising this method event instead of calling the identical <see cref="TransferDna"/> method.
+ // According to our code conventions we should not use method events.
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
{
if (component.DNA == null)
Dirty(ent);
}
- /// <summary>
- /// Transfer DNA from one entity onto the forensics of another
- /// </summary>
- /// <param name="recipient">The entity receiving the DNA</param>
- /// <param name="donor">The entity applying its DNA</param>
- /// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
- public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
+ public override void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
{
if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
{
namespace Content.Shared.Chemistry.Components;
-[Serializable, NetSerializable]
-public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent
-{
-}
-
/// <summary>
/// Implements draw/inject behavior for droppers and syringes.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class InjectorComponent : Component
{
+ /// <summary>
+ /// The solution to draw into or inject from.
+ /// </summary>
[DataField]
public string SolutionName = "injector";
+ /// <summary>
+ /// A cached reference to the solution.
+ /// </summary>
+ [ViewVariables]
+ 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.
public bool InjectOnly;
/// <summary>
- /// Whether or not the injector is able to draw from or inject from mobs
+ /// Whether or not the injector is able to draw from or inject from mobs.
/// </summary>
/// <remarks>
- /// for example: droppers would ignore mobs
+ /// For example: droppers would ignore mobs.
/// </remarks>
[DataField]
public bool IgnoreMobs;
/// <summary>
- /// Whether or not the injector is able to draw from or inject into containers that are closed/sealed
+ /// Whether or not the injector is able to draw from or inject into containers that are closed/sealed.
/// </summary>
/// <remarks>
- /// for example: droppers can not inject into cans, but syringes can
+ /// For example: droppers can not inject into cans, but syringes can.
/// </remarks>
[DataField]
public bool IgnoreClosed = true;
/// <summary>
- /// The minimum amount of solution that can be transferred at once from this solution.
- /// </summary>
- [DataField("minTransferAmount")]
- public FixedPoint2 MinimumTransferAmount = FixedPoint2.New(5);
-
- /// <summary>
- /// The maximum amount of solution that can be transferred at once from this solution.
+ /// The transfer amounts for the set-transfer verb.
/// </summary>
- [DataField("maxTransferAmount")]
- public FixedPoint2 MaximumTransferAmount = FixedPoint2.New(15);
+ [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 TransferAmount = FixedPoint2.New(5);
+ [DataField, AutoNetworkedField]
+ public FixedPoint2 CurrentTransferAmount = FixedPoint2.New(5);
/// <summary>
/// Injection delay (seconds) when the target is a mob.
/// right SolutionCaps to support injection/drawing. For InjectOnly injectors this should
/// only ever be set to Inject
/// </summary>
- [AutoNetworkedField]
- [DataField]
+ [DataField, AutoNetworkedField]
public InjectorToggleMode ToggleState = InjectorToggleMode.Draw;
/// <summary>
/// <summary>
/// Possible modes for an <see cref="InjectorComponent"/>.
/// </summary>
+[Serializable, NetSerializable]
public enum InjectorToggleMode : byte
{
/// <summary>
/// <summary>
/// The injector will try to draw reagent from things.
/// </summary>
- Draw
+ Draw,
}
+
+/// <summary>
+/// Raised on the injector when the doafter has finished.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class InjectorDoAfterEvent : SimpleDoAfterEvent;
+using System.Linq;
using Content.Shared.Administration.Logs;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.CombatMode;
+using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
+using Content.Shared.Forensics.Systems;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
+using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
+using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
+using Content.Shared.Stacks;
using Content.Shared.Verbs;
-using Robust.Shared.Player;
namespace Content.Shared.Chemistry.EntitySystems;
public abstract class SharedInjectorSystem : EntitySystem
{
- /// <summary>
- /// Default transfer amounts for the set-transfer verb.
- /// </summary>
- public static readonly FixedPoint2[] TransferAmounts = { 1, 5, 10, 15 };
-
- [Dependency] protected readonly SharedPopupSystem Popup = default!;
- [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainers = default!;
- [Dependency] protected readonly MobStateSystem MobState = default!;
- [Dependency] protected readonly SharedCombatModeSystem Combat = default!;
- [Dependency] protected readonly SharedDoAfterSystem DoAfter = default!;
- [Dependency] protected readonly ISharedAdminLogManager AdminLogger = default!;
+ [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, ComponentStartup>(OnInjectorStartup);
SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
+ SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
+ SubscribeLocalEvent<InjectorComponent, InjectorDoAfterEvent>(OnInjectDoAfter);
}
- private void AddSetTransferVerbs(Entity<InjectorComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
+ 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 (_, component) = entity;
- var min = component.MinimumTransferAmount;
- var max = component.MaximumTransferAmount;
- var cur = component.TransferAmount;
+ 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;
Category = VerbCategory.SetTransferAmount,
Act = () =>
{
- component.TransferAmount = toggleAmount;
- Popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user);
- Dirty(entity);
+ ent.Comp.CurrentTransferAmount = toggleAmount;
+ _popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", toggleAmount)), user, user);
+ Dirty(ent);
},
Priority = priority
priority -= 1;
- // Add specific transfer verbs according to the container's size
- foreach (var amount in TransferAmounts)
+ // Add specific transfer verbs for amounts defined in the component
+ foreach (var amount in ent.Comp.TransferAmounts)
{
- if (amount < component.MinimumTransferAmount || amount > component.MaximumTransferAmount)
- continue;
-
AlternativeVerb verb = new()
{
Text = Loc.GetString("comp-solution-transfer-verb-amount", ("amount", amount)),
Category = VerbCategory.SetTransferAmount,
Act = () =>
{
- component.TransferAmount = amount;
- Popup.PopupClient(Loc.GetString("comp-solution-transfer-set-amount", ("amount", amount)), user, user);
- Dirty(entity);
+ 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.
}
}
- private void OnInjectorStartup(Entity<InjectorComponent> entity, ref ComponentStartup args)
+ private void OnInjectorUse(Entity<InjectorComponent> ent, ref UseInHandEvent args)
{
- // ???? why ?????
- Dirty(entity);
+ if (args.Handled)
+ return;
+
+ Toggle(ent, args.User);
+ args.Handled = true;
}
- private void OnInjectorUse(Entity<InjectorComponent> entity, ref UseInHandEvent args)
+ private void OnInjectorAfterInteract(Entity<InjectorComponent> ent, ref AfterInteractEvent args)
{
- if (args.Handled)
+ if (args.Handled || !args.CanReach)
return;
- Toggle(entity, args.User);
- args.Handled = true;
+ //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>
- /// Toggle between draw/inject state if applicable
+ /// Send informative pop-up messages and wait for a do-after to complete.
/// </summary>
- private void Toggle(Entity<InjectorComponent> injector, EntityUid user)
+ private void InjectDoAfter(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
+ {
+ // Create a pop-up for the user
+ if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
+ {
+ _popup.PopupClient(Loc.GetString("injector-component-drawing-user"), target, user);
+ }
+ else
+ {
+ _popup.PopupClient(Loc.GetString("injector-component-injecting-user"), target, user);
+ }
+
+ if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution))
+ return;
+
+ var actualDelay = injector.Comp.Delay;
+ FixedPoint2 amountToInject;
+ if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
+ {
+ // additional delay is based on actual volume left to draw in syringe when smaller than transfer amount
+ amountToInject = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, solution.MaxVolume - solution.Volume);
+ }
+ else
+ {
+ // additional delay is based on actual volume left to inject in syringe when smaller than transfer amount
+ amountToInject = FixedPoint2.Min(injector.Comp.CurrentTransferAmount, solution.Volume);
+ }
+
+ // Injections take 0.5 seconds longer per 5u of possible space/content
+ // First 5u(MinimumTransferAmount) doesn't incur delay
+ actualDelay += injector.Comp.DelayPerVolume * FixedPoint2.Max(0, amountToInject - injector.Comp.TransferAmounts.Min()).Double();
+
+ // Ensure that minimum delay before incapacitation checks is 1 seconds
+ actualDelay = MathHelper.Max(actualDelay, TimeSpan.FromSeconds(1));
+
+ if (user != target) // injecting someone else
+ {
+ // Create a pop-up for the target
+ var userName = Identity.Entity(user, EntityManager);
+ if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
+ {
+ _popup.PopupEntity(Loc.GetString("injector-component-drawing-target",
+ ("user", userName)), user, target);
+ }
+ else
+ {
+ _popup.PopupEntity(Loc.GetString("injector-component-injecting-target",
+ ("user", userName)), user, target);
+ }
+
+
+ // Check if the target is incapacitated or in combat mode and modify time accordingly.
+ if (_mobState.IsIncapacitated(target))
+ {
+ actualDelay /= 2.5f;
+ }
+ else if (_combatMode.IsInCombatMode(target))
+ {
+ // Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
+ // combat with fast syringes & lag.
+ actualDelay += TimeSpan.FromSeconds(1);
+ }
+
+ // Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
+ if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
+ {
+ _adminLogger.Add(LogType.ForceFeed,
+ $"{ToPrettyString(user):user} is attempting to inject {ToPrettyString(target):target} with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}");
+ }
+ else
+ {
+ _adminLogger.Add(LogType.ForceFeed,
+ $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.CurrentTransferAmount.ToString()} units from {ToPrettyString(target):target}");
+ }
+ }
+ else // injecting yourself
+ {
+ // Self-injections take half as long.
+ actualDelay /= 2;
+
+ if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
+ {
+ _adminLogger.Add(LogType.Ingestion,
+ $"{ToPrettyString(user):user} is attempting to inject themselves with a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution}.");
+ }
+ else
+ {
+ _adminLogger.Add(LogType.ForceFeed,
+ $"{ToPrettyString(user):user} is attempting to draw {injector.Comp.CurrentTransferAmount.ToString()} units from themselves.");
+ }
+ }
+
+ _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, user, actualDelay, new InjectorDoAfterEvent(), injector.Owner, target: target, used: injector.Owner)
+ {
+ BreakOnMove = true,
+ BreakOnWeightlessMove = false,
+ BreakOnDamage = true,
+ NeedHand = injector.Comp.NeedHand,
+ BreakOnHandChange = injector.Comp.BreakOnHandChange,
+ MovementThreshold = injector.Comp.MovementThreshold,
+ });
+ }
+
+ private bool TryUseInjector(Entity<InjectorComponent> injector, EntityUid target, EntityUid user)
+ {
+ var isOpenOrIgnored = injector.Comp.IgnoreClosed || !_openable.IsClosed(target);
+ // Handle injecting/drawing for solutions
+ if (injector.Comp.ToggleState == InjectorToggleMode.Inject)
+ {
+ if (isOpenOrIgnored && SolutionContainer.TryGetInjectableSolution(target, out var injectableSolution, out _))
+ return TryInject(injector, target, injectableSolution.Value, user, false);
+
+ if (isOpenOrIgnored && SolutionContainer.TryGetRefillableSolution(target, out var refillableSolution, out _))
+ return TryInject(injector, target, refillableSolution.Value, user, true);
+
+ if (TryComp<BloodstreamComponent>(target, out var bloodstream))
+ return TryInjectIntoBloodstream(injector, (target, bloodstream), user);
+
+ LocId msg = target == user ? "injector-component-cannot-transfer-message-self" : "injector-component-cannot-transfer-message";
+ _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector, user);
+ }
+ else if (injector.Comp.ToggleState == InjectorToggleMode.Draw)
+ {
+ // Draw from a bloodstream, if the target has that
+ if (TryComp<BloodstreamComponent>(target, out var stream) &&
+ SolutionContainer.ResolveSolution(target, stream.BloodSolutionName, ref stream.BloodSolution))
+ {
+ return TryDraw(injector, (target, stream), stream.BloodSolution.Value, user);
+ }
+
+ // Draw from an object (food, beaker, etc)
+ if (isOpenOrIgnored && SolutionContainer.TryGetDrawableSolution(target, out var drawableSolution, out _))
+ return TryDraw(injector, target, drawableSolution.Value, user);
+
+ LocId msg = target == user ? "injector-component-cannot-draw-message-self" : "injector-component-cannot-draw-message";
+ _popup.PopupClient(Loc.GetString(msg, ("target", Identity.Entity(target, EntityManager))), injector.Owner, user);
+ }
+ return false;
+ }
+
+ private bool TryInject(Entity<InjectorComponent> injector, EntityUid target,
+ Entity<SolutionComponent> targetSolution, EntityUid user, bool asRefill)
+ {
+ if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution,
+ out var solution) || solution.Volume == 0)
+ return false;
+
+ // Get transfer amount. May be smaller than _transferAmount if not enough room
+ var realTransferAmount =
+ FixedPoint2.Min(injector.Comp.CurrentTransferAmount, targetSolution.Comp.Solution.AvailableVolume);
+
+ if (realTransferAmount <= 0)
+ {
+ LocId msg = target == user ? "injector-component-target-already-full-message-self" : "injector-component-target-already-full-message";
+ _popup.PopupClient(
+ Loc.GetString(msg,
+ ("target", Identity.Entity(target, EntityManager))),
+ injector.Owner,
+ user);
+ return false;
+ }
+
+ // Move units from attackSolution to targetSolution
+ Solution removedSolution;
+ if (TryComp<StackComponent>(target, out var stack))
+ removedSolution = SolutionContainer.SplitStackSolution(injector.Comp.Solution.Value, realTransferAmount, stack.Count);
+ else
+ removedSolution = SolutionContainer.SplitSolution(injector.Comp.Solution.Value, realTransferAmount);
+
+ _reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
+
+ if (!asRefill)
+ SolutionContainer.Inject(target, targetSolution, removedSolution);
+ else
+ SolutionContainer.Refill(target, targetSolution, removedSolution);
+
+ LocId msgSuccess = target == user ? "injector-component-transfer-success-message-self" : "injector-component-transfer-success-message";
+ _popup.PopupClient(
+ Loc.GetString(msgSuccess,
+ ("amount", removedSolution.Volume),
+ ("target", Identity.Entity(target, EntityManager))),
+ injector.Owner, user);
+
+ AfterInject(injector, target);
+ return true;
+ }
+
+ private bool TryInjectIntoBloodstream(Entity<InjectorComponent> injector, Entity<BloodstreamComponent> target,
+ EntityUid user)
+ {
+ // Get transfer amount. May be smaller than _transferAmount if not enough room
+ if (!SolutionContainer.ResolveSolution(target.Owner, target.Comp.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());
+ }
+
+ // 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 (!SolutionContainers.TryGetSolution(injector.Owner, injector.Comp.SolutionName, out var solEnt, out var solution))
+ if (!SolutionContainer.ResolveSolution(injector.Owner, injector.Comp.SolutionName, ref injector.Comp.Solution, out var solution))
return;
string msg;
throw new ArgumentOutOfRangeException();
}
- Popup.PopupClient(Loc.GetString(msg), injector, user);
+ _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;
var puddleSolution = _solutionContainerSystem.SplitSolution(soln.Value, solution.Volume);
TrySpillAt(Transform(target).Coordinates, puddleSolution, out _);
+ // TODO: Make this an event subscription once spilling puddles is predicted.
+ // Injectors should not be hardcoded here.
if (TryComp<InjectorComponent>(entity, out var injectorComp))
{
injectorComp.ToggleState = InjectorToggleMode.Draw;
/// Does nothing if it does not have the FingerprintComponent.
/// </summary>
public virtual void RandomizeFingerprint(Entity<FingerprintComponent?> ent) { }
+
+ /// <summary>
+ /// Transfer DNA from one entity onto the forensics of another.
+ /// </summary>
+ /// <param name="recipient">The entity receiving the DNA.</param>
+ /// <param name="donor">The entity applying its DNA.</param>
+ /// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood.</param>
+ public virtual void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true) { }
+
}
injector-component-drawing-text = Now drawing
injector-component-injecting-text = Now injecting
-injector-component-cannot-transfer-message = You aren't able to transfer to {THE($target)}!
+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-inject-message = You aren't able to inject to {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-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.
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!
injectOnly: false
ignoreMobs: true
ignoreClosed: false
- minTransferAmount: 1
- maxTransferAmount: 5
- transferAmount: 1
- toggleState: 1 # draw
+ transferAmounts:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ currentTransferAmount: 1
- type: ExaminableSolution
solution: dropper
exactVolume: true
id: Syringe
components:
- type: Injector
- transferAmount: 15
- toggleState: Draw
+ currentTransferAmount: 15
- type: Tag
tags:
- Syringe
injector:
maxVol: 5
- type: Injector
- minTransferAmount: 1
- maxTransferAmount: 5
- transferAmount: 5
+ transferAmounts:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ currentTransferAmount: 5
- type: SolutionContainerVisuals
maxFillLevels: 3
fillBaseName: minisyringe
- SyringeGunAmmo
- type: entity
+ abstract: true
parent: BaseSyringe
id: PrefilledSyringe
components:
canReact: false
- type: Injector
injectOnly: false
- minTransferAmount: 5
- maxTransferAmount: 10
- transferAmount: 10
+ transferAmounts:
+ - 5
+ - 10
+ currentTransferAmount: 10
- type: Tag
tags:
- Syringe
- Trash
-
- type: entity
name: pill
parent: BaseItem
# 2025-08-11
ClothingUniformJumpsuitChiefEngineerNT: ClothingUniformJumpsuitChiefEngineer
ClothingUniformJumpsuitParamedicNT: ClothingUniformJumpsuitParamedic
+
+# 2025-08-29
+PrefilledSyringe: Syringe