Add injectors + injected reagent metabolism via BloodstreamCompo… (#730)

* Add BloodstreamComponent and BloodstreamSystem

New component for metabolizing reagents that other organs like the stomach pass their input reagents to.

* Change StomachComponent to put ingested reagents in bloodstream after delay

Now StomachComponent does not metabolize any reagents. Instead, it tracks how long each reagent has been inside it, and once they pass "digestionDelay" they'll be put inside the bloodstream, where the bloodstream will handle metabolism of the reagent.

* Add reagent injectors

Injects reagents straight into the bloodstream when used on mobs with bloodstreams. Also allows draw/inject from beakers. Does not support drawing blood/reagents from the bloodstream yet.

* Address code review

Make use of `Loc` static class instead of using `ILocalizationManager`. Localize InjectorToggleMode enum properly.
This commit is contained in:
moneyl
2020-02-23 19:47:33 -05:00
committed by GitHub
parent d8291ed9c4
commit 1b7860aeda
16 changed files with 648 additions and 41 deletions

View File

@@ -131,7 +131,8 @@ namespace Content.Client
"UseDelay",
"Pourable",
"Paper",
"Write"
"Write",
"Bloodstream"
};
foreach (var ignoreName in registerIgnore)

View File

@@ -0,0 +1,80 @@
using Content.Client.UserInterface;
using Content.Client.Utility;
using Robust.Shared.Timing;
using Content.Shared.GameObjects.Components.Chemistry;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.ViewVariables;
namespace Content.Client.GameObjects.Components.Chemistry
{
/// <summary>
/// Client behavior for injectors & syringes. Used for item status on injectors
/// </summary>
[RegisterComponent]
public class InjectorComponent : SharedInjectorComponent, IItemStatus
{
[ViewVariables] private int CurrentVolume { get; set; }
[ViewVariables] private int TotalVolume { get; set; }
[ViewVariables] private InjectorToggleMode CurrentMode { get; set; }
[ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
//Add/remove item status code
Control IItemStatus.MakeControl() => new StatusControl(this);
void IItemStatus.DestroyControl(Control control) { }
//Handle net updates
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
{
var cast = (InjectorComponentState)curState;
if (cast != null)
{
CurrentVolume = cast.CurrentVolume;
TotalVolume = cast.TotalVolume;
CurrentMode = cast.CurrentMode;
_uiUpdateNeeded = true;
}
}
/// <summary>
/// Item status control for injectors
/// </summary>
private sealed class StatusControl : Control
{
private readonly InjectorComponent _parent;
private readonly RichTextLabel _label;
public StatusControl(InjectorComponent parent)
{
_parent = parent;
_label = new RichTextLabel { StyleClasses = { NanoStyle.StyleClassItemStatus } };
AddChild(_label);
parent._uiUpdateNeeded = true;
}
protected override void Update(FrameEventArgs args)
{
base.Update(args);
if (!_parent._uiUpdateNeeded)
{
return;
}
_parent._uiUpdateNeeded = false;
//Update current volume and injector state
var modeStringLocalized = _parent.CurrentMode switch
{
InjectorToggleMode.Draw => Loc.GetString("Draw"),
InjectorToggleMode.Inject => Loc.GetString("Inject"),
_ => Loc.GetString("Invalid")
};
_label.SetMarkup(Loc.GetString("Volume: [color=white]{0}/{1}[/color] | [color=white]{2}[/color]",
_parent.CurrentVolume, _parent.TotalVolume, modeStringLocalized));
}
}
}
}

View File

@@ -0,0 +1,239 @@
using System;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Chemistry;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Chemistry
{
/// <summary>
/// Server behavior for reagent injectors and syringes. 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.
/// </summary>
[RegisterComponent]
public class InjectorComponent : SharedInjectorComponent, IAfterAttack, IUse
{
#pragma warning disable 649
[Dependency] private readonly IServerNotifyManager _notifyManager;
#pragma warning restore 649
/// <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>
[ViewVariables]
private bool _injectOnly;
/// <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>
[ViewVariables]
private int _transferAmount;
/// <summary>
/// Initial storage volume of the injector
/// </summary>
[ViewVariables]
private int _initialMaxVolume;
/// <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>
[ViewVariables(VVAccess.ReadWrite)]
private InjectorToggleMode _toggleState;
/// <summary>
/// Internal solution container
/// </summary>
[ViewVariables]
private SolutionComponent _internalContents;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _injectOnly, "injectOnly", false);
serializer.DataField(ref _initialMaxVolume, "initialMaxVolume", 15);
serializer.DataField(ref _transferAmount, "transferAmount", 5);
}
public override void Initialize()
{
base.Initialize();
//Create and setup internal storage
_internalContents = new SolutionComponent();
_internalContents.InitializeFromPrototype();
_internalContents.Init();
_internalContents.MaxVolume = _initialMaxVolume;
_internalContents.Owner = Owner; //Manually set owner to avoid crash when VV'ing this
_internalContents.Capabilities |= SolutionCaps.Injector;
//Set _toggleState based on prototype
_toggleState = _injectOnly ? InjectorToggleMode.Inject : InjectorToggleMode.Draw;
}
/// <summary>
/// Toggle between draw/inject state if applicable
/// </summary>
private void Toggle()
{
if (_injectOnly)
{
return;
}
_toggleState = _toggleState switch
{
InjectorToggleMode.Inject => InjectorToggleMode.Draw,
InjectorToggleMode.Draw => InjectorToggleMode.Inject,
_ => throw new ArgumentOutOfRangeException()
};
Dirty();
}
/// <summary>
/// Called when clicking on entities while holding in active hand
/// </summary>
/// <param name="eventArgs"></param>
void IAfterAttack.AfterAttack(AfterAttackEventArgs eventArgs)
{
//Make sure we have the attacking entity
if (eventArgs.Attacked == null || !_internalContents.Injector)
{
return;
}
var targetEntity = eventArgs.Attacked;
//Handle injecting/drawing for solutions
if (targetEntity.TryGetComponent<SolutionComponent>(out var targetSolution) && targetSolution.Injectable)
{
if (_toggleState == InjectorToggleMode.Inject)
{
TryInject(targetSolution, eventArgs.User);
}
else if (_toggleState == InjectorToggleMode.Draw)
{
TryDraw(targetSolution, eventArgs.User);
}
}
else //Handle injecting into bloodstream
{
if (targetEntity.TryGetComponent<BloodstreamComponent>(out var bloodstream) && _toggleState == InjectorToggleMode.Inject)
{
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
}
}
}
/// <summary>
/// Called when use key is pressed when held in active hand
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
{
Toggle();
return true;
}
private void TryInjectIntoBloodstream(BloodstreamComponent targetBloodstream, IEntity user)
{
if (_internalContents.CurrentVolume == 0)
{
return;
}
//Get transfer amount. May be smaller than _transferAmount if not enough room
int realTransferAmount = Math.Min(_transferAmount, targetBloodstream.EmptyVolume);
if (realTransferAmount <= 0)
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
Loc.GetString("Container full."));
return;
}
//Move units from attackSolution to targetSolution
var removedSolution = _internalContents.SplitSolution(realTransferAmount);
if (!targetBloodstream.TryTransferSolution(removedSolution))
{
return;
}
_notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
Loc.GetString("Injected {0}u", removedSolution.TotalVolume));
Dirty();
}
private void TryInject(SolutionComponent targetSolution, IEntity user)
{
if (_internalContents.CurrentVolume == 0)
{
return;
}
//Get transfer amount. May be smaller than _transferAmount if not enough room
int realTransferAmount = Math.Min(_transferAmount, targetSolution.EmptyVolume);
if (realTransferAmount <= 0)
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
Loc.GetString("Container full."));
return;
}
//Move units from attackSolution to targetSolution
var removedSolution = _internalContents.SplitSolution(realTransferAmount);
if (!targetSolution.TryAddSolution(removedSolution))
{
return;
}
_notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
Loc.GetString("Injected {0}u", removedSolution.TotalVolume));
Dirty();
}
private void TryDraw(SolutionComponent targetSolution, IEntity user)
{
if (_internalContents.EmptyVolume == 0)
{
return;
}
//Get transfer amount. May be smaller than _transferAmount if not enough room
int realTransferAmount = Math.Min(_transferAmount, targetSolution.CurrentVolume);
if (realTransferAmount <= 0)
{
_notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
Loc.GetString("Container empty"));
return;
}
//Move units from attackSolution to targetSolution
var removedSolution = targetSolution.SplitSolution(realTransferAmount);
if (!_internalContents.TryAddSolution(removedSolution))
{
return;
}
_notifyManager.PopupMessage(Owner.Transform.GridPosition, user,
Loc.GetString("Drew {0}u", removedSolution.TotalVolume));
Dirty();
}
public override ComponentState GetComponentState()
{
return new InjectorComponentState(_internalContents.CurrentVolume, _internalContents.MaxVolume, _toggleState);
}
}
}

View File

@@ -35,7 +35,11 @@ namespace Content.Server.GameObjects.Components.Chemistry
protected override void Startup()
{
base.Startup();
Init();
}
public void Init()
{
_reactions = _prototypeManager.EnumeratePrototypes<ReactionPrototype>();
_audioSystem = _entitySystemManager.GetEntitySystem<AudioSystem>();
}

View File

@@ -0,0 +1,116 @@
using System.Linq;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Chemistry;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Metabolism
{
/// <summary>
/// Handles all metabolism for mobs. All delivery methods eventually bring reagents
/// to the bloodstream. For example, injectors put reagents directly into the bloodstream,
/// and the stomach does with some delay.
/// </summary>
[RegisterComponent]
public class BloodstreamComponent : Component
{
#pragma warning disable 649
[Dependency] private readonly IPrototypeManager _prototypeManager;
#pragma warning restore 649
public override string Name => "Bloodstream";
/// <summary>
/// Internal solution for reagent storage
/// </summary>
[ViewVariables]
private SolutionComponent _internalSolution;
/// <summary>
/// Max volume of internal solution storage
/// </summary>
[ViewVariables]
private int _initialMaxVolume;
/// <summary>
/// Empty volume of internal solution
/// </summary>
public int EmptyVolume => _internalSolution.EmptyVolume;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _initialMaxVolume, "maxVolume", 250);
}
public override void Initialize()
{
base.Initialize();
//Create and setup internal solution storage
_internalSolution = new SolutionComponent();
_internalSolution.InitializeFromPrototype();
_internalSolution.Init();
_internalSolution.MaxVolume = _initialMaxVolume;
_internalSolution.Owner = Owner; //Manually set owner to avoid crash when VV'ing this
}
/// <summary>
/// Attempt to transfer provided solution to internal solution. Only supports complete transfers
/// </summary>
/// <param name="solution">Solution to be transferred</param>
/// <returns>Whether or not transfer was a success</returns>
public bool TryTransferSolution(Solution solution)
{
//For now doesn't support partial transfers
if (solution.TotalVolume + _internalSolution.CurrentVolume > _internalSolution.MaxVolume)
{
return false;
}
_internalSolution.TryAddSolution(solution, false, true);
return true;
}
/// <summary>
/// Loops through each reagent in _internalSolution, and calls the IMetabolizable for each of them./>
/// </summary>
/// <param name="tickTime">The time since the last metabolism tick in seconds.</param>
private void Metabolize(float tickTime)
{
if (_internalSolution.CurrentVolume == 0)
{
return;
}
//Run metabolism for each reagent, remove metabolized reagents
foreach (var reagent in _internalSolution.ReagentList.ToList()) //Using ToList here lets us edit reagents while iterating
{
if (!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
{
continue;
}
//Run metabolism code for each reagent
foreach (var metabolizable in proto.Metabolism)
{
int reagentDelta = metabolizable.Metabolize(Owner, reagent.ReagentId, tickTime);
_internalSolution.TryRemoveReagent(reagent.ReagentId, reagentDelta);
}
}
}
/// <summary>
/// Triggers metabolism of the reagents inside _internalSolution. Called by <see cref="BloodstreamSystem"/>
/// </summary>
/// <param name="tickTime">The time since the last metabolism tick in seconds.</param>
public void OnUpdate(float tickTime)
{
Metabolize(tickTime);
}
}
}

View File

@@ -1,40 +1,74 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.GameObjects.Components.Metabolism;
using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Nutrition;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Nutrition
{
/// <summary>
/// Where reagents go when ingested. Tracks ingested reagents over time, and
/// eventually transfers them to <see cref="BloodstreamComponent"/> once digested.
/// </summary>
[RegisterComponent]
public class StomachComponent : SharedStomachComponent
{
#pragma warning disable 649
[Dependency] private readonly IPrototypeManager _prototypeManager;
[Dependency] private readonly ILocalizationManager _localizationManager;
#pragma warning restore 649
[ViewVariables(VVAccess.ReadOnly)]
private SolutionComponent _stomachContents;
/// <summary>
/// Max volume of internal solution storage
/// </summary>
public int MaxVolume
{
get => _stomachContents.MaxVolume;
set => _stomachContents.MaxVolume = value;
}
/// <summary>
/// Internal solution storage
/// </summary>
[ViewVariables]
private SolutionComponent _stomachContents;
/// <summary>
/// Initial internal solution storage volume
/// </summary>
[ViewVariables]
private int _initialMaxVolume;
//Used to track changes to reagent amounts during metabolism
private readonly Dictionary<string, int> _reagentDeltas = new Dictionary<string, int>();
/// <summary>
/// Time in seconds between reagents being ingested and them being transferred to <see cref="BloodstreamComponent"/>
/// </summary>
[ViewVariables]
private float _digestionDelay;
/// <summary>
/// Used to track how long each reagent has been in the stomach
/// </summary>
private readonly List<ReagentDelta> _reagentDeltas = new List<ReagentDelta>();
/// <summary>
/// Reference to bloodstream where digested reagents are transferred to
/// </summary>
private BloodstreamComponent _bloodstream;
public override void ExposeData(ObjectSerializer serializer)
{
base.ExposeData(serializer);
serializer.DataField(ref _initialMaxVolume, "max_volume", 20);
serializer.DataField(ref _initialMaxVolume, "maxVolume", 100);
serializer.DataField(ref _digestionDelay, "digestionDelay", 20);
}
public override void Initialize()
{
base.Initialize();
@@ -43,6 +77,14 @@ namespace Content.Server.GameObjects.Components.Nutrition
_stomachContents.InitializeFromPrototype();
_stomachContents.MaxVolume = _initialMaxVolume;
_stomachContents.Owner = Owner; //Manually set owner to avoid crash when VV'ing this
//Ensure bloodstream in present
if (!Owner.TryGetComponent<BloodstreamComponent>(out _bloodstream))
{
Logger.Warning(_localizationManager.GetString(
"StomachComponent entity does not have a BloodstreamComponent, which is required for it to function. Owner entity name: {0}",
Owner.Name));
}
}
public bool TryTransferSolution(Solution solution)
@@ -52,47 +94,64 @@ namespace Content.Server.GameObjects.Components.Nutrition
{
return false;
}
//Add solution to _stomachContents
_stomachContents.TryAddSolution(solution, false, true);
//Add each reagent to _reagentDeltas. Used to track how long each reagent has been in the stomach
foreach (var reagent in solution.Contents)
{
_reagentDeltas.Add(new ReagentDelta(reagent.ReagentId, reagent.Quantity));
}
return true;
}
/// <summary>
/// Loops through each reagent in _stomachContents, and calls the IMetabolizable for each of them./>
/// Updates digestion status of ingested reagents. Once reagents surpass _digestionDelay
/// they are moved to the bloodstream, where they are then metabolized.
/// </summary>
/// <param name="tickTime">The time since the last metabolism tick in seconds.</param>
public void Metabolize(float tickTime)
/// <param name="tickTime">The time since the last update in seconds.</param>
public void OnUpdate(float tickTime)
{
if (_bloodstream == null)
{
if (_stomachContents.CurrentVolume == 0)
return;
//Run metabolism for each reagent, track quantity changes
_reagentDeltas.Clear();
foreach (var reagent in _stomachContents.ReagentList)
{
if(!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto))
continue;
foreach (var metabolizable in proto.Metabolism)
{
_reagentDeltas[reagent.ReagentId] = metabolizable.Metabolize(Owner, reagent.ReagentId, tickTime);
}
}
//Apply changes to quantity afterwards. Can't change the reagent quantities while the iterating the
//list of reagents, because that would invalidate the iterator and throw an exception.
foreach (var reagentDelta in _reagentDeltas)
//Add reagents ready for transfer to bloodstream to transferSolution
var transferSolution = new Solution();
foreach (var delta in _reagentDeltas.ToList()) //Use ToList here to remove entries while iterating
{
_stomachContents.TryRemoveReagent(reagentDelta.Key, reagentDelta.Value);
//Increment lifetime of reagents
delta.Increment(tickTime);
if (delta.Lifetime > _digestionDelay)
{
_stomachContents.TryRemoveReagent(delta.ReagentId, delta.Quantity);
transferSolution.AddReagent(delta.ReagentId, delta.Quantity);
_reagentDeltas.Remove(delta);
}
}
//Transfer digested reagents to bloodstream
_bloodstream.TryTransferSolution(transferSolution);
}
/// <summary>
/// Triggers metabolism of the reagents inside _stomachContents. Called by <see cref="StomachSystem"/>
/// Used to track quantity changes when ingesting & digesting reagents
/// </summary>
/// <param name="tickTime">The time since the last metabolism tick in seconds.</param>
public void OnUpdate(float tickTime)
private class ReagentDelta
{
Metabolize(tickTime);
public readonly string ReagentId;
public readonly int Quantity;
public float Lifetime { get; private set; }
public ReagentDelta(string reagentId, int quantity)
{
ReagentId = reagentId;
Quantity = quantity;
Lifetime = 0.0f;
}
public void Increment(float delta) => Lifetime += delta;
}
}
}

View File

@@ -0,0 +1,35 @@
using Content.Server.GameObjects.Components.Metabolism;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// Triggers metabolism updates for <see cref="BloodstreamComponent"/>
/// </summary>
[UsedImplicitly]
public class BloodstreamSystem : EntitySystem
{
private float _accumulatedFrameTime;
public override void Initialize()
{
EntityQuery = new TypeEntityQuery(typeof(BloodstreamComponent));
}
public override void Update(float frameTime)
{
//Trigger metabolism updates at most once per second
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime > 1.0f)
{
foreach (var entity in RelevantEntities)
{
var comp = entity.GetComponent<BloodstreamComponent>();
comp.OnUpdate(_accumulatedFrameTime);
}
_accumulatedFrameTime = 0.0f;
}
}
}
}

View File

@@ -1,10 +1,13 @@
using Content.Server.GameObjects.Components.Nutrition;
using Content.Server.GameObjects.Components.Nutrition;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
namespace Content.Server.GameObjects.EntitySystems
{
/// <summary>
/// Triggers digestion updates on <see cref="StomachComponent"/>
/// </summary>
[UsedImplicitly]
public class StomachSystem : EntitySystem
{
@@ -16,8 +19,8 @@ namespace Content.Server.GameObjects.EntitySystems
public override void Update(float frameTime)
{
//Update at most once per second
_accumulatedFrameTime += frameTime;
// TODO: Potential performance improvement (e.g. going through say 1/5th the entities every tick)
if (_accumulatedFrameTime > 1.0f)
{
foreach (var entity in RelevantEntities)

View File

@@ -0,0 +1,39 @@
using System;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
namespace Content.Shared.GameObjects.Components.Chemistry
{
/// <summary>
/// Shared class for injectors & syringes
/// </summary>
public class SharedInjectorComponent : Component
{
public override string Name => "Injector";
public sealed override uint? NetID => ContentNetIDs.REAGENT_INJECTOR;
/// <summary>
/// Component data used for net updates. Used by client for item status ui
/// </summary>
[Serializable, NetSerializable]
protected sealed class InjectorComponentState : ComponentState
{
public int CurrentVolume { get; }
public int TotalVolume { get; }
public InjectorToggleMode CurrentMode { get; }
public InjectorComponentState(int currentVolume, int totalVolume, InjectorToggleMode currentMode) : base(ContentNetIDs.REAGENT_INJECTOR)
{
CurrentVolume = currentVolume;
TotalVolume = totalVolume;
CurrentMode = currentMode;
}
}
protected enum InjectorToggleMode
{
Inject,
Draw
}
}
}

View File

@@ -74,6 +74,14 @@ namespace Content.Shared.GameObjects.Components.Chemistry
/// Shortcut for Capabilities PourOut flag to avoid binary operators.
/// </summary>
public bool CanPourOut => (Capabilities & SolutionCaps.PourOut) != 0;
/// <summary>
/// Shortcut for Capabilities Injectable flag
/// </summary>
public bool Injectable => (Capabilities & SolutionCaps.Injectable) != 0;
/// <summary>
/// Shortcut for Capabilities Injector flag
/// </summary>
public bool Injector => (Capabilities & SolutionCaps.Injector) != 0;
/// <inheritdoc />
public override string Name => "Solution";

View File

@@ -1,8 +1,10 @@
using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Shared.GameObjects.Components.Nutrition
{
/// <summary>
/// Shared class for stomach components
/// </summary>
public class SharedStomachComponent : Component
{
public override string Name => "Stomach";

View File

@@ -40,5 +40,6 @@
public const uint STACK = 1035;
public const uint HANDHELD_LIGHT = 1036;
public const uint PAPER = 1037;
public const uint REAGENT_INJECTOR = 1038;
}
}

View File

@@ -10,7 +10,7 @@
texture: Objects/Chemistry/chemicals.rsi/beaker.png
- type: Solution
maxVol: 50
caps: 19
caps: 27
- type: Pourable
transferAmount: 5
@@ -26,7 +26,7 @@
texture: Objects/Chemistry/chemicals.rsi/beakerlarge.png
- type: Solution
maxVol: 100
caps: 19
caps: 27
- type: Pourable
transferAmount: 5
@@ -45,3 +45,19 @@
caps: 19
- type: Pourable
transferAmount: 5
- type: entity
name: Syringe
parent: BaseItem
description: Used to draw blood samples from mobs, or to inject them with reagents
id: Syringe
components:
- type: Sprite
texture: Objects/Chemistry/chemicals.rsi/syringeproj.png
- type: Icon
texture: Objects/Chemistry/chemicals.rsi/syringeproj.png
- type: Solution
maxVol: 15
caps: 19
- type: Injector
injectOnly: false

View File

@@ -14,6 +14,10 @@
- type: Thirst
# Organs
- type: Stomach
maxVolume: 100
digestionDelay: 20
- type: Bloodstream
maxVolume: 250
- type: Inventory
- type: Constructor

View File

@@ -1 +1 @@
{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from https://github.com/discordia-space/CEV-Eris/blob/2b969adc2dfd3e9621bf3597c5cbffeb3ac8c9f0/icons/obj/chemical.dmi", "states": [{"name": "beaker", "directions": 1, "delays": [[1.0]]}, {"name": "beakerbluespace", "directions": 1, "delays": [[0.1, 0.1]]}, {"name": "beakerlarge", "directions": 1, "delays": [[1.0]]}, {"name": "beakernoreact", "directions": 1, "delays": [[0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05]]}, {"name": "bottle", "directions": 1, "delays": [[1.0]]}, {"name": "bottle-1", "directions": 1, "delays": [[1.0]]}, {"name": "bottle-2", "directions": 1, "delays": [[1.0]]}, {"name": "bottle-3", "directions": 1, "delays": [[1.0]]}, {"name": "bottle-4", "directions": 1, "delays": [[1.0]]}, {"name": "bottle1", "directions": 1, "delays": [[1.0]]}, {"name": "bottle10", "directions": 1, "delays": [[1.0]]}, {"name": "bottle11", "directions": 1, "delays": [[1.0]]}, {"name": "bottle12", "directions": 1, "delays": [[1.0]]}, {"name": "bottle13", "directions": 1, "delays": [[1.0]]}, {"name": "bottle14", "directions": 1, "delays": [[1.0]]}, {"name": "bottle15", "directions": 1, "delays": [[1.0]]}, {"name": "bottle16", "directions": 1, "delays": [[1.0]]}, {"name": "bottle17", "directions": 1, "delays": [[1.0]]}, {"name": "bottle18", "directions": 1, "delays": [[1.0]]}, {"name": "bottle19", "directions": 1, "delays": [[1.0]]}, {"name": "bottle2", "directions": 1, "delays": [[1.0]]}, {"name": "bottle20", "directions": 1, "delays": [[1.0]]}, {"name": "bottle3", "directions": 1, "delays": [[1.0]]}, {"name": "bottle4", "directions": 1, "delays": [[1.0]]}, {"name": "bottle5", "directions": 1, "delays": [[1.0]]}, {"name": "bottle6", "directions": 1, "delays": [[1.0]]}, {"name": "bottle7", "directions": 1, "delays": [[1.0]]}, {"name": "bottle8", "directions": 1, "delays": [[1.0]]}, {"name": "bottle9", "directions": 1, "delays": [[1.0]]}, {"name": "chemg", "directions": 1, "delays": [[1.0]]}, {"name": "chemg_armed", "directions": 1, "delays": [[0.1, 0.2]]}, {"name": "chemg_ass", "directions": 1, "delays": [[1.0]]}, {"name": "chemg_locked", "directions": 1, "delays": [[1.0]]}, {"name": "chempuff", "directions": 4, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1]]}, {"name": "dropper", "directions": 1, "delays": [[1.0]]}, {"name": "large_grenade", "directions": 1, "delays": [[1.0]]}, {"name": "large_grenade_armed", "directions": 1, "delays": [[0.1, 0.1]]}, {"name": "large_grenade_ass", "directions": 1, "delays": [[1.0]]}, {"name": "large_grenade_locked", "directions": 1, "delays": [[1.0]]}, {"name": "lid_beaker", "directions": 1, "delays": [[1.0]]}, {"name": "lid_beakerlarge", "directions": 1, "delays": [[1.0]]}, {"name": "lid_beakernoreact", "directions": 1, "delays": [[1.0]]}, {"name": "lid_bottle", "directions": 1, "delays": [[1.0]]}, {"name": "lid_vial", "directions": 1, "delays": [[1.0]]}, {"name": "molten", "directions": 1, "delays": [[1.0]]}, {"name": "pill", "directions": 1, "delays": [[1.0]]}, {"name": "pill1", "directions": 1, "delays": [[1.0]]}, {"name": "pill10", "directions": 1, "delays": [[1.0]]}, {"name": "pill11", "directions": 1, "delays": [[1.0]]}, {"name": "pill12", "directions": 1, "delays": [[1.0]]}, {"name": "pill13", "directions": 1, "delays": [[1.0]]}, {"name": "pill14", "directions": 1, "delays": [[1.0]]}, {"name": "pill15", "directions": 1, "delays": [[1.0]]}, {"name": "pill16", "directions": 1, "delays": [[1.0]]}, {"name": "pill17", "directions": 1, "delays": [[1.0]]}, {"name": "pill18", "directions": 1, "delays": [[1.0]]}, {"name": "pill19", "directions": 1, "delays": [[1.0]]}, {"name": "pill2", "directions": 1, "delays": [[1.0]]}, {"name": "pill20", "directions": 1, "delays": [[1.0]]}, {"name": "pill3", "directions": 1, "delays": [[1.0]]}, {"name": "pill4", "directions": 1, "delays": [[1.0]]}, {"name": "pill5", "directions": 1, "delays": [[1.0]]}, {"name": "pill6", "directions": 1, "delays": [[1.0]]}, {"name": "pill7", "directions": 1, "delays": [[1.0]]}, {"name": "pill8", "directions": 1, "delays": [[1.0]]}, {"name": "pill9", "directions": 1, "delays": [[1.0]]}, {"name": "pill_canister", "directions": 1, "delays": [[1.0]]}, {"name": "syringeproj", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}, {"name": "vial", "directions": 1, "delays": [[1.0]]}, {"name": "weedpuff", "directions": 4, "delays": [[0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1, 0.1]]}]}
{"version":1,"size":{"x":32,"y":32},"license":"CC-BY-SA-3.0","copyright":"Taken from https://github.com/discordia-space/CEV-Eris/blob/2b969adc2dfd3e9621bf3597c5cbffeb3ac8c9f0/icons/obj/chemical.dmi","states":[{"name":"beaker","directions":1,"delays":[[1]]},{"name":"beakerbluespace","directions":1,"delays":[[0.1,0.1]]},{"name":"beakerlarge","directions":1,"delays":[[1]]},{"name":"beakernoreact","directions":1,"delays":[[0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05,0.05]]},{"name":"bottle","directions":1,"delays":[[1]]},{"name":"bottle-1","directions":1,"delays":[[1]]},{"name":"bottle-2","directions":1,"delays":[[1]]},{"name":"bottle-3","directions":1,"delays":[[1]]},{"name":"bottle-4","directions":1,"delays":[[1]]},{"name":"bottle1","directions":1,"delays":[[1]]},{"name":"bottle10","directions":1,"delays":[[1]]},{"name":"bottle11","directions":1,"delays":[[1]]},{"name":"bottle12","directions":1,"delays":[[1]]},{"name":"bottle13","directions":1,"delays":[[1]]},{"name":"bottle14","directions":1,"delays":[[1]]},{"name":"bottle15","directions":1,"delays":[[1]]},{"name":"bottle16","directions":1,"delays":[[1]]},{"name":"bottle17","directions":1,"delays":[[1]]},{"name":"bottle18","directions":1,"delays":[[1]]},{"name":"bottle19","directions":1,"delays":[[1]]},{"name":"bottle2","directions":1,"delays":[[1]]},{"name":"bottle20","directions":1,"delays":[[1]]},{"name":"bottle3","directions":1,"delays":[[1]]},{"name":"bottle4","directions":1,"delays":[[1]]},{"name":"bottle5","directions":1,"delays":[[1]]},{"name":"bottle6","directions":1,"delays":[[1]]},{"name":"bottle7","directions":1,"delays":[[1]]},{"name":"bottle8","directions":1,"delays":[[1]]},{"name":"bottle9","directions":1,"delays":[[1]]},{"name":"chemg","directions":1,"delays":[[1]]},{"name":"chemg_armed","directions":1,"delays":[[0.1,0.2]]},{"name":"chemg_ass","directions":1,"delays":[[1]]},{"name":"chemg_locked","directions":1,"delays":[[1]]},{"name":"chempuff","directions":4,"delays":[[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1]]},{"name":"dropper","directions":1,"delays":[[1]]},{"name":"large_grenade","directions":1,"delays":[[1]]},{"name":"large_grenade_armed","directions":1,"delays":[[0.1,0.1]]},{"name":"large_grenade_ass","directions":1,"delays":[[1]]},{"name":"large_grenade_locked","directions":1,"delays":[[1]]},{"name":"lid_beaker","directions":1,"delays":[[1]]},{"name":"lid_beakerlarge","directions":1,"delays":[[1]]},{"name":"lid_beakernoreact","directions":1,"delays":[[1]]},{"name":"lid_bottle","directions":1,"delays":[[1]]},{"name":"lid_vial","directions":1,"delays":[[1]]},{"name":"molten","directions":1,"delays":[[1]]},{"name":"pill","directions":1,"delays":[[1]]},{"name":"pill1","directions":1,"delays":[[1]]},{"name":"pill10","directions":1,"delays":[[1]]},{"name":"pill11","directions":1,"delays":[[1]]},{"name":"pill12","directions":1,"delays":[[1]]},{"name":"pill13","directions":1,"delays":[[1]]},{"name":"pill14","directions":1,"delays":[[1]]},{"name":"pill15","directions":1,"delays":[[1]]},{"name":"pill16","directions":1,"delays":[[1]]},{"name":"pill17","directions":1,"delays":[[1]]},{"name":"pill18","directions":1,"delays":[[1]]},{"name":"pill19","directions":1,"delays":[[1]]},{"name":"pill2","directions":1,"delays":[[1]]},{"name":"pill20","directions":1,"delays":[[1]]},{"name":"pill3","directions":1,"delays":[[1]]},{"name":"pill4","directions":1,"delays":[[1]]},{"name":"pill5","directions":1,"delays":[[1]]},{"name":"pill6","directions":1,"delays":[[1]]},{"name":"pill7","directions":1,"delays":[[1]]},{"name":"pill8","directions":1,"delays":[[1]]},{"name":"pill9","directions":1,"delays":[[1]]},{"name":"pill_canister","directions":1,"delays":[[1]]},{"name":"syringeproj","directions":1,"delays":[[1]]},{"name":"vial","directions":1,"delays":[[1]]},{"name":"weedpuff","directions":4,"delays":[[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1],[0.1,0.1,0.1,0.1,0.1]]}]}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 257 B