diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 7ed60563b9..e6a8347c4a 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -131,7 +131,8 @@ namespace Content.Client "UseDelay", "Pourable", "Paper", - "Write" + "Write", + "Bloodstream" }; foreach (var ignoreName in registerIgnore) diff --git a/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs b/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs new file mode 100644 index 0000000000..ab1bb415d1 --- /dev/null +++ b/Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs @@ -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 +{ + /// + /// Client behavior for injectors & syringes. Used for item status on injectors + /// + [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; + } + } + + /// + /// Item status control for injectors + /// + 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)); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs b/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs new file mode 100644 index 0000000000..f4cf53ad14 --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs @@ -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 +{ + /// + /// 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. + /// + [RegisterComponent] + public class InjectorComponent : SharedInjectorComponent, IAfterAttack, IUse + { +#pragma warning disable 649 + [Dependency] private readonly IServerNotifyManager _notifyManager; +#pragma warning restore 649 + + /// + /// Whether or not the injector is able to draw from containers or if it's a single use + /// device that can only inject. + /// + [ViewVariables] + private bool _injectOnly; + + /// + /// 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. + /// + [ViewVariables] + private int _transferAmount; + + /// + /// Initial storage volume of the injector + /// + [ViewVariables] + private int _initialMaxVolume; + + /// + /// 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 + /// + [ViewVariables(VVAccess.ReadWrite)] + private InjectorToggleMode _toggleState; + /// + /// Internal solution container + /// + [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; + } + + /// + /// Toggle between draw/inject state if applicable + /// + private void Toggle() + { + if (_injectOnly) + { + return; + } + + _toggleState = _toggleState switch + { + InjectorToggleMode.Inject => InjectorToggleMode.Draw, + InjectorToggleMode.Draw => InjectorToggleMode.Inject, + _ => throw new ArgumentOutOfRangeException() + }; + + Dirty(); + } + + /// + /// Called when clicking on entities while holding in active hand + /// + /// + 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(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(out var bloodstream) && _toggleState == InjectorToggleMode.Inject) + { + TryInjectIntoBloodstream(bloodstream, eventArgs.User); + } + } + } + + /// + /// Called when use key is pressed when held in active hand + /// + /// + /// + 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); + } + } +} diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs index 20d187a227..3e123fca11 100644 --- a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs @@ -35,7 +35,11 @@ namespace Content.Server.GameObjects.Components.Chemistry protected override void Startup() { base.Startup(); - + Init(); + } + + public void Init() + { _reactions = _prototypeManager.EnumeratePrototypes(); _audioSystem = _entitySystemManager.GetEntitySystem(); } diff --git a/Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs b/Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs new file mode 100644 index 0000000000..0fa862ce82 --- /dev/null +++ b/Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs @@ -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 +{ + /// + /// 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. + /// + [RegisterComponent] + public class BloodstreamComponent : Component + { +#pragma warning disable 649 + [Dependency] private readonly IPrototypeManager _prototypeManager; +#pragma warning restore 649 + + public override string Name => "Bloodstream"; + + /// + /// Internal solution for reagent storage + /// + [ViewVariables] + private SolutionComponent _internalSolution; + + /// + /// Max volume of internal solution storage + /// + [ViewVariables] + private int _initialMaxVolume; + + /// + /// Empty volume of internal solution + /// + 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 + } + + /// + /// Attempt to transfer provided solution to internal solution. Only supports complete transfers + /// + /// Solution to be transferred + /// Whether or not transfer was a success + 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; + } + + /// + /// Loops through each reagent in _internalSolution, and calls the IMetabolizable for each of them./> + /// + /// The time since the last metabolism tick in seconds. + 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); + } + } + } + + /// + /// Triggers metabolism of the reagents inside _internalSolution. Called by + /// + /// The time since the last metabolism tick in seconds. + public void OnUpdate(float tickTime) + { + Metabolize(tickTime); + } + } +} diff --git a/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs b/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs index 7478d6c038..95353351f9 100644 --- a/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs @@ -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 { + /// + /// Where reagents go when ingested. Tracks ingested reagents over time, and + /// eventually transfers them to once digested. + /// [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; + /// + /// Max volume of internal solution storage + /// public int MaxVolume { get => _stomachContents.MaxVolume; set => _stomachContents.MaxVolume = value; } + + /// + /// Internal solution storage + /// + [ViewVariables] + private SolutionComponent _stomachContents; + + /// + /// Initial internal solution storage volume + /// + [ViewVariables] private int _initialMaxVolume; - //Used to track changes to reagent amounts during metabolism - private readonly Dictionary _reagentDeltas = new Dictionary(); + + /// + /// Time in seconds between reagents being ingested and them being transferred to + /// + [ViewVariables] + private float _digestionDelay; + + /// + /// Used to track how long each reagent has been in the stomach + /// + private readonly List _reagentDeltas = new List(); + + /// + /// Reference to bloodstream where digested reagents are transferred to + /// + 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(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; } /// - /// 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. /// - /// The time since the last metabolism tick in seconds. - public void Metabolize(float tickTime) + /// The time since the last update in seconds. + public void OnUpdate(float tickTime) { - if (_stomachContents.CurrentVolume == 0) - return; - - //Run metabolism for each reagent, track quantity changes - _reagentDeltas.Clear(); - foreach (var reagent in _stomachContents.ReagentList) + if (_bloodstream == null) { - if(!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto)) - continue; + return; + } - foreach (var metabolizable in proto.Metabolism) + //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 + { + //Increment lifetime of reagents + delta.Increment(tickTime); + if (delta.Lifetime > _digestionDelay) { - _reagentDeltas[reagent.ReagentId] = metabolizable.Metabolize(Owner, reagent.ReagentId, tickTime); + _stomachContents.TryRemoveReagent(delta.ReagentId, delta.Quantity); + transferSolution.AddReagent(delta.ReagentId, delta.Quantity); + _reagentDeltas.Remove(delta); } } - - //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) - { - _stomachContents.TryRemoveReagent(reagentDelta.Key, reagentDelta.Value); - } + //Transfer digested reagents to bloodstream + _bloodstream.TryTransferSolution(transferSolution); } /// - /// Triggers metabolism of the reagents inside _stomachContents. Called by + /// Used to track quantity changes when ingesting & digesting reagents /// - /// The time since the last metabolism tick in seconds. - 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; } } } diff --git a/Content.Server/GameObjects/EntitySystems/BloodstreamSystem.cs b/Content.Server/GameObjects/EntitySystems/BloodstreamSystem.cs new file mode 100644 index 0000000000..7806e09b25 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/BloodstreamSystem.cs @@ -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 +{ + /// + /// Triggers metabolism updates for + /// + [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(); + comp.OnUpdate(_accumulatedFrameTime); + } + _accumulatedFrameTime = 0.0f; + } + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/StomachSystem.cs b/Content.Server/GameObjects/EntitySystems/StomachSystem.cs index 4b30bcfffb..9e7e12aac7 100644 --- a/Content.Server/GameObjects/EntitySystems/StomachSystem.cs +++ b/Content.Server/GameObjects/EntitySystems/StomachSystem.cs @@ -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 { + /// + /// Triggers digestion updates on + /// [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) diff --git a/Content.Shared/GameObjects/Components/Chemistry/SharedInjectorComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SharedInjectorComponent.cs new file mode 100644 index 0000000000..ebbb4b11e4 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Chemistry/SharedInjectorComponent.cs @@ -0,0 +1,39 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Chemistry +{ + /// + /// Shared class for injectors & syringes + /// + public class SharedInjectorComponent : Component + { + public override string Name => "Injector"; + public sealed override uint? NetID => ContentNetIDs.REAGENT_INJECTOR; + + /// + /// Component data used for net updates. Used by client for item status ui + /// + [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 + } + } +} diff --git a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs index d1e3b8fca7..426daa0639 100644 --- a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs +++ b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs @@ -74,6 +74,14 @@ namespace Content.Shared.GameObjects.Components.Chemistry /// Shortcut for Capabilities PourOut flag to avoid binary operators. /// public bool CanPourOut => (Capabilities & SolutionCaps.PourOut) != 0; + /// + /// Shortcut for Capabilities Injectable flag + /// + public bool Injectable => (Capabilities & SolutionCaps.Injectable) != 0; + /// + /// Shortcut for Capabilities Injector flag + /// + public bool Injector => (Capabilities & SolutionCaps.Injector) != 0; /// public override string Name => "Solution"; diff --git a/Content.Shared/GameObjects/Components/Nutrition/SharedStomachComponent.cs b/Content.Shared/GameObjects/Components/Nutrition/SharedStomachComponent.cs index 1333b58744..030b1eae50 100644 --- a/Content.Shared/GameObjects/Components/Nutrition/SharedStomachComponent.cs +++ b/Content.Shared/GameObjects/Components/Nutrition/SharedStomachComponent.cs @@ -1,8 +1,10 @@ -using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects; namespace Content.Shared.GameObjects.Components.Nutrition { - + /// + /// Shared class for stomach components + /// public class SharedStomachComponent : Component { public override string Name => "Stomach"; diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index bb3f1c6400..eeecefda41 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -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; } } diff --git a/Resources/Prototypes/Entities/items/chemistry.yml b/Resources/Prototypes/Entities/items/chemistry.yml index ced1e17639..cca0065844 100644 --- a/Resources/Prototypes/Entities/items/chemistry.yml +++ b/Resources/Prototypes/Entities/items/chemistry.yml @@ -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 diff --git a/Resources/Prototypes/Entities/mobs/human.yml b/Resources/Prototypes/Entities/mobs/human.yml index e7b3c9c2c2..3ae50bfaa9 100644 --- a/Resources/Prototypes/Entities/mobs/human.yml +++ b/Resources/Prototypes/Entities/mobs/human.yml @@ -14,6 +14,10 @@ - type: Thirst # Organs - type: Stomach + maxVolume: 100 + digestionDelay: 20 + - type: Bloodstream + maxVolume: 250 - type: Inventory - type: Constructor diff --git a/Resources/Textures/Objects/Chemistry/chemicals.rsi/meta.json b/Resources/Textures/Objects/Chemistry/chemicals.rsi/meta.json index 7c43141c20..6ff5767f18 100644 --- a/Resources/Textures/Objects/Chemistry/chemicals.rsi/meta.json +++ b/Resources/Textures/Objects/Chemistry/chemicals.rsi/meta.json @@ -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]]}]} \ No newline at end of file +{"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]]}]} diff --git a/Resources/Textures/Objects/Chemistry/chemicals.rsi/syringeproj.png b/Resources/Textures/Objects/Chemistry/chemicals.rsi/syringeproj.png index 175d7cdf63..7819a48c66 100644 Binary files a/Resources/Textures/Objects/Chemistry/chemicals.rsi/syringeproj.png and b/Resources/Textures/Objects/Chemistry/chemicals.rsi/syringeproj.png differ