diff --git a/Content.Server/Body/Behavior/LiverBehavior.cs b/Content.Server/Body/Behavior/LiverBehavior.cs index 82e12b29ce..95897b320a 100644 --- a/Content.Server/Body/Behavior/LiverBehavior.cs +++ b/Content.Server/Body/Behavior/LiverBehavior.cs @@ -17,6 +17,11 @@ namespace Content.Server.Body.Behavior private float _accumulatedFrameTime; + /// + /// Delay time that determines how often to metabolise blood contents (in seconds). + /// + private float _updateIntervalSeconds = 1.0f; + /// /// Whether the liver is functional. /// @@ -63,13 +68,13 @@ namespace Content.Server.Body.Behavior _accumulatedFrameTime += frameTime; - // Update at most once per second - if (_accumulatedFrameTime < 1) + // Update at most once every _updateIntervalSeconds + if (_accumulatedFrameTime < _updateIntervalSeconds) { return; } - _accumulatedFrameTime -= 1; + _accumulatedFrameTime -= _updateIntervalSeconds; if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream)) { @@ -90,6 +95,10 @@ namespace Content.Server.Body.Behavior continue; } + // How much reagent is available to metabolise? + // This needs to be passed to other functions that have metabolism rate information, such that they don't "overmetabolise" a reagent. + var availableReagent = bloodstream.Solution.Solution.GetReagentQuantity(reagent.ReagentId); + //TODO BODY Check if it's a Toxin. If volume < _toxinTolerance, just remove it. If greater, add damage = volume * _toxinLethality //TODO BODY Check if it has BoozePower > 0. Affect drunkenness, apply damage. Proposed formula (SS13-derived): damage = sqrt(volume) * BoozePower^_alcoholExponent * _alcoholLethality / 10 //TODO BODY Liver failure. @@ -99,8 +108,9 @@ namespace Content.Server.Body.Behavior // Run metabolism code for each reagent foreach (var metabolizable in prototype.Metabolism) { - var reagentDelta = metabolizable.Metabolize(Body.Owner, reagent.ReagentId, frameTime); + var reagentDelta = metabolizable.Metabolize(Body.Owner, reagent.ReagentId, _updateIntervalSeconds, availableReagent); bloodstream.Solution.TryRemoveReagent(reagent.ReagentId, reagentDelta); + availableReagent -= reagentDelta; } } } diff --git a/Content.Server/Body/Behavior/StomachBehavior.cs b/Content.Server/Body/Behavior/StomachBehavior.cs index a6c903efe9..813b8a3a5e 100644 --- a/Content.Server/Body/Behavior/StomachBehavior.cs +++ b/Content.Server/Body/Behavior/StomachBehavior.cs @@ -30,6 +30,8 @@ namespace Content.Server.Body.Behavior /// public override void Update(float frameTime) { + + // Do not metabolise if the organ does not have a body. if (Body == null) { return; @@ -45,7 +47,9 @@ namespace Content.Server.Body.Behavior _accumulatedFrameTime -= 1; - if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solution) || + // Note that "Owner" should be the organ that has this behaviour/mechanism, and it should have a dedicated + // solution container. "Body.Owner" is something else, and may have more than one solution container. + if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || !Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream)) { return; @@ -61,8 +65,19 @@ namespace Content.Server.Body.Behavior delta.Increment(1); if (delta.Lifetime > _digestionDelay) { - solution.TryRemoveReagent(delta.ReagentId, delta.Quantity); - transferSolution.AddReagent(delta.ReagentId, delta.Quantity); + // This reagent has been in the somach long enough, TRY to transfer it. + // But first, check if the reagent still exists, and how much is left. + // Some poor spessman may have washed down a potassium snack with some water. + if (solution.Solution.ContainsReagent(delta.ReagentId, out ReagentUnit quantity)){ + + if (quantity > delta.Quantity) { + quantity = delta.Quantity; + } + + solution.TryRemoveReagent(delta.ReagentId, quantity); + transferSolution.AddReagent(delta.ReagentId, quantity); + } + _reagentDeltas.Remove(delta); } } @@ -133,10 +148,10 @@ namespace Content.Server.Body.Behavior public bool TryTransferSolution(Solution solution) { - if (Body == null || !CanTransferSolution(solution)) + if (Owner == null || !CanTransferSolution(solution)) return false; - if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent)) + if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent)) { return false; } diff --git a/Content.Server/Chemistry/Metabolism/DefaultDrink.cs b/Content.Server/Chemistry/Metabolism/DefaultDrink.cs index 3b32776c85..255200f103 100644 --- a/Content.Server/Chemistry/Metabolism/DefaultDrink.cs +++ b/Content.Server/Chemistry/Metabolism/DefaultDrink.cs @@ -1,4 +1,4 @@ -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.Components; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Metabolizable; using Content.Shared.Chemistry.Reagent; @@ -9,28 +9,27 @@ namespace Content.Server.Chemistry.Metabolism { /// /// Default metabolism for drink reagents. Attempts to find a ThirstComponent on the target, - /// and to update it's thirst values. + /// and to update it's thirst values. Inherits metabolisation rate logic from DefaultMetabolizable. /// [DataDefinition] - public class DefaultDrink : IMetabolizable + public class DefaultDrink : DefaultMetabolizable { - //Rate of metabolism in units / second - [DataField("rate")] - public ReagentUnit MetabolismRate { get; set; } = ReagentUnit.New(1); - //How much thirst is satiated when 1u of the reagent is metabolized [DataField("hydrationFactor")] public float HydrationFactor { get; set; } = 30.0f; //Remove reagent at set rate, satiate thirst if a ThirstComponent can be found - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + public override ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) { - var metabolismAmount = MetabolismRate * tickTime; + // use DefaultMetabolism to determine how much reagent we should metabolize + var amountMetabolized = base.Metabolize(solutionEntity, reagentId, tickTime, availableReagent); + + // If metabolizing entity has a ThirstComponent, hydrate them. if (solutionEntity.TryGetComponent(out ThirstComponent? thirst)) - thirst.UpdateThirst(metabolismAmount.Float() * HydrationFactor); + thirst.UpdateThirst(amountMetabolized.Float() * HydrationFactor); //Return amount of reagent to be removed, remove reagent regardless of ThirstComponent presence - return metabolismAmount; + return amountMetabolized; } } } diff --git a/Content.Server/Chemistry/Metabolism/DefaultFood.cs b/Content.Server/Chemistry/Metabolism/DefaultFood.cs index aeba8ed95d..fafcf58dd1 100644 --- a/Content.Server/Chemistry/Metabolism/DefaultFood.cs +++ b/Content.Server/Chemistry/Metabolism/DefaultFood.cs @@ -1,4 +1,4 @@ -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.Components; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Metabolizable; using Content.Shared.Chemistry.Reagent; @@ -9,30 +9,30 @@ namespace Content.Server.Chemistry.Metabolism { /// /// Default metabolism for food reagents. Attempts to find a HungerComponent on the target, - /// and to update it's hunger values. + /// and to update it's hunger values. Inherits metabolisation rate logic from DefaultMetabolizable. /// [DataDefinition] - public class DefaultFood : IMetabolizable + public class DefaultFood : DefaultMetabolizable { - /// - /// Rate of metabolism in units / second - /// - [DataField("rate")] public ReagentUnit MetabolismRate { get; private set; } = ReagentUnit.New(1.0); /// /// How much hunger is satiated when 1u of the reagent is metabolized /// [DataField("nutritionFactor")] public float NutritionFactor { get; set; } = 30.0f; - //Remove reagent at set rate, satiate hunger if a HungerComponent can be found - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) - { - var metabolismAmount = MetabolismRate * tickTime; - if (solutionEntity.TryGetComponent(out HungerComponent? hunger)) - hunger.UpdateFood(metabolismAmount.Float() * NutritionFactor); - //Return amount of reagent to be removed, remove reagent regardless of HungerComponent presence - return metabolismAmount; + //Remove reagent at set rate, satiate hunger if a HungerComponent can be found + public override ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) + { + // use DefaultMetabolism to determine how much reagent we should metabolize + var amountMetabolized = base.Metabolize(solutionEntity, reagentId, tickTime, availableReagent); + + // If metabolizing entity has a HungerComponent, feed them. + if (solutionEntity.TryGetComponent(out HungerComponent? hunger)) + hunger.UpdateFood(amountMetabolized.Float() * NutritionFactor); + + //Return amount of reagent to be removed. Reagent is removed regardless of HungerComponent presence + return amountMetabolized; } } } diff --git a/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs b/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs index 45bbff68d7..bbf2534505 100644 --- a/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs +++ b/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs @@ -10,16 +10,11 @@ namespace Content.Server.Chemistry.Metabolism { /// /// Default metabolism for medicine reagents. Attempts to find a DamageableComponent on the target, - /// and to update its damage values. + /// and to update its damage values. Inherits metabolisation rate logic from DefaultMetabolizable. /// [DataDefinition] - public class HealthChangeMetabolism : IMetabolizable + public class HealthChangeMetabolism : DefaultMetabolizable { - /// - /// How much of the reagent should be metabolized each sec. - /// - [DataField("rate")] - public ReagentUnit MetabolismRate { get; set; } = ReagentUnit.New(1); /// /// How much damage is changed when 1u of the reagent is metabolized. @@ -41,14 +36,23 @@ namespace Content.Server.Chemistry.Metabolism /// /// /// + /// Reagent available to be metabolized. /// - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + public override ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) { + // use DefaultMetabolism to determine how much reagent we should metabolize + var amountMetabolized = base.Metabolize(solutionEntity, reagentId, tickTime, availableReagent); + + // how much does this much reagent heal for + var healthChangeAmount = HealthChange * amountMetabolized.Float(); + if (solutionEntity.TryGetComponent(out IDamageableComponent? health)) { - health.ChangeDamage(DamageType, (int)HealthChange, true); - float decHealthChange = (float) (HealthChange - (int) HealthChange); - _accumulatedHealth += decHealthChange; + // Heal damage by healthChangeAmmount, rounding down to nearest integer + health.ChangeDamage(DamageType, (int) healthChangeAmount, true); + + // Store decimal remainder of healthChangeAmmount in _accumulatedHealth + _accumulatedHealth += (healthChangeAmount - (int) healthChangeAmount); if (_accumulatedHealth >= 1) { @@ -62,7 +66,7 @@ namespace Content.Server.Chemistry.Metabolism _accumulatedHealth += 1; } } - return MetabolismRate; + return amountMetabolized; } } } diff --git a/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs b/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs index cbd10016a5..939acdd21a 100644 --- a/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs +++ b/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs @@ -1,11 +1,13 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.Chemistry.Reagent; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Shared.Chemistry.Metabolizable { /// - /// Default metabolism for reagents. Metabolizes the reagent with no effects + /// Default metabolization for reagents. Returns the amount of reagents metabolized without applying effects. + /// Metabolizes reagents at a constant rate, limited by how much is available. Other classes are derived from + /// this class, so that they do not need their own metabolization quantity calculation. /// [DataDefinition] public class DefaultMetabolizable : IMetabolizable @@ -13,12 +15,22 @@ namespace Content.Shared.Chemistry.Metabolizable /// /// Rate of metabolism in units / second /// - [DataField("rate")] - public double MetabolismRate { get; set; } = 1; + [DataField("rate")] public ReagentUnit MetabolismRate { get; set; } = ReagentUnit.New(1); - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + public virtual ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) { - return ReagentUnit.New(MetabolismRate * tickTime); + + // How much reagent should we metabolize + // The default behaviour is to metabolize at a constant rate, independent of the quantity of reagents. + var amountMetabolized = MetabolismRate * tickTime; + + // is that much reagent actually available? + if (availableReagent < amountMetabolized) + { + return availableReagent; + } + + return amountMetabolized; } } } diff --git a/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs b/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs index e08e209a0c..f8b2a888cc 100644 --- a/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs +++ b/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs @@ -1,4 +1,4 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.Chemistry.Reagent; using Robust.Shared.GameObjects; namespace Content.Shared.Chemistry.Metabolizable @@ -16,7 +16,8 @@ namespace Content.Shared.Chemistry.Metabolizable /// The entity containing the solution. /// The reagent id /// The time since the last metabolism tick in seconds. + /// Reagent available to be metabolized. /// The amount of reagent to be removed. The metabolizing organ should handle removing the reagent. - ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime); + ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent); } }