diff --git a/Content.Client/Body/Systems/BloodStreamSystem.cs b/Content.Client/Body/Systems/BloodStreamSystem.cs
new file mode 100644
index 0000000000..85f4f6198e
--- /dev/null
+++ b/Content.Client/Body/Systems/BloodStreamSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Body.Systems;
+
+namespace Content.Client.Body.Systems;
+
+public sealed class BloodstreamSystem : SharedBloodstreamSystem;
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
index 442c768709..b764c7f68d 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
@@ -273,7 +273,7 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"),
Act = () =>
{
- _bloodstreamSystem.SpillAllSolutions(args.Target, bloodstream);
+ _bloodstreamSystem.SpillAllSolutions((args.Target, bloodstream));
var xform = Transform(args.Target);
_popupSystem.PopupEntity(Loc.GetString("admin-smite-remove-blood-self"), args.Target,
args.Target, PopupType.LargeCaution);
diff --git a/Content.Server/Atmos/Rotting/RottingSystem.cs b/Content.Server/Atmos/Rotting/RottingSystem.cs
index 43dddce4a4..6f14debc3d 100644
--- a/Content.Server/Atmos/Rotting/RottingSystem.cs
+++ b/Content.Server/Atmos/Rotting/RottingSystem.cs
@@ -1,8 +1,8 @@
using Content.Server.Atmos.EntitySystems;
-using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Rotting;
+using Content.Shared.Body.Events;
using Content.Shared.Damage;
using Robust.Server.Containers;
using Robust.Shared.Physics.Components;
diff --git a/Content.Server/Body/Components/BloodstreamComponent.cs b/Content.Server/Body/Components/BloodstreamComponent.cs
deleted file mode 100644
index 35e76403bb..0000000000
--- a/Content.Server/Body/Components/BloodstreamComponent.cs
+++ /dev/null
@@ -1,186 +0,0 @@
-using Content.Server.Body.Systems;
-using Content.Server.Chemistry.EntitySystems;
-using Content.Shared.Alert;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Reagent;
-using Content.Shared.Damage;
-using Content.Shared.Damage.Prototypes;
-using Content.Shared.FixedPoint;
-using Robust.Shared.Audio;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-
-namespace Content.Server.Body.Components
-{
- [RegisterComponent, Access(typeof(BloodstreamSystem), typeof(ReactionMixerSystem))]
- public sealed partial class BloodstreamComponent : Component
- {
- public static string DefaultChemicalsSolutionName = "chemicals";
- public static string DefaultBloodSolutionName = "bloodstream";
- public static string DefaultBloodTemporarySolutionName = "bloodstreamTemporary";
-
- ///
- /// The next time that blood level will be updated and bloodloss damage dealt.
- ///
- [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
- public TimeSpan NextUpdate;
-
- ///
- /// The interval at which this component updates.
- ///
- [DataField]
- public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3);
-
- ///
- /// How much is this entity currently bleeding?
- /// Higher numbers mean more blood lost every tick.
- ///
- /// Goes down slowly over time, and items like bandages
- /// or clotting reagents can lower bleeding.
- ///
- ///
- /// This generally corresponds to an amount of damage and can't go above 100.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public float BleedAmount;
-
- ///
- /// How much should bleeding be reduced every update interval?
- ///
- [DataField]
- public float BleedReductionAmount = 0.33f;
-
- ///
- /// How high can go?
- ///
- [DataField]
- public float MaxBleedAmount = 10.0f;
-
- ///
- /// What percentage of current blood is necessary to avoid dealing blood loss damage?
- ///
- [DataField]
- public float BloodlossThreshold = 0.9f;
-
- ///
- /// The base bloodloss damage to be incurred if below
- /// The default values are defined per mob/species in YML.
- ///
- [DataField(required: true)]
- public DamageSpecifier BloodlossDamage = new();
-
- ///
- /// The base bloodloss damage to be healed if above
- /// The default values are defined per mob/species in YML.
- ///
- [DataField(required: true)]
- public DamageSpecifier BloodlossHealDamage = new();
-
- // TODO shouldn't be hardcoded, should just use some organ simulation like bone marrow or smth.
- ///
- /// How much reagent of blood should be restored each update interval?
- ///
- [DataField]
- public FixedPoint2 BloodRefreshAmount = 1.0f;
-
- ///
- /// How much blood needs to be in the temporary solution in order to create a puddle?
- ///
- [DataField]
- public FixedPoint2 BleedPuddleThreshold = 1.0f;
-
- ///
- /// A modifier set prototype ID corresponding to how damage should be modified
- /// before taking it into account for bloodloss.
- ///
- ///
- /// For example, piercing damage is increased while poison damage is nullified entirely.
- ///
- [DataField]
- public ProtoId DamageBleedModifiers = "BloodlossHuman";
-
- ///
- /// The sound to be played when a weapon instantly deals blood loss damage.
- ///
- [DataField]
- public SoundSpecifier InstantBloodSound = new SoundCollectionSpecifier("blood");
-
- ///
- /// The sound to be played when some damage actually heals bleeding rather than starting it.
- ///
- [DataField]
- public SoundSpecifier BloodHealedSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg");
-
- ///
- /// The minimum amount damage reduction needed to play the healing sound/popup.
- /// This prevents tiny amounts of heat damage from spamming the sound, e.g. spacing.
- ///
- [DataField]
- public float BloodHealedSoundThreshold = -0.1f;
-
- // TODO probably damage bleed thresholds.
-
- ///
- /// Max volume of internal chemical solution storage
- ///
- [DataField]
- public FixedPoint2 ChemicalMaxVolume = FixedPoint2.New(250);
-
- ///
- /// Max volume of internal blood storage,
- /// and starting level of blood.
- ///
- [DataField]
- public FixedPoint2 BloodMaxVolume = FixedPoint2.New(300);
-
- ///
- /// Which reagent is considered this entities 'blood'?
- ///
- ///
- /// Slime-people might use slime as their blood or something like that.
- ///
- [DataField]
- public ProtoId BloodReagent = "Blood";
-
- /// Name/Key that is indexed by.
- [DataField]
- public string BloodSolutionName = DefaultBloodSolutionName;
-
- /// Name/Key that is indexed by.
- [DataField]
- public string ChemicalSolutionName = DefaultChemicalsSolutionName;
-
- /// Name/Key that is indexed by.
- [DataField]
- public string BloodTemporarySolutionName = DefaultBloodTemporarySolutionName;
-
- ///
- /// Internal solution for blood storage
- ///
- [ViewVariables]
- public Entity? BloodSolution;
-
- ///
- /// Internal solution for reagent storage
- ///
- [ViewVariables]
- public Entity? ChemicalSolution;
-
- ///
- /// Temporary blood solution.
- /// When blood is lost, it goes to this solution, and when this
- /// solution hits a certain cap, the blood is actually spilled as a puddle.
- ///
- [ViewVariables]
- public Entity? TemporarySolution;
-
- ///
- /// Variable that stores the amount of status time added by having a low blood level.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public TimeSpan StatusTime;
-
- [DataField]
- public ProtoId BleedingAlert = "Bleed";
- }
-}
diff --git a/Content.Server/Body/Components/MetabolizerComponent.cs b/Content.Server/Body/Components/MetabolizerComponent.cs
index 90c99df7db..3699267ebf 100644
--- a/Content.Server/Body/Components/MetabolizerComponent.cs
+++ b/Content.Server/Body/Components/MetabolizerComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.Body.Prototypes;
using Content.Shared.FixedPoint;
diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs
index 6d85affad3..c2185750af 100644
--- a/Content.Server/Body/Systems/BloodstreamSystem.cs
+++ b/Content.Server/Body/Systems/BloodstreamSystem.cs
@@ -1,189 +1,32 @@
-using Content.Server.Body.Components;
-using Content.Server.Fluids.EntitySystems;
-using Content.Server.Popups;
-using Content.Shared.Alert;
-using Content.Shared.Body.Events;
-using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Chemistry.Reaction;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Reagent;
-using Content.Shared.Damage;
-using Content.Shared.Damage.Prototypes;
-using Content.Shared.Drunk;
-using Content.Shared.EntityEffects.Effects;
-using Content.Shared.FixedPoint;
using Content.Shared.Forensics;
-using Content.Shared.Forensics.Components;
-using Content.Shared.HealthExaminable;
-using Content.Shared.Mobs.Systems;
-using Content.Shared.Popups;
-using Content.Shared.Rejuvenate;
-using Content.Shared.Speech.EntitySystems;
-using Robust.Server.Audio;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-using Robust.Shared.Timing;
namespace Content.Server.Body.Systems;
-public sealed class BloodstreamSystem : EntitySystem
+public sealed class BloodstreamSystem : SharedBloodstreamSystem
{
- [Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
- [Dependency] private readonly AudioSystem _audio = default!;
- [Dependency] private readonly DamageableSystem _damageableSystem = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
- [Dependency] private readonly PuddleSystem _puddleSystem = default!;
- [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
- [Dependency] private readonly SharedDrunkSystem _drunkSystem = default!;
- [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
- [Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
- [Dependency] private readonly AlertsSystem _alertsSystem = default!;
-
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnComponentInit);
- SubscribeLocalEvent(OnMapInit);
- SubscribeLocalEvent(OnUnpaused);
- SubscribeLocalEvent(OnDamageChanged);
- SubscribeLocalEvent(OnHealthBeingExamined);
- SubscribeLocalEvent(OnBeingGibbed);
- SubscribeLocalEvent(OnApplyMetabolicMultiplier);
- SubscribeLocalEvent(OnReactionAttempt);
- SubscribeLocalEvent>(OnReactionAttempt);
- SubscribeLocalEvent(OnRejuvenate);
SubscribeLocalEvent(OnDnaGenerated);
}
- private void OnMapInit(Entity ent, ref MapInitEvent args)
- {
- ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
- }
-
- private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args)
- {
- ent.Comp.NextUpdate += args.PausedTime;
- }
-
- private void OnReactionAttempt(Entity entity, ref ReactionAttemptEvent args)
- {
- if (args.Cancelled)
- return;
-
- foreach (var effect in args.Reaction.Effects)
- {
- switch (effect)
- {
- case CreateEntityReactionEffect: // Prevent entities from spawning in the bloodstream
- case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels.
- args.Cancelled = true;
- return;
- }
- }
-
- // The area-reaction effect canceling is part of avoiding smoke-fork-bombs (create two smoke bombs, that when
- // ingested by mobs create more smoke). This also used to act as a rapid chemical-purge, because all the
- // reagents would get carried away by the smoke/foam. This does still work for the stomach (I guess people vomit
- // up the smoke or spawned entities?).
-
- // TODO apply organ damage instead of just blocking the reaction?
- // Having cheese-clots form in your veins can't be good for you.
- }
-
- private void OnReactionAttempt(Entity entity, ref SolutionRelayEvent args)
- {
- if (args.Name != entity.Comp.BloodSolutionName
- && args.Name != entity.Comp.ChemicalSolutionName
- && args.Name != entity.Comp.BloodTemporarySolutionName)
- {
- return;
- }
-
- OnReactionAttempt(entity, ref args.Event);
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var bloodstream))
- {
- if (_gameTiming.CurTime < bloodstream.NextUpdate)
- continue;
-
- bloodstream.NextUpdate += bloodstream.UpdateInterval;
-
- if (!_solutionContainerSystem.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
- continue;
-
- // Adds blood to their blood level if it is below the maximum; Blood regeneration. Must be alive.
- if (bloodSolution.Volume < bloodSolution.MaxVolume && !_mobStateSystem.IsDead(uid))
- {
- TryModifyBloodLevel(uid, bloodstream.BloodRefreshAmount, bloodstream);
- }
-
- // Removes blood from the bloodstream based on bleed amount (bleed rate)
- // as well as stop their bleeding to a certain extent.
- if (bloodstream.BleedAmount > 0)
- {
- // Blood is removed from the bloodstream at a 1-1 rate with the bleed amount
- TryModifyBloodLevel(uid, (-bloodstream.BleedAmount), bloodstream);
- // Bleed rate is reduced by the bleed reduction amount in the bloodstream component.
- TryModifyBleedAmount(uid, -bloodstream.BleedReductionAmount, bloodstream);
- }
-
- // deal bloodloss damage if their blood level is below a threshold.
- var bloodPercentage = GetBloodLevelPercentage(uid, bloodstream);
- if (bloodPercentage < bloodstream.BloodlossThreshold && !_mobStateSystem.IsDead(uid))
- {
- // bloodloss damage is based on the base value, and modified by how low your blood level is.
- var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage);
-
- _damageableSystem.TryChangeDamage(uid, amt,
- ignoreResistances: false, interruptsDoAfters: false);
-
- // Apply dizziness as a symptom of bloodloss.
- // The effect is applied in a way that it will never be cleared without being healthy.
- // Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out
- _drunkSystem.TryApplyDrunkenness(
- uid,
- (float) bloodstream.UpdateInterval.TotalSeconds * 2,
- applySlur: false);
- _stutteringSystem.DoStutter(uid, bloodstream.UpdateInterval * 2, refresh: false);
-
- // storing the drunk and stutter time so we can remove it independently from other effects additions
- bloodstream.StatusTime += bloodstream.UpdateInterval * 2;
- }
- else if (!_mobStateSystem.IsDead(uid))
- {
- // If they're healthy, we'll try and heal some bloodloss instead.
- _damageableSystem.TryChangeDamage(
- uid,
- bloodstream.BloodlossHealDamage * bloodPercentage,
- ignoreResistances: true, interruptsDoAfters: false);
-
- // Remove the drunk effect when healthy. Should only remove the amount of drunk and stutter added by low blood level
- _drunkSystem.TryRemoveDrunkenessTime(uid, bloodstream.StatusTime.TotalSeconds);
- _stutteringSystem.DoRemoveStutterTime(uid, bloodstream.StatusTime.TotalSeconds);
- // Reset the drunk and stutter time to zero
- bloodstream.StatusTime = TimeSpan.Zero;
- }
- }
- }
-
+ // not sure if we can move this to shared or not
+ // it would certainly help if SolutionContainer was documented
+ // but since we usually don't add the component dynamically to entities we can keep this unpredicted for now
private void OnComponentInit(Entity entity, ref ComponentInit args)
{
- if (!_solutionContainerSystem.EnsureSolution(entity.Owner,
+ if (!SolutionContainer.EnsureSolution(entity.Owner,
entity.Comp.ChemicalSolutionName,
out var chemicalSolution) ||
- !_solutionContainerSystem.EnsureSolution(entity.Owner,
+ !SolutionContainer.EnsureSolution(entity.Owner,
entity.Comp.BloodSolutionName,
out var bloodSolution) ||
- !_solutionContainerSystem.EnsureSolution(entity.Owner,
+ !SolutionContainer.EnsureSolution(entity.Owner,
entity.Comp.BloodTemporarySolutionName,
out var tempSolution))
return;
@@ -197,298 +40,10 @@ public sealed class BloodstreamSystem : EntitySystem
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
}
- private void OnDamageChanged(Entity ent, ref DamageChangedEvent args)
- {
- if (args.DamageDelta is null || !args.DamageIncreased)
- {
- return;
- }
-
- // TODO probably cache this or something. humans get hurt a lot
- if (!_prototypeManager.TryIndex(ent.Comp.DamageBleedModifiers, out var modifiers))
- return;
-
- // some reagents may deal and heal different damage types in the same tick, which means DamageIncreased will be true
- // but we only want to consider the dealt damage when causing bleeding
- var damage = DamageSpecifier.GetPositive(args.DamageDelta);
- var bloodloss = DamageSpecifier.ApplyModifierSet(damage, modifiers);
-
- if (bloodloss.Empty)
- return;
-
- // Does the calculation of how much bleed rate should be added/removed, then applies it
- var oldBleedAmount = ent.Comp.BleedAmount;
- var total = bloodloss.GetTotal();
- var totalFloat = total.Float();
- TryModifyBleedAmount(ent, totalFloat, ent);
-
- ///
- /// Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5
- /// The crit chance is currently the bleed rate modifier divided by 25.
- /// Higher damage weapons have a higher chance to crit!
- ///
- var prob = Math.Clamp(totalFloat / 25, 0, 1);
- if (totalFloat > 0 && _robustRandom.Prob(prob))
- {
- TryModifyBloodLevel(ent, -total / 5, ent);
- _audio.PlayPvs(ent.Comp.InstantBloodSound, ent);
- }
-
- // Heat damage will cauterize, causing the bleed rate to be reduced.
- else if (totalFloat <= ent.Comp.BloodHealedSoundThreshold && oldBleedAmount > 0)
- {
- // Magically, this damage has healed some bleeding, likely
- // because it's burn damage that cauterized their wounds.
-
- // We'll play a special sound and popup for feedback.
- _audio.PlayPvs(ent.Comp.BloodHealedSound, ent);
- _popupSystem.PopupEntity(Loc.GetString("bloodstream-component-wounds-cauterized"), ent,
- ent, PopupType.Medium);
- }
- }
- ///
- /// Shows text on health examine, based on bleed rate and blood level.
- ///
- private void OnHealthBeingExamined(Entity ent, ref HealthBeingExaminedEvent args)
- {
- // Shows massively bleeding at 0.75x the max bleed rate.
- if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.75f)
- {
- args.Message.PushNewline();
- args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-massive-bleeding", ("target", ent.Owner)));
- }
- // Shows bleeding message when bleeding above half the max rate, but less than massively.
- else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.5f)
- {
- args.Message.PushNewline();
- args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-strong-bleeding", ("target", ent.Owner)));
- }
- // Shows bleeding message when bleeding above 0.25x the max rate, but less than half the max.
- else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.25f)
- {
- args.Message.PushNewline();
- args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
- }
- // Shows bleeding message when bleeding below 0.25x the max cap
- else if (ent.Comp.BleedAmount > 0)
- {
- args.Message.PushNewline();
- args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-slight-bleeding", ("target", ent.Owner)));
- }
-
- // If the mob's blood level is below the damage threshhold, the pale message is added.
- if (GetBloodLevelPercentage(ent, ent) < ent.Comp.BloodlossThreshold)
- {
- args.Message.PushNewline();
- args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
- }
- }
-
- private void OnBeingGibbed(Entity ent, ref BeingGibbedEvent args)
- {
- SpillAllSolutions(ent, ent);
- }
-
- private void OnApplyMetabolicMultiplier(
- Entity ent,
- ref ApplyMetabolicMultiplierEvent args)
- {
- // TODO REFACTOR THIS
- // This will slowly drift over time due to floating point errors.
- // Instead, raise an event with the base rates and allow modifiers to get applied to it.
- if (args.Apply)
- {
- ent.Comp.UpdateInterval *= args.Multiplier;
- return;
- }
- ent.Comp.UpdateInterval /= args.Multiplier;
- }
-
- private void OnRejuvenate(Entity entity, ref RejuvenateEvent args)
- {
- TryModifyBleedAmount(entity.Owner, -entity.Comp.BleedAmount, entity.Comp);
-
- if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
- TryModifyBloodLevel(entity.Owner, bloodSolution.AvailableVolume, entity.Comp);
-
- if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.ChemicalSolutionName, ref entity.Comp.ChemicalSolution))
- _solutionContainerSystem.RemoveAllSolution(entity.Comp.ChemicalSolution.Value);
- }
-
- ///
- /// Attempt to transfer provided solution to internal solution.
- ///
- public bool TryAddToChemicals(EntityUid uid, Solution solution, BloodstreamComponent? component = null)
- {
- return Resolve(uid, ref component, logMissing: false)
- && _solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution)
- && _solutionContainerSystem.TryAddSolution(component.ChemicalSolution.Value, solution);
- }
-
- public bool FlushChemicals(EntityUid uid, string excludedReagentID, FixedPoint2 quantity, BloodstreamComponent? component = null)
- {
- if (!Resolve(uid, ref component, logMissing: false)
- || !_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution))
- return false;
-
- for (var i = chemSolution.Contents.Count - 1; i >= 0; i--)
- {
- var (reagentId, _) = chemSolution.Contents[i];
- if (reagentId.Prototype != excludedReagentID)
- {
- _solutionContainerSystem.RemoveReagent(component.ChemicalSolution.Value, reagentId, quantity);
- }
- }
-
- return true;
- }
-
- public float GetBloodLevelPercentage(EntityUid uid, BloodstreamComponent? component = null)
- {
- if (!Resolve(uid, ref component)
- || !_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
- {
- return 0.0f;
- }
-
- return bloodSolution.FillFraction;
- }
-
- public void SetBloodLossThreshold(EntityUid uid, float threshold, BloodstreamComponent? comp = null)
- {
- if (!Resolve(uid, ref comp))
- return;
-
- comp.BloodlossThreshold = threshold;
- }
-
- ///
- /// Attempts to modify the blood level of this entity directly.
- ///
- public bool TryModifyBloodLevel(EntityUid uid, FixedPoint2 amount, BloodstreamComponent? component = null)
- {
- if (!Resolve(uid, ref component, logMissing: false)
- || !_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution))
- {
- return false;
- }
-
- if (amount >= 0)
- return _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, amount, null, GetEntityBloodData(uid));
-
- // Removal is more involved,
- // since we also wanna handle moving it to the temporary solution
- // and then spilling it if necessary.
- var newSol = _solutionContainerSystem.SplitSolution(component.BloodSolution.Value, -amount);
-
- if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodTemporarySolutionName, ref component.TemporarySolution, out var tempSolution))
- return true;
-
- tempSolution.AddSolution(newSol, _prototypeManager);
-
- if (tempSolution.Volume > component.BleedPuddleThreshold)
- {
- // Pass some of the chemstream into the spilled blood.
- if (_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution))
- {
- var temp = _solutionContainerSystem.SplitSolution(component.ChemicalSolution.Value, tempSolution.Volume / 10);
- tempSolution.AddSolution(temp, _prototypeManager);
- }
-
- _puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, sound: false);
-
- tempSolution.RemoveAllSolution();
- }
-
- _solutionContainerSystem.UpdateChemicals(component.TemporarySolution.Value);
-
- return true;
- }
-
- ///
- /// Tries to make an entity bleed more or less
- ///
- public bool TryModifyBleedAmount(EntityUid uid, float amount, BloodstreamComponent? component = null)
- {
- if (!Resolve(uid, ref component, logMissing: false))
- return false;
-
- component.BleedAmount += amount;
- component.BleedAmount = Math.Clamp(component.BleedAmount, 0, component.MaxBleedAmount);
-
- if (component.BleedAmount == 0)
- _alertsSystem.ClearAlert(uid, component.BleedingAlert);
- else
- {
- var severity = (short) Math.Clamp(Math.Round(component.BleedAmount, MidpointRounding.ToZero), 0, 10);
- _alertsSystem.ShowAlert(uid, component.BleedingAlert, severity);
- }
-
- return true;
- }
-
- ///
- /// BLOOD FOR THE BLOOD GOD
- ///
- public void SpillAllSolutions(EntityUid uid, BloodstreamComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- var tempSol = new Solution();
-
- if (_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
- {
- tempSol.MaxVolume += bloodSolution.MaxVolume;
- tempSol.AddSolution(bloodSolution, _prototypeManager);
- _solutionContainerSystem.RemoveAllSolution(component.BloodSolution.Value);
- }
-
- if (_solutionContainerSystem.ResolveSolution(uid, component.ChemicalSolutionName, ref component.ChemicalSolution, out var chemSolution))
- {
- tempSol.MaxVolume += chemSolution.MaxVolume;
- tempSol.AddSolution(chemSolution, _prototypeManager);
- _solutionContainerSystem.RemoveAllSolution(component.ChemicalSolution.Value);
- }
-
- if (_solutionContainerSystem.ResolveSolution(uid, component.BloodTemporarySolutionName, ref component.TemporarySolution, out var tempSolution))
- {
- tempSol.MaxVolume += tempSolution.MaxVolume;
- tempSol.AddSolution(tempSolution, _prototypeManager);
- _solutionContainerSystem.RemoveAllSolution(component.TemporarySolution.Value);
- }
-
- _puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid);
- }
-
- ///
- /// Change what someone's blood is made of, on the fly.
- ///
- public void ChangeBloodReagent(EntityUid uid, string reagent, BloodstreamComponent? component = null)
- {
- if (!Resolve(uid, ref component, logMissing: false)
- || reagent == component.BloodReagent)
- {
- return;
- }
-
- if (!_solutionContainerSystem.ResolveSolution(uid, component.BloodSolutionName, ref component.BloodSolution, out var bloodSolution))
- {
- component.BloodReagent = reagent;
- return;
- }
-
- var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume, ignoreReagentData: true);
-
- component.BloodReagent = reagent;
-
- if (currentVolume > 0)
- _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, currentVolume, null, GetEntityBloodData(uid));
- }
-
+ // forensics is not predicted yet
private void OnDnaGenerated(Entity entity, ref GenerateDnaEvent args)
{
- if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
+ if (SolutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
{
foreach (var reagent in bloodSolution.Contents)
{
@@ -500,22 +55,4 @@ public sealed class BloodstreamSystem : EntitySystem
else
Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
}
-
- ///
- /// Get the reagent data for blood that a specific entity should have.
- ///
- public List GetEntityBloodData(EntityUid uid)
- {
- var bloodData = new List();
- var dnaData = new DnaData();
-
- if (TryComp(uid, out var donorComp) && donorComp.DNA != null)
- dnaData.DNA = donorComp.DNA;
- else
- dnaData.DNA = Loc.GetString("forensics-dna-unknown");
-
- bloodData.Add(dnaData);
-
- return bloodData;
- }
}
diff --git a/Content.Server/Body/Systems/BodySystem.cs b/Content.Server/Body/Systems/BodySystem.cs
index d2fc3d6558..957dea9d41 100644
--- a/Content.Server/Body/Systems/BodySystem.cs
+++ b/Content.Server/Body/Systems/BodySystem.cs
@@ -1,5 +1,4 @@
using System.Numerics;
-using Content.Server.Body.Components;
using Content.Server.Ghost;
using Content.Server.Humanoid;
using Content.Shared.Body.Components;
diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
index f53688a241..7b43e7f092 100644
--- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
@@ -1,10 +1,9 @@
-using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Body.Components;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
@@ -237,7 +236,7 @@ public sealed class InjectorSystem : SharedInjectorSystem
// Move units from attackSolution to targetSolution
var removedSolution = SolutionContainers.SplitSolution(target.Comp.ChemicalSolution.Value, realTransferAmount);
- _blood.TryAddToChemicals(target, removedSolution, target.Comp);
+ _blood.TryAddToChemicals(target.AsNullable(), removedSolution);
_reactiveSystem.DoEntityReaction(target, removedSolution, ReactionMethod.Injection);
diff --git a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs
index 503a0ebde6..7b4deea9f4 100644
--- a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnEventSystem.cs
@@ -1,7 +1,7 @@
-using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Body.Components;
using Content.Shared.Chemistry.Events;
using Content.Shared.Inventory;
using Content.Shared.Popups;
@@ -148,7 +148,7 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
// Take our portion of the adjusted solution for this target
var individualInjection = solutionToInject.SplitSolution(volumePerBloodstream);
// Inject our portion into the target's bloodstream
- if (_bloodstream.TryAddToChemicals(targetBloodstream.Owner, individualInjection, targetBloodstream.Comp))
+ if (_bloodstream.TryAddToChemicals(targetBloodstream.AsNullable(), individualInjection))
anySuccess = true;
}
diff --git a/Content.Server/Devour/DevourSystem.cs b/Content.Server/Devour/DevourSystem.cs
index 8ee4cf852b..88edc3ec4c 100644
--- a/Content.Server/Devour/DevourSystem.cs
+++ b/Content.Server/Devour/DevourSystem.cs
@@ -1,5 +1,5 @@
-using Content.Server.Body.Components;
using Content.Server.Body.Systems;
+using Content.Shared.Body.Events;
using Content.Shared.Chemistry.Components;
using Content.Shared.Devour;
using Content.Shared.Devour.Components;
diff --git a/Content.Server/EntityEffects/EntityEffectSystem.cs b/Content.Server/EntityEffects/EntityEffectSystem.cs
index 98853a0e61..e18b3b1470 100644
--- a/Content.Server/EntityEffects/EntityEffectSystem.cs
+++ b/Content.Server/EntityEffects/EntityEffectSystem.cs
@@ -22,6 +22,7 @@ using Content.Server.Temperature.Systems;
using Content.Server.Traits.Assorted;
using Content.Server.Zombies;
using Content.Shared.Atmos;
+using Content.Shared.Body.Components;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.EntityEffects.EffectConditions;
using Content.Shared.EntityEffects.Effects.PlantMetabolism;
@@ -558,11 +559,11 @@ public sealed class EntityEffectSystem : EntitySystem
return;
cleanseRate *= reagentArgs.Scale.Float();
- _bloodstream.FlushChemicals(args.Args.TargetEntity, reagentArgs.Reagent.ID, cleanseRate);
+ _bloodstream.FlushChemicals(args.Args.TargetEntity, reagentArgs.Reagent, cleanseRate);
}
else
{
- _bloodstream.FlushChemicals(args.Args.TargetEntity, "", cleanseRate);
+ _bloodstream.FlushChemicals(args.Args.TargetEntity, null, cleanseRate);
}
}
@@ -780,7 +781,7 @@ public sealed class EntityEffectSystem : EntitySystem
amt *= reagentArgs.Scale.Float();
}
- _bloodstream.TryModifyBleedAmount(args.Args.TargetEntity, amt, blood);
+ _bloodstream.TryModifyBleedAmount((args.Args.TargetEntity, blood), amt);
}
}
@@ -796,7 +797,7 @@ public sealed class EntityEffectSystem : EntitySystem
amt *= reagentArgs.Scale;
}
- _bloodstream.TryModifyBloodLevel(args.Args.TargetEntity, amt, blood);
+ _bloodstream.TryModifyBloodLevel((args.Args.TargetEntity, blood), amt);
}
}
diff --git a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs
index dfbad1bbe9..13695caff1 100644
--- a/Content.Server/Fluids/EntitySystems/SmokeSystem.cs
+++ b/Content.Server/Fluids/EntitySystems/SmokeSystem.cs
@@ -1,8 +1,8 @@
using Content.Server.Administration.Logs;
-using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.EntityEffects.Effects;
using Content.Server.Spreader;
+using Content.Shared.Body.Components;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
@@ -288,7 +288,7 @@ public sealed class SmokeSystem : EntitySystem
if (blockIngestion)
return;
- if (_blood.TryAddToChemicals(entity, transferSolution, bloodstream))
+ if (_blood.TryAddToChemicals((entity, bloodstream), transferSolution))
{
// Log solution addition by smoke
_logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} ingested smoke {SharedSolutionContainerSystem.ToPrettyString(transferSolution)}");
diff --git a/Content.Server/Forensics/Systems/ForensicsSystem.cs b/Content.Server/Forensics/Systems/ForensicsSystem.cs
index ec4683460b..9f94e39fb7 100644
--- a/Content.Server/Forensics/Systems/ForensicsSystem.cs
+++ b/Content.Server/Forensics/Systems/ForensicsSystem.cs
@@ -1,9 +1,9 @@
-using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics.Components;
using Content.Server.Popups;
+using Content.Shared.Body.Events;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Chemistry.Components;
diff --git a/Content.Server/Implants/ImplantedSystem.cs b/Content.Server/Implants/ImplantedSystem.cs
index c5048cbd8d..7851758730 100644
--- a/Content.Server/Implants/ImplantedSystem.cs
+++ b/Content.Server/Implants/ImplantedSystem.cs
@@ -1,4 +1,4 @@
-using Content.Server.Body.Components;
+using Content.Shared.Body.Events;
using Content.Shared.Implants.Components;
using Content.Shared.Storage;
using Robust.Shared.Containers;
diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs
index f6b751c398..42e455a47f 100644
--- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs
+++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs
@@ -1,11 +1,11 @@
using System.Numerics;
-using Content.Server.Body.Components;
using Content.Server.Botany.Components;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Materials;
using Content.Server.Power.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Audio;
+using Content.Shared.Body.Components;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Climbing.Events;
@@ -30,7 +30,6 @@ using Robust.Server.Player;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Physics.Components;
-using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Medical.BiomassReclaimer
diff --git a/Content.Server/Medical/Components/HealingComponent.cs b/Content.Server/Medical/Components/HealingComponent.cs
deleted file mode 100644
index a56bc17143..0000000000
--- a/Content.Server/Medical/Components/HealingComponent.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using Content.Shared.Damage;
-using Content.Shared.Damage.Prototypes;
-using Robust.Shared.Audio;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
-
-namespace Content.Server.Medical.Components
-{
- ///
- /// Applies a damage change to the target when used in an interaction.
- ///
- [RegisterComponent]
- public sealed partial class HealingComponent : Component
- {
- [DataField("damage", required: true)]
- [ViewVariables(VVAccess.ReadWrite)]
- public DamageSpecifier Damage = default!;
-
- ///
- /// This should generally be negative,
- /// since you're, like, trying to heal damage.
- ///
- [DataField("bloodlossModifier")]
- [ViewVariables(VVAccess.ReadWrite)]
- public float BloodlossModifier = 0.0f;
-
- ///
- /// Restore missing blood.
- ///
- [DataField("ModifyBloodLevel")]
- [ViewVariables(VVAccess.ReadWrite)]
- public float ModifyBloodLevel = 0.0f;
-
- ///
- /// The supported damage types are specified using a s. For a
- /// HealingComponent this filters what damage container type this component should work on. If null,
- /// all damage container types are supported.
- ///
- [DataField("damageContainers", customTypeSerializer: typeof(PrototypeIdListSerializer))]
- public List? DamageContainers;
-
- ///
- /// How long it takes to apply the damage.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("delay")]
- public float Delay = 3f;
-
- ///
- /// Delay multiplier when healing yourself.
- ///
- [DataField("selfHealPenaltyMultiplier")]
- public float SelfHealPenaltyMultiplier = 3f;
-
- ///
- /// Sound played on healing begin
- ///
- [DataField("healingBeginSound")]
- public SoundSpecifier? HealingBeginSound = null;
-
- ///
- /// Sound played on healing end
- ///
- [DataField("healingEndSound")]
- public SoundSpecifier? HealingEndSound = null;
- }
-}
diff --git a/Content.Server/Medical/CryoPodSystem.cs b/Content.Server/Medical/CryoPodSystem.cs
index 8dd0330a27..1160f6aa17 100644
--- a/Content.Server/Medical/CryoPodSystem.cs
+++ b/Content.Server/Medical/CryoPodSystem.cs
@@ -2,7 +2,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.EntitySystems;
-using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Medical.Components;
using Content.Server.NodeContainer.EntitySystems;
@@ -10,6 +9,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Temperature.Components;
using Content.Shared.Atmos;
+using Content.Shared.Body.Components;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
@@ -116,7 +116,7 @@ public sealed partial class CryoPodSystem : SharedCryoPodSystem
}
var solutionToInject = _solutionContainerSystem.SplitSolution(containerSolution.Value, cryoPod.BeakerTransferAmount);
- _bloodstreamSystem.TryAddToChemicals(patient.Value, solutionToInject, bloodstream);
+ _bloodstreamSystem.TryAddToChemicals((patient.Value, bloodstream), solutionToInject);
_reactiveSystem.DoEntityReaction(patient.Value, solutionToInject, ReactionMethod.Injection);
}
}
diff --git a/Content.Server/Medical/HealthAnalyzerSystem.cs b/Content.Server/Medical/HealthAnalyzerSystem.cs
index f2235363ad..11e4ed4fcf 100644
--- a/Content.Server/Medical/HealthAnalyzerSystem.cs
+++ b/Content.Server/Medical/HealthAnalyzerSystem.cs
@@ -1,8 +1,7 @@
-using Content.Server.Body.Components;
using Content.Server.Medical.Components;
using Content.Server.PowerCell;
using Content.Server.Temperature.Components;
-using Content.Shared.Traits.Assorted;
+using Content.Shared.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
@@ -14,6 +13,7 @@ using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
+using Content.Shared.Traits.Assorted;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
diff --git a/Content.Server/Medical/VomitSystem.cs b/Content.Server/Medical/VomitSystem.cs
index 9d27247a38..66cc9bf5f6 100644
--- a/Content.Server/Medical/VomitSystem.cs
+++ b/Content.Server/Medical/VomitSystem.cs
@@ -1,4 +1,3 @@
-using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
diff --git a/Content.Server/Mind/TransferMindOnGibSystem.cs b/Content.Server/Mind/TransferMindOnGibSystem.cs
index 758a23c868..ff71fa0560 100644
--- a/Content.Server/Mind/TransferMindOnGibSystem.cs
+++ b/Content.Server/Mind/TransferMindOnGibSystem.cs
@@ -1,5 +1,5 @@
using System.Linq;
-using Content.Server.Body.Components;
+using Content.Shared.Body.Events;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Tag;
diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs
index fa5ec0a1bf..5a269eace5 100644
--- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs
+++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs
@@ -1,8 +1,8 @@
-using Content.Server.Body.Components;
using Content.Server.DoAfter;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Nutrition.Components;
using Content.Server.Popups;
+using Content.Shared.Body.Components;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs
index b3ef8bff69..4960ff1ba8 100644
--- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs
+++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs
@@ -1,10 +1,9 @@
using Content.Server.Atmos.EntitySystems;
-using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Server.Forensics;
+using Content.Shared.Body.Components;
using Content.Shared.Chemistry;
-using Content.Shared.Chemistry.Reagent;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.FixedPoint;
@@ -17,7 +16,6 @@ using Content.Shared.Temperature;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
-using System.Linq;
using Content.Shared.Atmos;
namespace Content.Server.Nutrition.EntitySystems
@@ -159,7 +157,7 @@ namespace Content.Server.Nutrition.EntitySystems
}
_reactiveSystem.DoEntityReaction(containerManager.Owner, inhaledSolution, ReactionMethod.Ingestion);
- _bloodstreamSystem.TryAddToChemicals(containerManager.Owner, inhaledSolution, bloodstream);
+ _bloodstreamSystem.TryAddToChemicals((containerManager.Owner, bloodstream), inhaledSolution);
}
_timer -= UpdateTimer;
diff --git a/Content.Server/Rootable/RootableSystem.cs b/Content.Server/Rootable/RootableSystem.cs
index ce88f18dc3..cd18315bd0 100644
--- a/Content.Server/Rootable/RootableSystem.cs
+++ b/Content.Server/Rootable/RootableSystem.cs
@@ -1,6 +1,6 @@
-using Content.Server.Body.Components;
-using Content.Server.Body.Systems;
+using Content.Server.Body.Systems;
using Content.Shared.Administration.Logs;
+using Content.Shared.Body.Components;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
@@ -12,6 +12,7 @@ using Robust.Shared.Timing;
namespace Content.Server.Rootable;
+// TODO: Move all of this to shared
///
/// Adds an action to toggle rooting to the ground, primarily for the Diona species.
///
@@ -68,7 +69,7 @@ public sealed class RootableSystem : SharedRootableSystem
_reactive.DoEntityReaction(entity, transferSolution, ReactionMethod.Ingestion);
- if (_blood.TryAddToChemicals(entity, transferSolution, entity.Comp2))
+ if (_blood.TryAddToChemicals((entity, entity.Comp2), transferSolution))
{
// Log solution addition by puddle
_logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} absorbed puddle {SharedSolutionContainerSystem.ToPrettyString(transferSolution)}");
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs
index 3957e02d2d..fd40aa8816 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.cs
@@ -1,13 +1,12 @@
using Content.Server.Actions;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
-using Content.Server.Body.Components;
+using Content.Shared.Body.Events;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Hands.Systems;
using Content.Server.PowerCell;
using Content.Shared.Alert;
-using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs
index 03be40aad2..cf0dac30b2 100644
--- a/Content.Server/Zombies/ZombieSystem.Transform.cs
+++ b/Content.Server/Zombies/ZombieSystem.Transform.cs
@@ -13,6 +13,7 @@ using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Server.Speech.Components;
using Content.Server.Temperature.Components;
+using Content.Shared.Body.Components;
using Content.Shared.CombatMode;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Damage;
diff --git a/Content.Shared/Body/Components/BloodstreamComponent.cs b/Content.Shared/Body/Components/BloodstreamComponent.cs
new file mode 100644
index 0000000000..7997d92066
--- /dev/null
+++ b/Content.Shared/Body/Components/BloodstreamComponent.cs
@@ -0,0 +1,200 @@
+using Content.Shared.Alert;
+using Content.Shared.Body.Systems;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Body.Components;
+
+///
+/// Gives an entity a bloodstream.
+///
+[RegisterComponent, NetworkedComponent,]
+[AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause]
+[Access(typeof(SharedBloodstreamSystem))]
+public sealed partial class BloodstreamComponent : Component
+{
+ public const string DefaultChemicalsSolutionName = "chemicals";
+ public const string DefaultBloodSolutionName = "bloodstream";
+ public const string DefaultBloodTemporarySolutionName = "bloodstreamTemporary";
+
+ ///
+ /// The next time that blood level will be updated and bloodloss damage dealt.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [AutoNetworkedField, AutoPausedField]
+ public TimeSpan NextUpdate;
+
+ ///
+ /// The interval at which this component updates.
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3);
+
+ ///
+ /// How much is this entity currently bleeding?
+ /// Higher numbers mean more blood lost every tick.
+ ///
+ /// Goes down slowly over time, and items like bandages
+ /// or clotting reagents can lower bleeding.
+ ///
+ ///
+ /// This generally corresponds to an amount of damage and can't go above 100.
+ ///
+ [DataField, AutoNetworkedField]
+ public float BleedAmount;
+
+ ///
+ /// How much should bleeding be reduced every update interval?
+ ///
+ [DataField, AutoNetworkedField]
+ public float BleedReductionAmount = 0.33f;
+
+ ///
+ /// How high can go?
+ ///
+ [DataField, AutoNetworkedField]
+ public float MaxBleedAmount = 10.0f;
+
+ ///
+ /// What percentage of current blood is necessary to avoid dealing blood loss damage?
+ ///
+ [DataField, AutoNetworkedField]
+ public float BloodlossThreshold = 0.9f;
+
+ ///
+ /// The base bloodloss damage to be incurred if below
+ /// The default values are defined per mob/species in YML.
+ ///
+ [DataField(required: true), AutoNetworkedField]
+ public DamageSpecifier BloodlossDamage = new();
+
+ ///
+ /// The base bloodloss damage to be healed if above
+ /// The default values are defined per mob/species in YML.
+ ///
+ [DataField(required: true), AutoNetworkedField]
+ public DamageSpecifier BloodlossHealDamage = new();
+
+ // TODO shouldn't be hardcoded, should just use some organ simulation like bone marrow or smth.
+ ///
+ /// How much reagent of blood should be restored each update interval?
+ ///
+ [DataField, AutoNetworkedField]
+ public FixedPoint2 BloodRefreshAmount = 1.0f;
+
+ ///
+ /// How much blood needs to be in the temporary solution in order to create a puddle?
+ ///
+ [DataField, AutoNetworkedField]
+ public FixedPoint2 BleedPuddleThreshold = 1.0f;
+
+ ///
+ /// A modifier set prototype ID corresponding to how damage should be modified
+ /// before taking it into account for bloodloss.
+ ///
+ ///
+ /// For example, piercing damage is increased while poison damage is nullified entirely.
+ ///
+ [DataField, AutoNetworkedField]
+ public ProtoId DamageBleedModifiers = "BloodlossHuman";
+
+ ///
+ /// The sound to be played when a weapon instantly deals blood loss damage.
+ ///
+ [DataField, AutoNetworkedField]
+ public SoundSpecifier InstantBloodSound = new SoundCollectionSpecifier("blood");
+
+ ///
+ /// The sound to be played when some damage actually heals bleeding rather than starting it.
+ ///
+ [DataField]
+ public SoundSpecifier BloodHealedSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg");
+
+ ///
+ /// The minimum amount damage reduction needed to play the healing sound/popup.
+ /// This prevents tiny amounts of heat damage from spamming the sound, e.g. spacing.
+ ///
+ [DataField]
+ public float BloodHealedSoundThreshold = -0.1f;
+
+ // TODO probably damage bleed thresholds.
+
+ ///
+ /// Max volume of internal chemical solution storage
+ ///
+ [DataField]
+ public FixedPoint2 ChemicalMaxVolume = FixedPoint2.New(250);
+
+ ///
+ /// Max volume of internal blood storage,
+ /// and starting level of blood.
+ ///
+ [DataField]
+ public FixedPoint2 BloodMaxVolume = FixedPoint2.New(300);
+
+ ///
+ /// Which reagent is considered this entities 'blood'?
+ ///
+ ///
+ /// Slime-people might use slime as their blood or something like that.
+ ///
+ [DataField, AutoNetworkedField]
+ public ProtoId BloodReagent = "Blood";
+
+ ///
+ /// Name/Key that is indexed by.
+ ///
+ [DataField]
+ public string BloodSolutionName = DefaultBloodSolutionName;
+
+ ///
+ /// Name/Key that is indexed by.
+ ///
+ [DataField]
+ public string ChemicalSolutionName = DefaultChemicalsSolutionName;
+
+ ///
+ /// Name/Key that is indexed by.
+ ///
+ [DataField]
+ public string BloodTemporarySolutionName = DefaultBloodTemporarySolutionName;
+
+ ///
+ /// Internal solution for blood storage
+ ///
+ [ViewVariables]
+ public Entity? BloodSolution;
+
+ ///
+ /// Internal solution for reagent storage
+ ///
+ [ViewVariables]
+ public Entity? ChemicalSolution;
+
+ ///
+ /// Temporary blood solution.
+ /// When blood is lost, it goes to this solution, and when this
+ /// solution hits a certain cap, the blood is actually spilled as a puddle.
+ ///
+ [ViewVariables]
+ public Entity? TemporarySolution;
+
+ ///
+ /// Variable that stores the amount of status time added by having a low blood level.
+ ///
+ [DataField, AutoNetworkedField]
+ public TimeSpan StatusTime;
+
+ ///
+ /// Alert to show when bleeding.
+ ///
+ [DataField]
+ public ProtoId BleedingAlert = "Bleed";
+}
diff --git a/Content.Shared/Body/Events/ApplyMetabolicMultiplierEvent.cs b/Content.Shared/Body/Events/ApplyMetabolicMultiplierEvent.cs
index dafc1e49de..1a7b589392 100644
--- a/Content.Shared/Body/Events/ApplyMetabolicMultiplierEvent.cs
+++ b/Content.Shared/Body/Events/ApplyMetabolicMultiplierEvent.cs
@@ -1,4 +1,4 @@
-namespace Content.Shared.Body.Events;
+namespace Content.Shared.Body.Events;
// TODO REFACTOR THIS
// This will cause rates to slowly drift over time due to floating point errors.
diff --git a/Content.Server/Body/Components/BeingGibbedEvent.cs b/Content.Shared/Body/Events/BeingGibbedEvent.cs
similarity index 81%
rename from Content.Server/Body/Components/BeingGibbedEvent.cs
rename to Content.Shared/Body/Events/BeingGibbedEvent.cs
index a010855f78..7ab34f2e18 100644
--- a/Content.Server/Body/Components/BeingGibbedEvent.cs
+++ b/Content.Shared/Body/Events/BeingGibbedEvent.cs
@@ -1,4 +1,4 @@
-namespace Content.Server.Body.Components;
+namespace Content.Shared.Body.Events;
///
/// Raised when a body gets gibbed, before it is deleted.
diff --git a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
new file mode 100644
index 0000000000..3b1674cf3c
--- /dev/null
+++ b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
@@ -0,0 +1,519 @@
+using Content.Shared.Alert;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Events;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reaction;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Damage;
+using Content.Shared.Drunk;
+using Content.Shared.EntityEffects.Effects;
+using Content.Shared.FixedPoint;
+using Content.Shared.Fluids;
+using Content.Shared.Forensics.Components;
+using Content.Shared.HealthExaminable;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Rejuvenate;
+using Content.Shared.Speech.EntitySystems;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Body.Systems;
+
+public abstract class SharedBloodstreamSystem : EntitySystem
+{
+ [Dependency] protected readonly SharedSolutionContainerSystem SolutionContainer = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedPuddleSystem _puddle = default!;
+ [Dependency] private readonly AlertsSystem _alertsSystem = default!;
+ [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly SharedDrunkSystem _drunkSystem = default!;
+ [Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnEntRemoved);
+ SubscribeLocalEvent(OnReactionAttempt);
+ SubscribeLocalEvent>(OnReactionAttempt);
+ SubscribeLocalEvent(OnDamageChanged);
+ SubscribeLocalEvent(OnHealthBeingExamined);
+ SubscribeLocalEvent(OnBeingGibbed);
+ SubscribeLocalEvent(OnApplyMetabolicMultiplier);
+ SubscribeLocalEvent(OnRejuvenate);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var curTime = _timing.CurTime;
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var bloodstream))
+ {
+ if (curTime < bloodstream.NextUpdate)
+ continue;
+
+ bloodstream.NextUpdate += bloodstream.UpdateInterval;
+ DirtyField(uid, bloodstream, nameof(BloodstreamComponent.NextUpdate)); // needs to be dirtied on the client so it can be rerolled during prediction
+
+ if (!SolutionContainer.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
+ continue;
+
+ // Adds blood to their blood level if it is below the maximum; Blood regeneration. Must be alive.
+ if (bloodSolution.Volume < bloodSolution.MaxVolume && !_mobStateSystem.IsDead(uid))
+ {
+ TryModifyBloodLevel((uid, bloodstream), bloodstream.BloodRefreshAmount);
+ }
+
+ // Removes blood from the bloodstream based on bleed amount (bleed rate)
+ // as well as stop their bleeding to a certain extent.
+ if (bloodstream.BleedAmount > 0)
+ {
+ // Blood is removed from the bloodstream at a 1-1 rate with the bleed amount
+ TryModifyBloodLevel((uid, bloodstream), -bloodstream.BleedAmount);
+ // Bleed rate is reduced by the bleed reduction amount in the bloodstream component.
+ TryModifyBleedAmount((uid, bloodstream), -bloodstream.BleedReductionAmount);
+ }
+
+ // deal bloodloss damage if their blood level is below a threshold.
+ var bloodPercentage = GetBloodLevelPercentage((uid, bloodstream));
+ if (bloodPercentage < bloodstream.BloodlossThreshold && !_mobStateSystem.IsDead(uid))
+ {
+ // bloodloss damage is based on the base value, and modified by how low your blood level is.
+ var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage);
+
+ _damageableSystem.TryChangeDamage(uid, amt,
+ ignoreResistances: false, interruptsDoAfters: false);
+
+ // Apply dizziness as a symptom of bloodloss.
+ // The effect is applied in a way that it will never be cleared without being healthy.
+ // Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out
+ _drunkSystem.TryApplyDrunkenness(
+ uid,
+ (float)bloodstream.UpdateInterval.TotalSeconds * 2,
+ applySlur: false);
+ _stutteringSystem.DoStutter(uid, bloodstream.UpdateInterval * 2, refresh: false);
+
+ // storing the drunk and stutter time so we can remove it independently from other effects additions
+ bloodstream.StatusTime += bloodstream.UpdateInterval * 2;
+ DirtyField(uid, bloodstream, nameof(BloodstreamComponent.StatusTime));
+ }
+ else if (!_mobStateSystem.IsDead(uid))
+ {
+ // If they're healthy, we'll try and heal some bloodloss instead.
+ _damageableSystem.TryChangeDamage(
+ uid,
+ bloodstream.BloodlossHealDamage * bloodPercentage,
+ ignoreResistances: true, interruptsDoAfters: false);
+
+ // Remove the drunk effect when healthy. Should only remove the amount of drunk and stutter added by low blood level
+ _drunkSystem.TryRemoveDrunkenessTime(uid, bloodstream.StatusTime.TotalSeconds);
+ _stutteringSystem.DoRemoveStutterTime(uid, bloodstream.StatusTime.TotalSeconds);
+ // Reset the drunk and stutter time to zero
+ bloodstream.StatusTime = TimeSpan.Zero;
+ DirtyField(uid, bloodstream, nameof(BloodstreamComponent.StatusTime));
+ }
+ }
+ }
+
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ ent.Comp.NextUpdate = _timing.CurTime + ent.Comp.UpdateInterval;
+ DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.NextUpdate));
+ }
+
+ // prevent the infamous UdderSystem debug assert, see https://github.com/space-wizards/space-station-14/pull/35314
+ // TODO: find a better solution than copy pasting this into every shared system that caches solution entities
+ private void OnEntRemoved(Entity entity, ref EntRemovedFromContainerMessage args)
+ {
+ // Make sure the removed entity was our contained solution and set it to null
+ if (args.Entity == entity.Comp.BloodSolution?.Owner)
+ entity.Comp.BloodSolution = null;
+
+ if (args.Entity == entity.Comp.ChemicalSolution?.Owner)
+ entity.Comp.ChemicalSolution = null;
+
+ if (args.Entity == entity.Comp.TemporarySolution?.Owner)
+ entity.Comp.TemporarySolution = null;
+ }
+
+ private void OnReactionAttempt(Entity ent, ref ReactionAttemptEvent args)
+ {
+ if (args.Cancelled)
+ return;
+
+ foreach (var effect in args.Reaction.Effects)
+ {
+ switch (effect)
+ {
+ case CreateEntityReactionEffect: // Prevent entities from spawning in the bloodstream
+ case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels.
+ args.Cancelled = true;
+ return;
+ }
+ }
+
+ // The area-reaction effect canceling is part of avoiding smoke-fork-bombs (create two smoke bombs, that when
+ // ingested by mobs create more smoke). This also used to act as a rapid chemical-purge, because all the
+ // reagents would get carried away by the smoke/foam. This does still work for the stomach (I guess people vomit
+ // up the smoke or spawned entities?).
+
+ // TODO apply organ damage instead of just blocking the reaction?
+ // Having cheese-clots form in your veins can't be good for you.
+ }
+
+ private void OnReactionAttempt(Entity ent, ref SolutionRelayEvent args)
+ {
+ if (args.Name != ent.Comp.BloodSolutionName
+ && args.Name != ent.Comp.ChemicalSolutionName
+ && args.Name != ent.Comp.BloodTemporarySolutionName)
+ {
+ return;
+ }
+
+ OnReactionAttempt(ent, ref args.Event);
+ }
+
+ private void OnDamageChanged(Entity ent, ref DamageChangedEvent args)
+ {
+ // The incoming state from the server raises a DamageChangedEvent as well.
+ // But the changes to the bloodstream have also been dirtied,
+ // so we prevent applying them twice.
+ if (_timing.ApplyingState)
+ return;
+
+ if (args.DamageDelta is null || !args.DamageIncreased)
+ {
+ return;
+ }
+
+ // TODO probably cache this or something. humans get hurt a lot
+ if (!_prototypeManager.TryIndex(ent.Comp.DamageBleedModifiers, out var modifiers))
+ return;
+
+ // some reagents may deal and heal different damage types in the same tick, which means DamageIncreased will be true
+ // but we only want to consider the dealt damage when causing bleeding
+ var damage = DamageSpecifier.GetPositive(args.DamageDelta);
+ var bloodloss = DamageSpecifier.ApplyModifierSet(damage, modifiers);
+
+ if (bloodloss.Empty)
+ return;
+
+ // Does the calculation of how much bleed rate should be added/removed, then applies it
+ var oldBleedAmount = ent.Comp.BleedAmount;
+ var total = bloodloss.GetTotal();
+ var totalFloat = total.Float();
+ TryModifyBleedAmount(ent.AsNullable(), totalFloat);
+
+ /// Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5
+ /// The crit chance is currently the bleed rate modifier divided by 25.
+ /// Higher damage weapons have a higher chance to crit!
+
+ // TODO: Replace with RandomPredicted once the engine PR is merged
+ // Use both the receiver and the damage causing entity for the seed so that we have different results for multiple attacks in the same tick
+ var seed = HashCode.Combine((int)_timing.CurTick.Value, GetNetEntity(ent).Id, GetNetEntity(args.Origin)?.Id ?? 0);
+ var rand = new System.Random(seed);
+ var prob = Math.Clamp(totalFloat / 25, 0, 1);
+ if (totalFloat > 0 && rand.Prob(prob))
+ {
+ TryModifyBloodLevel(ent.AsNullable(), -total / 5);
+ _audio.PlayPredicted(ent.Comp.InstantBloodSound, ent, args.Origin);
+ }
+
+ // Heat damage will cauterize, causing the bleed rate to be reduced.
+ else if (totalFloat <= ent.Comp.BloodHealedSoundThreshold && oldBleedAmount > 0)
+ {
+ // Magically, this damage has healed some bleeding, likely
+ // because it's burn damage that cauterized their wounds.
+
+ // We'll play a special sound and popup for feedback.
+ _popup.PopupEntity(Loc.GetString("bloodstream-component-wounds-cauterized"), ent,
+ ent, PopupType.Medium); // only the burned entity can see this
+ _audio.PlayPredicted(ent.Comp.BloodHealedSound, ent, args.Origin);
+ }
+ }
+
+ ///
+ /// Shows text on health examine, based on bleed rate and blood level.
+ ///
+ private void OnHealthBeingExamined(Entity ent, ref HealthBeingExaminedEvent args)
+ {
+ // Shows massively bleeding at 0.75x the max bleed rate.
+ if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.75f)
+ {
+ args.Message.PushNewline();
+ args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-massive-bleeding", ("target", ent.Owner)));
+ }
+ // Shows bleeding message when bleeding above half the max rate, but less than massively.
+ else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.5f)
+ {
+ args.Message.PushNewline();
+ args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-strong-bleeding", ("target", ent.Owner)));
+ }
+ // Shows bleeding message when bleeding above 0.25x the max rate, but less than half the max.
+ else if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount * 0.25f)
+ {
+ args.Message.PushNewline();
+ args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
+ }
+ // Shows bleeding message when bleeding below 0.25x the max cap
+ else if (ent.Comp.BleedAmount > 0)
+ {
+ args.Message.PushNewline();
+ args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-slight-bleeding", ("target", ent.Owner)));
+ }
+
+ // If the mob's blood level is below the damage threshhold, the pale message is added.
+ if (GetBloodLevelPercentage(ent.AsNullable()) < ent.Comp.BloodlossThreshold)
+ {
+ args.Message.PushNewline();
+ args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
+ }
+ }
+
+ private void OnBeingGibbed(Entity ent, ref BeingGibbedEvent args)
+ {
+ SpillAllSolutions(ent.AsNullable());
+ }
+
+ private void OnApplyMetabolicMultiplier(Entity ent, ref ApplyMetabolicMultiplierEvent args)
+ {
+ // TODO REFACTOR THIS
+ // This will slowly drift over time due to floating point errors.
+ // Instead, raise an event with the base rates and allow modifiers to get applied to it.
+ if (args.Apply)
+ ent.Comp.UpdateInterval *= args.Multiplier;
+ else
+ ent.Comp.UpdateInterval /= args.Multiplier;
+
+ DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.UpdateInterval));
+ }
+
+ private void OnRejuvenate(Entity ent, ref RejuvenateEvent args)
+ {
+ TryModifyBleedAmount(ent.AsNullable(), -ent.Comp.BleedAmount);
+
+ if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
+ TryModifyBloodLevel(ent.AsNullable(), bloodSolution.AvailableVolume);
+
+ if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution))
+ SolutionContainer.RemoveAllSolution(ent.Comp.ChemicalSolution.Value);
+ }
+
+ ///
+ /// Returns the current blood level as a percentage (between 0 and 1).
+ ///
+ public float GetBloodLevelPercentage(Entity ent)
+ {
+ if (!Resolve(ent, ref ent.Comp)
+ || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
+ {
+ return 0.0f;
+ }
+
+ return bloodSolution.FillFraction;
+ }
+
+ ///
+ /// Setter for the BloodlossThreshold datafield.
+ ///
+ public void SetBloodLossThreshold(Entity ent, float threshold)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ ent.Comp.BloodlossThreshold = threshold;
+ DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodlossThreshold));
+ }
+
+ ///
+ /// Attempt to transfer a provided solution to internal solution.
+ ///
+ public bool TryAddToChemicals(Entity ent, Solution solution)
+ {
+ if (!Resolve(ent, ref ent.Comp, logMissing: false)
+ || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution))
+ return false;
+
+ if (SolutionContainer.TryAddSolution(ent.Comp.ChemicalSolution.Value, solution))
+ return true;
+
+ return false;
+ }
+
+ ///
+ /// Removes a certain amount of all reagents except of a single excluded one from the bloodstream.
+ ///
+ public bool FlushChemicals(Entity ent, ProtoId? excludedReagentID, FixedPoint2 quantity)
+ {
+ if (!Resolve(ent, ref ent.Comp, logMissing: false)
+ || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution, out var chemSolution))
+ return false;
+
+ for (var i = chemSolution.Contents.Count - 1; i >= 0; i--)
+ {
+ var (reagentId, _) = chemSolution.Contents[i];
+ if (reagentId.Prototype != excludedReagentID)
+ {
+ SolutionContainer.RemoveReagent(ent.Comp.ChemicalSolution.Value, reagentId, quantity);
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Attempts to modify the blood level of this entity directly.
+ ///
+ public bool TryModifyBloodLevel(Entity ent, FixedPoint2 amount)
+ {
+ if (!Resolve(ent, ref ent.Comp, logMissing: false)
+ || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution))
+ return false;
+
+ if (amount >= 0)
+ return SolutionContainer.TryAddReagent(ent.Comp.BloodSolution.Value, ent.Comp.BloodReagent, amount, null, GetEntityBloodData(ent));
+
+ // Removal is more involved,
+ // since we also wanna handle moving it to the temporary solution
+ // and then spilling it if necessary.
+ var newSol = SolutionContainer.SplitSolution(ent.Comp.BloodSolution.Value, -amount);
+
+ if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution))
+ return true;
+
+ tempSolution.AddSolution(newSol, _prototypeManager);
+
+ if (tempSolution.Volume > ent.Comp.BleedPuddleThreshold)
+ {
+ // Pass some of the chemstream into the spilled blood.
+ if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution))
+ {
+ var temp = SolutionContainer.SplitSolution(ent.Comp.ChemicalSolution.Value, tempSolution.Volume / 10);
+ tempSolution.AddSolution(temp, _prototypeManager);
+ }
+
+ _puddle.TrySpillAt(ent.Owner, tempSolution, out _, sound: false);
+
+ tempSolution.RemoveAllSolution();
+ }
+
+ SolutionContainer.UpdateChemicals(ent.Comp.TemporarySolution.Value);
+
+ return true;
+ }
+
+ ///
+ /// Tries to make an entity bleed more or less.
+ ///
+ public bool TryModifyBleedAmount(Entity ent, float amount)
+ {
+ if (!Resolve(ent, ref ent.Comp, logMissing: false))
+ return false;
+
+ ent.Comp.BleedAmount += amount;
+ ent.Comp.BleedAmount = Math.Clamp(ent.Comp.BleedAmount, 0, ent.Comp.MaxBleedAmount);
+
+ DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BleedAmount));
+
+ if (ent.Comp.BleedAmount == 0)
+ _alertsSystem.ClearAlert(ent, ent.Comp.BleedingAlert);
+ else
+ {
+ var severity = (short)Math.Clamp(Math.Round(ent.Comp.BleedAmount, MidpointRounding.ToZero), 0, 10);
+ _alertsSystem.ShowAlert(ent, ent.Comp.BleedingAlert, severity);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Spill all bloodstream solutions into a puddle.
+ /// BLOOD FOR THE BLOOD GOD
+ ///
+ public void SpillAllSolutions(Entity ent)
+ {
+ if (!Resolve(ent, ref ent.Comp))
+ return;
+
+ var tempSol = new Solution();
+
+ if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
+ {
+ tempSol.MaxVolume += bloodSolution.MaxVolume;
+ tempSol.AddSolution(bloodSolution, _prototypeManager);
+ SolutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value);
+ }
+
+ if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution, out var chemSolution))
+ {
+ tempSol.MaxVolume += chemSolution.MaxVolume;
+ tempSol.AddSolution(chemSolution, _prototypeManager);
+ SolutionContainer.RemoveAllSolution(ent.Comp.ChemicalSolution.Value);
+ }
+
+ if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution))
+ {
+ tempSol.MaxVolume += tempSolution.MaxVolume;
+ tempSol.AddSolution(tempSolution, _prototypeManager);
+ SolutionContainer.RemoveAllSolution(ent.Comp.TemporarySolution.Value);
+ }
+
+ _puddle.TrySpillAt(ent, tempSol, out _);
+ }
+
+ ///
+ /// Change what someone's blood is made of, on the fly.
+ ///
+ public void ChangeBloodReagent(Entity ent, ProtoId reagent)
+ {
+ if (!Resolve(ent, ref ent.Comp, logMissing: false)
+ || reagent == ent.Comp.BloodReagent)
+ {
+ return;
+ }
+
+ if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
+ {
+ ent.Comp.BloodReagent = reagent;
+ return;
+ }
+
+ var currentVolume = bloodSolution.RemoveReagent(ent.Comp.BloodReagent, bloodSolution.Volume, ignoreReagentData: true);
+
+ ent.Comp.BloodReagent = reagent;
+ DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReagent));
+
+ if (currentVolume > 0)
+ SolutionContainer.TryAddReagent(ent.Comp.BloodSolution.Value, ent.Comp.BloodReagent, currentVolume, null, GetEntityBloodData(ent));
+ }
+
+ ///
+ /// Get the reagent data for blood that a specific entity should have.
+ ///
+ public List GetEntityBloodData(EntityUid uid)
+ {
+ var bloodData = new List();
+ var dnaData = new DnaData();
+
+ if (TryComp(uid, out var donorComp) && donorComp.DNA != null)
+ dnaData.DNA = donorComp.DNA;
+ else
+ dnaData.DNA = Loc.GetString("forensics-dna-unknown");
+
+ bloodData.Add(dnaData);
+
+ return bloodData;
+ }
+}
diff --git a/Content.Shared/Body/Systems/StomachSystem.cs b/Content.Shared/Body/Systems/StomachSystem.cs
index 8b2df453a0..d0ecb6b93b 100644
--- a/Content.Shared/Body/Systems/StomachSystem.cs
+++ b/Content.Shared/Body/Systems/StomachSystem.cs
@@ -1,6 +1,7 @@
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
+using Content.Shared.Body.Events;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
diff --git a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs
index 86f6edc903..34bc44f1cb 100644
--- a/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs
+++ b/Content.Shared/Chemistry/EntitySystems/HypospraySystem.cs
@@ -1,6 +1,8 @@
using Content.Shared.Administration.Logs;
-using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Body.Components;
+using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Hypospray.Events;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
diff --git a/Content.Shared/Damage/DamageSpecifier.cs b/Content.Shared/Damage/DamageSpecifier.cs
index 51472a56bd..00bd416e1f 100644
--- a/Content.Shared/Damage/DamageSpecifier.cs
+++ b/Content.Shared/Damage/DamageSpecifier.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using System.Text.Json.Serialization;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
@@ -77,6 +78,11 @@ namespace Content.Shared.Damage
[JsonIgnore]
public bool Empty => DamageDict.Count == 0;
+ public override string ToString()
+ {
+ return "DamageSpecifier(" + string.Join("; ", DamageDict.Select(x => x.Key + ":" + x.Value)) + ")";
+ }
+
#region constructors
///
/// Constructor that just results in an empty dictionary.
diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs
index 31426f6bd0..70fbc46806 100644
--- a/Content.Shared/Damage/Systems/DamageableSystem.cs
+++ b/Content.Shared/Damage/Systems/DamageableSystem.cs
@@ -353,7 +353,7 @@ namespace Content.Shared.Damage
// Has the damage actually changed?
DamageSpecifier newDamage = new() { DamageDict = new(state.DamageDict) };
- var delta = component.Damage - newDamage;
+ var delta = newDamage - component.Damage;
delta.TrimZeros();
if (!delta.Empty)
diff --git a/Content.Shared/Medical/Healing/HealingComponent.cs b/Content.Shared/Medical/Healing/HealingComponent.cs
new file mode 100644
index 0000000000..53358fd28d
--- /dev/null
+++ b/Content.Shared/Medical/Healing/HealingComponent.cs
@@ -0,0 +1,65 @@
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Medical.Healing;
+
+///
+/// Applies a damage change to the target when used in an interaction.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class HealingComponent : Component
+{
+ ///
+ /// The amount of damage to heal per use.
+ ///
+ [DataField(required: true), AutoNetworkedField]
+ public DamageSpecifier Damage = default!;
+
+ ///
+ /// This should generally be negative,
+ /// since you're, like, trying to heal damage.
+ ///
+ [DataField, AutoNetworkedField]
+ public float BloodlossModifier = 0.0f;
+
+ ///
+ /// Restore missing blood.
+ ///
+ [DataField, AutoNetworkedField]
+ public float ModifyBloodLevel = 0.0f;
+
+ ///
+ /// The supported damage types are specified using a s. For a
+ /// HealingComponent this filters what damage container type this component should work on. If null,
+ /// all damage container types are supported.
+ ///
+ [DataField, AutoNetworkedField]
+ public List>? DamageContainers;
+
+ ///
+ /// How long it takes to apply the damage.
+ ///
+ [DataField, AutoNetworkedField]
+ public float Delay = 3f;
+
+ ///
+ /// Delay multiplier when healing yourself.
+ ///
+ [DataField, AutoNetworkedField]
+ public float SelfHealPenaltyMultiplier = 3f;
+
+ ///
+ /// Sound played on healing begin.
+ ///
+ [DataField]
+ public SoundSpecifier? HealingBeginSound = null;
+
+ ///
+ /// Sound played on healing end.
+ ///
+ [DataField]
+ public SoundSpecifier? HealingEndSound = null;
+}
diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Shared/Medical/Healing/HealingSystem.cs
similarity index 54%
rename from Content.Server/Medical/HealingSystem.cs
rename to Content.Shared/Medical/Healing/HealingSystem.cs
index c18f29d2fb..74cb8881f4 100644
--- a/Content.Server/Medical/HealingSystem.cs
+++ b/Content.Shared/Medical/Healing/HealingSystem.cs
@@ -1,9 +1,6 @@
-using Content.Server.Administration.Logs;
-using Content.Server.Body.Components;
-using Content.Server.Body.Systems;
-using Content.Server.Medical.Components;
-using Content.Server.Popups;
-using Content.Server.Stack;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.Database;
@@ -12,77 +9,74 @@ using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
-using Content.Shared.Medical;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Stacks;
using Robust.Shared.Audio.Systems;
-using Robust.Shared.Random;
-using Robust.Shared.Audio;
-namespace Content.Server.Medical;
+namespace Content.Shared.Medical.Healing;
public sealed class HealingSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly IAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
- [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
+ [Dependency] private readonly SharedBloodstreamSystem _bloodstreamSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
- [Dependency] private readonly StackSystem _stacks = default!;
+ [Dependency] private readonly SharedStackSystem _stacks = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
public override void Initialize()
{
base.Initialize();
+
SubscribeLocalEvent(OnHealingUse);
SubscribeLocalEvent(OnHealingAfterInteract);
SubscribeLocalEvent(OnDoAfter);
}
- private void OnDoAfter(Entity entity, ref HealingDoAfterEvent args)
+ private void OnDoAfter(Entity target, ref HealingDoAfterEvent args)
{
- var dontRepeat = false;
-
- if (!TryComp(args.Used, out HealingComponent? healing))
- return;
if (args.Handled || args.Cancelled)
return;
+ if (!TryComp(args.Used, out HealingComponent? healing))
+ return;
+
if (healing.DamageContainers is not null &&
- entity.Comp.DamageContainerID is not null &&
- !healing.DamageContainers.Contains(entity.Comp.DamageContainerID))
+ target.Comp.DamageContainerID is not null &&
+ !healing.DamageContainers.Contains(target.Comp.DamageContainerID.Value))
{
return;
}
+ TryComp(target, out var bloodstream);
+
// Heal some bloodloss damage.
- if (healing.BloodlossModifier != 0)
+ if (healing.BloodlossModifier != 0 && bloodstream != null)
{
- if (!TryComp(entity, out var bloodstream))
- return;
var isBleeding = bloodstream.BleedAmount > 0;
- _bloodstreamSystem.TryModifyBleedAmount(entity.Owner, healing.BloodlossModifier);
+ _bloodstreamSystem.TryModifyBleedAmount((target.Owner, bloodstream), healing.BloodlossModifier);
if (isBleeding != bloodstream.BleedAmount > 0)
{
- var popup = (args.User == entity.Owner)
+ var popup = (args.User == target.Owner)
? Loc.GetString("medical-item-stop-bleeding-self")
- : Loc.GetString("medical-item-stop-bleeding", ("target", Identity.Entity(entity.Owner, EntityManager)));
- _popupSystem.PopupEntity(popup, entity, args.User);
+ : Loc.GetString("medical-item-stop-bleeding", ("target", Identity.Entity(target.Owner, EntityManager)));
+ _popupSystem.PopupClient(popup, target, args.User);
}
}
// Restores missing blood
- if (healing.ModifyBloodLevel != 0)
- _bloodstreamSystem.TryModifyBloodLevel(entity.Owner, healing.ModifyBloodLevel);
+ if (healing.ModifyBloodLevel != 0 && bloodstream != null)
+ _bloodstreamSystem.TryModifyBloodLevel((target.Owner, bloodstream), healing.ModifyBloodLevel);
- var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage * _damageable.UniversalTopicalsHealModifier, true, origin: args.Args.User);
+ var healed = _damageable.TryChangeDamage(target.Owner, healing.Damage * _damageable.UniversalTopicalsHealModifier, true, origin: args.Args.User);
if (healed == null && healing.BloodlossModifier != 0)
return;
@@ -90,7 +84,7 @@ public sealed class HealingSystem : EntitySystem
var total = healed?.GetTotal() ?? FixedPoint2.Zero;
// Re-verify that we can heal the damage.
-
+ var dontRepeat = false;
if (TryComp(args.Used.Value, out var stackComp))
{
_stacks.Use(args.Used.Value, 1, stackComp);
@@ -100,13 +94,13 @@ public sealed class HealingSystem : EntitySystem
}
else
{
- QueueDel(args.Used.Value);
+ PredictedQueueDel(args.Used.Value);
}
- if (entity.Owner != args.User)
+ if (target.Owner != args.User)
{
_adminLogger.Add(LogType.Healed,
- $"{ToPrettyString(args.User):user} healed {ToPrettyString(entity.Owner):target} for {total:damage} damage");
+ $"{ToPrettyString(args.User):user} healed {ToPrettyString(target.Owner):target} for {total:damage} damage");
}
else
{
@@ -114,19 +108,19 @@ public sealed class HealingSystem : EntitySystem
$"{ToPrettyString(args.User):user} healed themselves for {total:damage} damage");
}
- _audio.PlayPvs(healing.HealingEndSound, entity.Owner);
+ _audio.PlayPredicted(healing.HealingEndSound, target.Owner, args.User);
// Logic to determine the whether or not to repeat the healing action
- args.Repeat = (HasDamage(entity, healing) && !dontRepeat);
+ args.Repeat = HasDamage((args.Used.Value, healing), target) && !dontRepeat;
if (!args.Repeat && !dontRepeat)
- _popupSystem.PopupEntity(Loc.GetString("medical-item-finished-using", ("item", args.Used)), entity.Owner, args.User);
+ _popupSystem.PopupClient(Loc.GetString("medical-item-finished-using", ("item", args.Used)), target.Owner, args.User);
args.Handled = true;
}
- private bool HasDamage(Entity ent, HealingComponent healing)
+ private bool HasDamage(Entity healing, Entity target)
{
- var damageableDict = ent.Comp.Damage.DamageDict;
- var healingDict = healing.Damage.DamageDict;
+ var damageableDict = target.Comp.Damage.DamageDict;
+ var healingDict = healing.Comp.Damage.DamageDict;
foreach (var type in healingDict)
{
if (damageableDict[type.Key].Value > 0)
@@ -135,18 +129,18 @@ public sealed class HealingSystem : EntitySystem
}
}
- if (TryComp(ent, out var bloodstream))
+ if (TryComp(target, out var bloodstream))
{
// Is ent missing blood that we can restore?
- if (healing.ModifyBloodLevel > 0
- && _solutionContainerSystem.ResolveSolution(ent.Owner, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)
+ if (healing.Comp.ModifyBloodLevel > 0
+ && _solutionContainerSystem.ResolveSolution(target.Owner, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution)
&& bloodSolution.Volume < bloodSolution.MaxVolume)
{
return true;
}
// Is ent bleeding and can we stop it?
- if (healing.BloodlossModifier < 0 && bloodstream.BleedAmount > 0)
+ if (healing.Comp.BloodlossModifier < 0 && bloodstream.BleedAmount > 0)
{
return true;
}
@@ -155,64 +149,64 @@ public sealed class HealingSystem : EntitySystem
return false;
}
- private void OnHealingUse(Entity entity, ref UseInHandEvent args)
+ private void OnHealingUse(Entity healing, ref UseInHandEvent args)
{
if (args.Handled)
return;
- if (TryHeal(entity, args.User, args.User, entity.Comp))
+ if (TryHeal(healing, args.User, args.User))
args.Handled = true;
}
- private void OnHealingAfterInteract(Entity entity, ref AfterInteractEvent args)
+ private void OnHealingAfterInteract(Entity healing, ref AfterInteractEvent args)
{
if (args.Handled || !args.CanReach || args.Target == null)
return;
- if (TryHeal(entity, args.User, args.Target.Value, entity.Comp))
+ if (TryHeal(healing, args.Target.Value, args.User))
args.Handled = true;
}
- private bool TryHeal(EntityUid uid, EntityUid user, EntityUid target, HealingComponent component)
+ private bool TryHeal(Entity healing, Entity target, EntityUid user)
{
- if (!TryComp(target, out var targetDamage))
+ if (!Resolve(target, ref target.Comp, false))
return false;
- if (component.DamageContainers is not null &&
- targetDamage.DamageContainerID is not null &&
- !component.DamageContainers.Contains(targetDamage.DamageContainerID))
+ if (healing.Comp.DamageContainers is not null &&
+ target.Comp.DamageContainerID is not null &&
+ !healing.Comp.DamageContainers.Contains(target.Comp.DamageContainerID.Value))
{
return false;
}
- if (user != target && !_interactionSystem.InRangeUnobstructed(user, target, popup: true))
+ if (user != target.Owner && !_interactionSystem.InRangeUnobstructed(user, target.Owner, popup: true))
return false;
- if (TryComp(uid, out var stack) && stack.Count < 1)
+ if (TryComp(healing, out var stack) && stack.Count < 1)
return false;
- if (!HasDamage((target, targetDamage), component))
+ if (!HasDamage(healing, target!))
{
- _popupSystem.PopupEntity(Loc.GetString("medical-item-cant-use", ("item", uid)), uid, user);
+ _popupSystem.PopupClient(Loc.GetString("medical-item-cant-use", ("item", healing.Owner)), healing, user);
return false;
}
- _audio.PlayPvs(component.HealingBeginSound, uid);
+ _audio.PlayPredicted(healing.Comp.HealingBeginSound, healing, user);
- var isNotSelf = user != target;
+ var isNotSelf = user != target.Owner;
if (isNotSelf)
{
- var msg = Loc.GetString("medical-item-popup-target", ("user", Identity.Entity(user, EntityManager)), ("item", uid));
+ var msg = Loc.GetString("medical-item-popup-target", ("user", Identity.Entity(user, EntityManager)), ("item", healing.Owner));
_popupSystem.PopupEntity(msg, target, target, PopupType.Medium);
}
var delay = isNotSelf
- ? component.Delay
- : component.Delay * GetScaledHealingPenalty(user, component);
+ ? healing.Comp.Delay
+ : healing.Comp.Delay * GetScaledHealingPenalty(healing);
var doAfterEventArgs =
- new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), target, target: target, used: uid)
+ new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), target, target: target, used: healing)
{
// Didn't break on damage as they may be trying to prevent it and
// not being able to heal your own ticking damage would be frustrating.
@@ -231,18 +225,18 @@ public sealed class HealingSystem : EntitySystem
///
///
///
- public float GetScaledHealingPenalty(EntityUid uid, HealingComponent component)
+ public float GetScaledHealingPenalty(Entity healing)
{
- var output = component.Delay;
- if (!TryComp(uid, out var mobThreshold) ||
- !TryComp(uid, out var damageable))
+ var output = healing.Comp.Delay;
+ if (!TryComp(healing, out var mobThreshold) ||
+ !TryComp(healing, out var damageable))
return output;
- if (!_mobThresholdSystem.TryGetThresholdForState(uid, MobState.Critical, out var amount, mobThreshold))
+ if (!_mobThresholdSystem.TryGetThresholdForState(healing, MobState.Critical, out var amount, mobThreshold))
return 1;
- var percentDamage = (float) (damageable.TotalDamage / amount);
+ var percentDamage = (float)(damageable.TotalDamage / amount);
//basically make it scale from 1 to the multiplier.
- var modifier = percentDamage * (component.SelfHealPenaltyMultiplier - 1) + 1;
+ var modifier = percentDamage * (healing.Comp.SelfHealPenaltyMultiplier - 1) + 1;
return Math.Max(modifier, 1);
}
}
diff --git a/Resources/Locale/en-US/medical/components/healing-component.ftl b/Resources/Locale/en-US/medical/components/healing-component.ftl
index 20ad23dcb2..d206150a0e 100644
--- a/Resources/Locale/en-US/medical/components/healing-component.ftl
+++ b/Resources/Locale/en-US/medical/components/healing-component.ftl
@@ -1,5 +1,5 @@
-medical-item-finished-using = You have finished healing with the {$item}
-medical-item-cant-use = There is no damage you can heal with the {$item}
-medical-item-stop-bleeding = {CAPITALIZE($target)} has stopped bleeding
-medical-item-stop-bleeding-self = You have stopped bleeding
+medical-item-finished-using = You have finished healing with the {$item}.
+medical-item-cant-use = There is no damage you can heal with the {$item}.
+medical-item-stop-bleeding = {CAPITALIZE($target)} has stopped bleeding.
+medical-item-stop-bleeding-self = You have stopped bleeding.
medical-item-popup-target = {CAPITALIZE(THE($user))} is trying to heal you with the {$item}!
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
index 98565931fd..4a05817646 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
@@ -257,7 +257,7 @@
damage:
types:
Bloodloss: -0.5 #lowers bloodloss damage
- ModifyBloodLevel: 15 #restores about 5% blood per use on standard humanoids.
+ modifyBloodLevel: 15 #restores about 5% blood per use on standard humanoids.
healingBeginSound:
path: "/Audio/Items/Medical/brutepack_begin.ogg"
params: