From 1b7860aeda455ec4938270999f7dfff410badb87 Mon Sep 17 00:00:00 2001 From: moneyl <8206401+Moneyl@users.noreply.github.com> Date: Sun, 23 Feb 2020 19:47:33 -0500 Subject: [PATCH] =?UTF-8?q?Add=20injectors=20+=20injected=20reagent=20meta?= =?UTF-8?q?bolism=20via=20BloodstreamCompo=E2=80=A6=20(#730)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- Content.Client/EntryPoint.cs | 3 +- .../Components/Chemistry/InjectorComponent.cs | 80 ++++++ .../Components/Chemistry/InjectorComponent.cs | 239 ++++++++++++++++++ .../Components/Chemistry/SolutionComponent.cs | 6 +- .../Metabolism/BloodstreamComponent.cs | 116 +++++++++ .../Components/Nutrition/StomachComponent.cs | 123 ++++++--- .../EntitySystems/BloodstreamSystem.cs | 35 +++ .../EntitySystems/StomachSystem.cs | 7 +- .../Chemistry/SharedInjectorComponent.cs | 39 +++ .../Components/Chemistry/SolutionComponent.cs | 8 + .../Nutrition/SharedStomachComponent.cs | 6 +- Content.Shared/GameObjects/ContentNetIDs.cs | 1 + .../Prototypes/Entities/items/chemistry.yml | 20 +- Resources/Prototypes/Entities/mobs/human.yml | 4 + .../Objects/Chemistry/chemicals.rsi/meta.json | 2 +- .../Chemistry/chemicals.rsi/syringeproj.png | Bin 489 -> 257 bytes 16 files changed, 648 insertions(+), 41 deletions(-) create mode 100644 Content.Client/GameObjects/Components/Chemistry/InjectorComponent.cs create mode 100644 Content.Server/GameObjects/Components/Chemistry/InjectorComponent.cs create mode 100644 Content.Server/GameObjects/Components/Metabolism/BloodstreamComponent.cs create mode 100644 Content.Server/GameObjects/EntitySystems/BloodstreamSystem.cs create mode 100644 Content.Shared/GameObjects/Components/Chemistry/SharedInjectorComponent.cs 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 175d7cdf631e1790f5a24120f98f59f20c0846a8..7819a48c6615c15c509e9d02952b74d9f8439bd8 100644 GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!;51Jc$B+p3w^t0gjwlGQUbvZSKD8-fqR63~ z_og;G({J2ynsi9ZJ4J7TJ-23A+-G~uL+pV@$NubXf0CO2?6IlumVD{jYoh=9Pgy!? zz0s_UJoj_P^wcFaO$V6g6o+_8u4Lr@Au7E<>O$1yw#~{7>|gG!yk_;uOmpe)xI@Cm z-iKGRRlHyN@Qv6CwibaU4?~!fdM@=CYAn*{RFwMiZtp2>pbHs1UHx3vIVCg!0Lz77 A0RR91 literal 489 zcmV=@n!u+~!Bwq&hk zCov`Osc_RY(RRCar_=%%Wzf)(5k9j*L4p^;rhNOtu-Y{LPb&BKmWQ*T-Wt| z-(bvW!ct0i9R^x&HdL17!*`z#NVIKBT5A$QJZza|Sw?xD(|*4XR-BL>Fq+g<9RL6T f00000@N+%@KUbQ1h>pm900000NkvXXu0mjf1%=S5