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: