diff --git a/Content.Server/Body/Components/BloodstreamComponent.cs b/Content.Server/Body/Components/BloodstreamComponent.cs
index 612ef6e153..9ba2ea314f 100644
--- a/Content.Server/Body/Components/BloodstreamComponent.cs
+++ b/Content.Server/Body/Components/BloodstreamComponent.cs
@@ -1,28 +1,136 @@
-using Content.Server.Atmos;
using Content.Server.Body.Systems;
-using Content.Shared.Atmos;
using Content.Shared.Chemistry.Components;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
-using Robust.Shared.Analyzers;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization.Manager.Attributes;
-using Robust.Shared.ViewVariables;
+using Content.Shared.Sound;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Body.Components
{
[RegisterComponent, Friend(typeof(BloodstreamSystem))]
public sealed class BloodstreamComponent : Component
{
+ public static string DefaultChemicalsSolutionName = "chemicals";
+ public static string DefaultBloodSolutionName = "bloodstream";
+ public static string DefaultBloodTemporarySolutionName = "bloodstreamTemporary";
+
+ public float AccumulatedFrametime = 0.0f;
+
///
- /// Max volume of internal solution storage
+ /// 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.
///
- [DataField("maxVolume")]
- public FixedPoint2 InitialMaxVolume = FixedPoint2.New(250);
+ ///
+ /// This generally corresponds to an amount of damage and can't go above 100.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float BleedAmount;
+
+ ///
+ /// How much should bleeding should be reduced every update interval?
+ ///
+ [DataField("bleedReductionAmount")]
+ public float BleedReductionAmount = 1.0f;
+
+ ///
+ /// What percentage of current blood is necessary to avoid dealing blood loss damage?
+ ///
+ [DataField("bloodlossThreshold")]
+ public float BloodlossThreshold = 0.9f;
+
+ ///
+ /// The base bloodloss damage to be incurred if below
+ ///
+ [DataField("bloodlossDamage", required: true)]
+ public DamageSpecifier BloodlossDamage = default!;
+
+ ///
+ /// The base bloodloss damage to be healed if above
+ ///
+ [DataField("bloodlossHealDamage", required: true)]
+ public DamageSpecifier BloodlossHealDamage = default!;
+
+ ///
+ /// How frequently should this bloodstream update, in seconds?
+ ///
+ [DataField("updateInterval")]
+ public float UpdateInterval = 5.0f;
+
+ // 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("bloodRefreshAmount")]
+ public float BloodRefreshAmount = 0.2f;
+
+ ///
+ /// How much blood needs to be in the temporary solution in order to create a puddle?
+ ///
+ [DataField("bleedPuddleThreshold")]
+ public FixedPoint2 BleedPuddleThreshold = 10.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("damageBleedModifiers", customTypeSerializer:typeof(PrototypeIdSerializer))]
+ public string DamageBleedModifiers = "BloodlossHuman";
+
+ ///
+ /// The sound to be played when a weapon instantly deals blood loss damage.
+ ///
+ [DataField("instantBloodSound")]
+ public SoundSpecifier InstantBloodSound = new SoundCollectionSpecifier("blood");
+
+ // TODO probably damage bleed thresholds.
+
+ ///
+ /// Max volume of internal chemical solution storage
+ ///
+ [DataField("chemicalMaxVolume")]
+ public FixedPoint2 ChemicalMaxVolume = FixedPoint2.New(250);
+
+ ///
+ /// Max volume of internal blood storage,
+ /// and starting level of blood.
+ ///
+ [DataField("bloodMaxVolume")]
+ 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("bloodReagent")]
+ public string BloodReagent = "Blood";
///
/// Internal solution for reagent storage
///
[ViewVariables(VVAccess.ReadWrite)]
- public Solution Solution = default!;
+ public Solution ChemicalSolution = default!;
+
+ ///
+ /// Internal solution for blood storage
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public Solution BloodSolution = default!;
+
+ ///
+ /// 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(VVAccess.ReadWrite)]
+ public Solution BloodTemporarySolution = default!;
}
}
diff --git a/Content.Server/Body/Components/MetabolizerComponent.cs b/Content.Server/Body/Components/MetabolizerComponent.cs
index fb7fe838db..bd1e6f6ac2 100644
--- a/Content.Server/Body/Components/MetabolizerComponent.cs
+++ b/Content.Server/Body/Components/MetabolizerComponent.cs
@@ -30,7 +30,7 @@ namespace Content.Server.Body.Components
/// From which solution will this metabolizer attempt to metabolize chemicals
///
[DataField("solution")]
- public string SolutionName { get; set; } = BloodstreamSystem.DefaultSolutionName;
+ public string SolutionName { get; set; } = BloodstreamComponent.DefaultChemicalsSolutionName;
///
/// Does this component use a solution on it's parent entity (the body) or itself
diff --git a/Content.Server/Body/Components/StomachComponent.cs b/Content.Server/Body/Components/StomachComponent.cs
index ec7227f720..3ec931ab84 100644
--- a/Content.Server/Body/Components/StomachComponent.cs
+++ b/Content.Server/Body/Components/StomachComponent.cs
@@ -24,7 +24,7 @@ namespace Content.Server.Body.Components
/// What solution should this stomach push reagents into, on the body?
///
[DataField("bodySolutionName")]
- public string BodySolutionName = BloodstreamSystem.DefaultSolutionName;
+ public string BodySolutionName = BloodstreamComponent.DefaultChemicalsSolutionName;
///
/// Initial internal solution storage volume
diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs
index 518b309b7d..b0c38dd1ca 100644
--- a/Content.Server/Body/Systems/BloodstreamSystem.cs
+++ b/Content.Server/Body/Systems/BloodstreamSystem.cs
@@ -1,47 +1,193 @@
-using System;
-using Content.Server.Atmos;
-using Content.Server.Atmos.EntitySystems;
+using System.Linq;
using Content.Server.Body.Components;
using Content.Server.Chemistry.EntitySystems;
+using Content.Server.Fluids.EntitySystems;
using Content.Shared.Chemistry.Components;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.Examine;
using Content.Shared.FixedPoint;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
+using Content.Shared.MobState.Components;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
namespace Content.Server.Body.Systems;
public sealed class BloodstreamSystem : EntitySystem
{
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
- [Dependency] private readonly AtmosphereSystem _atmosSystem = default!;
- [Dependency] private readonly RespiratorSystem _respiratorSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly SpillableSystem _spillableSystem = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
- public static string DefaultSolutionName = "bloodstream";
+ // TODO here
+ // Update over time. Modify bloodloss damage in accordance with (amount of blood / max blood level), and reduce bleeding over time
+ // Sub to damage changed event and modify bloodloss if incurring large hits of slashing/piercing
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnDamageChanged);
+ SubscribeLocalEvent(OnExamined);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ foreach (var bloodstream in EntityManager.EntityQuery())
+ {
+ bloodstream.AccumulatedFrametime += frameTime;
+
+ if (bloodstream.AccumulatedFrametime < bloodstream.UpdateInterval)
+ continue;
+
+ bloodstream.AccumulatedFrametime -= bloodstream.UpdateInterval;
+
+ var uid = bloodstream.Owner;
+ if (TryComp(uid, out var state) && state.IsDead())
+ continue;
+
+ // First, let's refresh their blood if possible.
+ if (bloodstream.BloodSolution.CurrentVolume < bloodstream.BloodSolution.MaxVolume)
+ TryModifyBloodLevel(uid, bloodstream.BloodRefreshAmount, bloodstream);
+
+ // Next, let's remove some blood from them according to their bleed level.
+ // as well as stop their bleeding to a certain extent.
+ if (bloodstream.BleedAmount > 0)
+ {
+ TryModifyBloodLevel(uid, (-bloodstream.BleedAmount) / 10, bloodstream);
+ TryModifyBleedAmount(uid, -bloodstream.BleedReductionAmount, bloodstream);
+ }
+
+ // Next, we'll deal some bloodloss damage if their blood level is below a threshold.
+ var bloodPercentage = GetBloodLevelPercentage(uid, bloodstream);
+ if (bloodPercentage < bloodstream.BloodlossThreshold)
+ {
+ // TODO use a better method for determining this.
+ var amt = bloodstream.BloodlossDamage / bloodPercentage;
+
+ _damageableSystem.TryChangeDamage(uid, amt, true, false);
+ }
+ else
+ {
+ // If they're healthy, we'll try and heal some bloodloss instead.
+ _damageableSystem.TryChangeDamage(uid, bloodstream.BloodlossHealDamage * bloodPercentage, true, false);
+ }
+ }
}
private void OnComponentInit(EntityUid uid, BloodstreamComponent component, ComponentInit args)
{
- component.Solution = _solutionContainerSystem.EnsureSolution(uid, DefaultSolutionName);
- if (component.Solution != null)
+ component.ChemicalSolution = _solutionContainerSystem.EnsureSolution(uid, BloodstreamComponent.DefaultChemicalsSolutionName);
+ component.BloodSolution = _solutionContainerSystem.EnsureSolution(uid, BloodstreamComponent.DefaultBloodSolutionName);
+ component.BloodTemporarySolution = _solutionContainerSystem.EnsureSolution(uid, BloodstreamComponent.DefaultBloodTemporarySolutionName);
+
+ component.ChemicalSolution.MaxVolume = component.ChemicalMaxVolume;
+ component.BloodSolution.MaxVolume = component.BloodMaxVolume;
+ component.BloodTemporarySolution.MaxVolume = component.BleedPuddleThreshold * 2; // give some leeway
+
+ // Fill blood solution with BLOOD
+ _solutionContainerSystem.TryAddReagent(uid, component.BloodSolution, component.BloodReagent,
+ component.BloodMaxVolume, out _);
+ }
+
+ private void OnDamageChanged(EntityUid uid, BloodstreamComponent component, DamageChangedEvent args)
+ {
+ if (args.DamageDelta is null)
+ return;
+
+ // TODO probably cache this or something. humans get hurt a lot
+ if (!_prototypeManager.TryIndex(component.DamageBleedModifiers, out var modifiers))
+ return;
+
+ var bloodloss = DamageSpecifier.ApplyModifierSet(args.DamageDelta, modifiers);
+
+ if (bloodloss.Empty)
+ return;
+
+ var total = bloodloss.Total;
+ var totalFloat = total.Float();
+ TryModifyBleedAmount(uid, totalFloat, component);
+
+ var prob = Math.Clamp(totalFloat / 50, 0, 1);
+ if (_robustRandom.Prob(prob))
{
- component.Solution.MaxVolume = component.InitialMaxVolume;
+ // This is gonna hurt.
+ TryModifyBloodLevel(uid, (-total) / 5, component);
+ SoundSystem.Play(Filter.Pvs(uid), component.InstantBloodSound.GetSound(), uid, AudioParams.Default);
+ }
+ }
+
+ private void OnExamined(EntityUid uid, BloodstreamComponent component, ExaminedEvent args)
+ {
+ if (GetBloodLevelPercentage(uid, component) < component.BloodlossThreshold)
+ {
+ args.PushMarkup(Loc.GetString("bloodstream-component-looks-pale", ("target", uid)));
}
}
///
/// Attempt to transfer provided solution to internal solution.
///
- public bool TryAddToBloodstream(EntityUid uid, Solution solution, BloodstreamComponent? component=null)
+ public bool TryAddToChemicals(EntityUid uid, Solution solution, BloodstreamComponent? component=null)
{
if (!Resolve(uid, ref component, false))
return false;
- return _solutionContainerSystem.TryAddSolution(uid, component.Solution, solution);
+ return _solutionContainerSystem.TryAddSolution(uid, component.ChemicalSolution, solution);
+ }
+
+ public float GetBloodLevelPercentage(EntityUid uid, BloodstreamComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return 0.0f;
+
+ return (component.BloodSolution.CurrentVolume / component.BloodSolution.MaxVolume).Float();
+ }
+
+ ///
+ /// 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, false))
+ return false;
+
+ if (amount >= 0)
+ return _solutionContainerSystem.TryAddReagent(uid, component.BloodSolution, component.BloodReagent, amount, out _);
+
+ // Removal is more involved,
+ // since we also wanna handle moving it to the temporary solution
+ // and then spilling it if necessary.
+ var newSol = component.BloodSolution.SplitSolution(-amount);
+ component.BloodTemporarySolution.AddSolution(newSol);
+
+ if (component.BloodTemporarySolution.MaxVolume > component.BleedPuddleThreshold)
+ {
+ _spillableSystem.SpillAt(uid, component.BloodTemporarySolution, "PuddleBlood", false);
+ component.BloodTemporarySolution.RemoveAllSolution();
+ }
+
+ 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, false))
+ return false;
+
+ component.BleedAmount += amount;
+ component.BleedAmount = Math.Clamp(component.BleedAmount, 0, 40);
+
+ return true;
}
}
diff --git a/Content.Server/Chemistry/Components/FoamSolutionAreaEffectComponent.cs b/Content.Server/Chemistry/Components/FoamSolutionAreaEffectComponent.cs
index 94a7aac416..925c2c92b0 100644
--- a/Content.Server/Chemistry/Components/FoamSolutionAreaEffectComponent.cs
+++ b/Content.Server/Chemistry/Components/FoamSolutionAreaEffectComponent.cs
@@ -61,10 +61,10 @@ namespace Content.Server.Chemistry.Components
var cloneSolution = solution.Clone();
var transferAmount = FixedPoint2.Min(cloneSolution.TotalVolume * solutionFraction * (1 - protection),
- bloodstream.Solution.AvailableVolume);
+ bloodstream.ChemicalSolution.AvailableVolume);
var transferSolution = cloneSolution.SplitSolution(transferAmount);
- bloodstreamSys.TryAddToBloodstream(entity, transferSolution, bloodstream);
+ bloodstreamSys.TryAddToChemicals(entity, transferSolution, bloodstream);
}
protected override void OnKill()
diff --git a/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs b/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs
index a59ec45b0b..347fdd0143 100644
--- a/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs
+++ b/Content.Server/Chemistry/Components/SmokeSolutionAreaEffectComponent.cs
@@ -41,7 +41,7 @@ namespace Content.Server.Chemistry.Components
var chemistry = EntitySystem.Get();
var cloneSolution = solution.Clone();
- var transferAmount = FixedPoint2.Min(cloneSolution.TotalVolume * solutionFraction, bloodstream.Solution.AvailableVolume);
+ var transferAmount = FixedPoint2.Min(cloneSolution.TotalVolume * solutionFraction, bloodstream.ChemicalSolution.AvailableVolume);
var transferSolution = cloneSolution.SplitSolution(transferAmount);
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
@@ -51,7 +51,7 @@ namespace Content.Server.Chemistry.Components
}
var bloodstreamSys = EntitySystem.Get();
- bloodstreamSys.TryAddToBloodstream(entity, transferSolution, bloodstream);
+ bloodstreamSys.TryAddToChemicals(entity, transferSolution, bloodstream);
}
diff --git a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Injector.cs b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Injector.cs
index 50fc86964a..a94ac2fd96 100644
--- a/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Injector.cs
+++ b/Content.Server/Chemistry/EntitySystems/ChemistrySystem.Injector.cs
@@ -44,7 +44,7 @@ public sealed partial class ChemistrySystem
}
private void UseInjector(EntityUid target, EntityUid user, InjectorComponent component)
- {
+ {
// Handle injecting/drawing for solutions
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
{
@@ -247,7 +247,7 @@ public sealed partial class ChemistrySystem
private void TryInjectIntoBloodstream(InjectorComponent component, BloodstreamComponent targetBloodstream, EntityUid user)
{
// Get transfer amount. May be smaller than _transferAmount if not enough room
- var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetBloodstream.Solution.AvailableVolume);
+ var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetBloodstream.ChemicalSolution.AvailableVolume);
if (realTransferAmount <= 0)
{
@@ -257,9 +257,9 @@ public sealed partial class ChemistrySystem
}
// Move units from attackSolution to targetSolution
- var removedSolution = _solutions.SplitSolution(user, targetBloodstream.Solution, realTransferAmount);
+ var removedSolution = _solutions.SplitSolution(user, targetBloodstream.ChemicalSolution, realTransferAmount);
- _blood.TryAddToBloodstream((targetBloodstream).Owner, removedSolution, targetBloodstream);
+ _blood.TryAddToChemicals((targetBloodstream).Owner, removedSolution, targetBloodstream);
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
diff --git a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnCollideSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnCollideSystem.cs
index e26ca86fac..bbebedb4a4 100644
--- a/Content.Server/Chemistry/EntitySystems/SolutionInjectOnCollideSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/SolutionInjectOnCollideSystem.cs
@@ -38,7 +38,7 @@ namespace Content.Server.Chemistry.EntitySystems
var solToInject = solRemoved.SplitSolution(solRemovedVol * component.TransferEfficiency);
- _bloodstreamSystem.TryAddToBloodstream((args.OtherFixture.Body).Owner, solToInject, bloodstream);
+ _bloodstreamSystem.TryAddToChemicals((args.OtherFixture.Body).Owner, solToInject, bloodstream);
}
}
}
diff --git a/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs b/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs
new file mode 100644
index 0000000000..ba3dde02be
--- /dev/null
+++ b/Content.Server/Chemistry/ReagentEffects/ModifyBleedAmount.cs
@@ -0,0 +1,25 @@
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.FixedPoint;
+
+namespace Content.Server.Chemistry.ReagentEffects;
+
+public sealed class ModifyBleedAmount : ReagentEffect
+{
+ [DataField("scaled")]
+ public bool Scaled = false;
+
+ [DataField("amount")]
+ public float Amount = -1.0f;
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ if (args.EntityManager.TryGetComponent(args.SolutionEntity, out var blood))
+ {
+ var sys = EntitySystem.Get();
+ var amt = Scaled ? Amount * args.Quantity.Float() : Amount;
+ sys.TryModifyBleedAmount(args.SolutionEntity, amt, blood);
+ }
+ }
+}
diff --git a/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs b/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs
new file mode 100644
index 0000000000..5b0ffe5c40
--- /dev/null
+++ b/Content.Server/Chemistry/ReagentEffects/ModifyBloodLevel.cs
@@ -0,0 +1,25 @@
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.FixedPoint;
+
+namespace Content.Server.Chemistry.ReagentEffects;
+
+public sealed class ModifyBloodLevel : ReagentEffect
+{
+ [DataField("scaled")]
+ public bool Scaled = false;
+
+ [DataField("amount")]
+ public FixedPoint2 Amount = 1.0f;
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ if (args.EntityManager.TryGetComponent(args.SolutionEntity, out var blood))
+ {
+ var sys = EntitySystem.Get();
+ var amt = Scaled ? Amount * args.Quantity : Amount;
+ sys.TryModifyBloodLevel(args.SolutionEntity, amt, blood);
+ }
+ }
+}
diff --git a/Content.Server/Medical/Components/HealingComponent.cs b/Content.Server/Medical/Components/HealingComponent.cs
index 78bceb21a9..0ae08a435b 100644
--- a/Content.Server/Medical/Components/HealingComponent.cs
+++ b/Content.Server/Medical/Components/HealingComponent.cs
@@ -18,6 +18,14 @@ namespace Content.Server.Medical.Components
[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;
+
///
/// 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,
diff --git a/Content.Server/Medical/HealingSystem.cs b/Content.Server/Medical/HealingSystem.cs
index 3c0d7c1c2f..cc7c05b141 100644
--- a/Content.Server/Medical/HealingSystem.cs
+++ b/Content.Server/Medical/HealingSystem.cs
@@ -1,5 +1,6 @@
using System.Threading;
using Content.Server.Administration.Logs;
+using Content.Server.Body.Systems;
using Content.Server.DoAfter;
using Content.Server.Medical.Components;
using Content.Server.Stack;
@@ -17,6 +18,7 @@ public sealed class HealingSystem : EntitySystem
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly AdminLogSystem _logs = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
+ [Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly StackSystem _stacks = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
@@ -37,6 +39,12 @@ public sealed class HealingSystem : EntitySystem
if (component.DamageContainerID is not null &&
!component.DamageContainerID.Equals(component.DamageContainerID)) return;
+ if (args.Component.BloodlossModifier != 0)
+ {
+ // Heal some bloodloss damage.
+ _bloodstreamSystem.TryModifyBleedAmount(uid, args.Component.BloodlossModifier);
+ }
+
var healed = _damageable.TryChangeDamage(uid, args.Component.Damage, true);
// Reverify that we can heal the damage.
diff --git a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs
index 356b4792b2..6243e2593f 100644
--- a/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs
+++ b/Content.Server/Nutrition/EntitySystems/SmokingSystem.cs
@@ -117,7 +117,7 @@ namespace Content.Server.Nutrition.EntitySystems
continue;
_reactiveSystem.ReactionEntity(containerManager.Owner, ReactionMethod.Ingestion, inhaledSolution);
- _bloodstreamSystem.TryAddToBloodstream(containerManager.Owner, inhaledSolution, bloodstream);
+ _bloodstreamSystem.TryAddToChemicals(containerManager.Owner, inhaledSolution, bloodstream);
}
_timer -= UpdateTimer;
diff --git a/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs
index 1818e440e3..83a6118ea2 100644
--- a/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs
+++ b/Content.Server/Weapon/Melee/MeleeWeaponSystem.cs
@@ -295,7 +295,7 @@ namespace Content.Server.Weapon.Melee
foreach (var bloodstream in hitBloodstreams)
{
var individualInjection = solutionToInject.SplitSolution(volPerBloodstream);
- _bloodstreamSystem.TryAddToBloodstream((bloodstream).Owner, individualInjection, bloodstream);
+ _bloodstreamSystem.TryAddToChemicals((bloodstream).Owner, individualInjection, bloodstream);
}
}
diff --git a/Resources/Audio/Effects/Fluids/ATTRIBUTION.txt b/Resources/Audio/Effects/Fluids/ATTRIBUTION.txt
new file mode 100644
index 0000000000..78c9a1114a
--- /dev/null
+++ b/Resources/Audio/Effects/Fluids/ATTRIBUTION.txt
@@ -0,0 +1,2 @@
+blood1.ogg under CC-0 from https://freesound.org/people/kyles/sounds/453769/
+blood2.ogg under CC-BY 3.0 from https://freesound.org/people/EminYILDIRIM/sounds/554284/
\ No newline at end of file
diff --git a/Resources/Audio/Effects/Fluids/blood1.ogg b/Resources/Audio/Effects/Fluids/blood1.ogg
new file mode 100644
index 0000000000..381d499517
Binary files /dev/null and b/Resources/Audio/Effects/Fluids/blood1.ogg differ
diff --git a/Resources/Audio/Effects/Fluids/blood2.ogg b/Resources/Audio/Effects/Fluids/blood2.ogg
new file mode 100644
index 0000000000..cec135b80a
Binary files /dev/null and b/Resources/Audio/Effects/Fluids/blood2.ogg differ
diff --git a/Resources/Locale/en-US/bloodstream/bloodstream.ftl b/Resources/Locale/en-US/bloodstream/bloodstream.ftl
new file mode 100644
index 0000000000..96996b6b91
--- /dev/null
+++ b/Resources/Locale/en-US/bloodstream/bloodstream.ftl
@@ -0,0 +1 @@
+bloodstream-component-looks-pale = [color=bisque]{CAPITALIZE(SUBJECT($target))} looks pale.[/color]
diff --git a/Resources/Prototypes/Damage/modifier_sets.yml b/Resources/Prototypes/Damage/modifier_sets.yml
index f3b10b32f2..2a8bc3f7fc 100644
--- a/Resources/Prototypes/Damage/modifier_sets.yml
+++ b/Resources/Prototypes/Damage/modifier_sets.yml
@@ -72,3 +72,20 @@
Cold: 1.5
Poison: 0.8
+# Represents which damage types should be modified
+# in relation to how they cause bloodloss damage.
+- type: damageModifierSet
+ id: BloodlossHuman
+ coefficients:
+ Blunt: 0.8
+ Slash: 2.5
+ Piercing: 2.0
+ Shock: 0.0
+ Cold: 0.0
+ Heat: 0.0
+ Poison: 0.0
+ Radiation: 0.0
+ Asphyxiation: 0.0
+ Bloodloss: 0.0 # no double dipping
+ Cellular: 0.0
+
diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml
index 42df573f26..24b2a86c0a 100644
--- a/Resources/Prototypes/Entities/Effects/puddle.yml
+++ b/Resources/Prototypes/Entities/Effects/puddle.yml
@@ -88,6 +88,25 @@
visuals:
- type: PuddleVisualizer
+- type: entity
+ id: PuddleBlood
+ name: blood
+ description: This can't be a good sign.
+ parent: PuddleBase
+ components:
+ - type: Sprite
+ sprite: Fluids/splatter.rsi # Placeholder
+ state: splatter-0
+ netsync: false
+ - type: Puddle
+ slipThreshold: 20
+ - type: Evaporation
+ evaporateTime: 20 # 4 times slower than normal
+ - type: Appearance
+ visuals:
+ - type: PuddleVisualizer
+ recolor: true
+
- type: entity
name: vomit
id: PuddleVomit
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
index b906cfb533..d956ca3a28 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
@@ -53,7 +53,14 @@
- Opaque
- type: SolutionContainerManager
- type: Bloodstream
- max_volume: 100
+ bloodlossDamage:
+ types:
+ Bloodloss:
+ 1
+ bloodlossHealDamage:
+ types:
+ Bloodloss:
+ -0.25
- type: Damageable
damageContainer: Biological
- type: AtmosExposed
diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml
index 5afb2bfabc..6ac8e820b3 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml
@@ -44,16 +44,19 @@
types:
Blunt: 2
# Organs
- - type: SolutionContainerManager
- solutions:
- bloodstream:
- maxVol: 250
- type: InjectableSolution
- solution: bloodstream
+ solution: chemicals
- type: DrawableSolution
solution: bloodstream
- type: Bloodstream
- max_volume: 100
+ bloodlossDamage:
+ types:
+ Bloodloss:
+ 1
+ bloodlossHealDamage:
+ types:
+ Bloodloss:
+ -0.25
- type: StatusEffects
allowed:
- Stun
diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml
index 58666b2515..175de37085 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml
@@ -99,6 +99,16 @@
- type: Damageable
damageContainer: Biological
damageModifierSet: Slime
+ - type: Bloodstream
+ bloodReagent: Slime # TODO Color slime blood based on their slime color or smth
+ bloodlossDamage:
+ types:
+ Bloodloss:
+ 1
+ bloodlossHealDamage:
+ types:
+ Bloodloss:
+ -0.25
- type: Barotrauma
damage:
types:
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
index 267bcaa9aa..a68581c068 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml
@@ -39,6 +39,7 @@
damage:
groups:
Brute: -15 # 5 for each type in the group
+ bloodlossModifier: -3 # a little bit of bloodloss healing.
- type: Stack
stackType: Brutepack
@@ -57,8 +58,9 @@
damageContainer: Biological
damage:
types:
- Slash: -5
- Piercing: -5
+ Slash: -2.5
+ Piercing: -2.5
+ bloodlossModifier: -10
- type: Stack
stackType: Gauze
diff --git a/Resources/Prototypes/Reagents/biological.yml b/Resources/Prototypes/Reagents/biological.yml
new file mode 100644
index 0000000000..a89831a77b
--- /dev/null
+++ b/Resources/Prototypes/Reagents/biological.yml
@@ -0,0 +1,30 @@
+- type: reagent
+ id: Blood
+ name: blood
+ group: Biological
+ desc: I hope this is ketchup.
+ color: "#9e1010"
+ physicalDesc: ferrous
+ metabolisms:
+ Drink:
+ # Quenching!
+ effects:
+ - !type:SatiateThirst
+ factor: 1.5
+ plantMetabolism:
+ - !type:PlantAdjustWater
+ amount: 0.5
+
+- type: reagent
+ id: Slime
+ name: slime
+ group: Biological
+ desc: You thought this was gradient blood at first, but you were mistaken.
+ color: "#2cf274"
+ physicalDesc: viscous
+ metabolisms:
+ Food:
+ # Delicious!
+ effects:
+ - !type:SatiateHunger
+ factor: 1.5
diff --git a/Resources/Prototypes/Reagents/elements.yml b/Resources/Prototypes/Reagents/elements.yml
index 9b0f65b619..c376f320e0 100644
--- a/Resources/Prototypes/Reagents/elements.yml
+++ b/Resources/Prototypes/Reagents/elements.yml
@@ -105,6 +105,12 @@
color: "#434b4d"
boilingPoint: 2862.0
meltingPoint: 1538.0
+ metabolisms:
+ Medicine:
+ effects:
+ - !type:ModifyBloodLevel
+ scaled: true # scales 2x with the amount of iron you consume
+ amount: 2
- type: reagent
id: Lithium
diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml
index c44a4040b9..7014f7d725 100644
--- a/Resources/Prototypes/Reagents/medicine.yml
+++ b/Resources/Prototypes/Reagents/medicine.yml
@@ -158,7 +158,7 @@
id: Dexalin
name: dexalin
group: Medicine
- desc: Used for treating oxygen deprivation. In most cases where it is likely to be needed, the strength of Dexalin Plus will probably be more useful (Results in 1 unit instead of 2).
+ desc: Used for treating oxygen deprivation. In most cases where it is likely to be needed, the strength of Dexalin Plus will probably be more useful.
physicalDesc: opaque
color: "#0041a8"
metabolisms:
@@ -168,12 +168,13 @@
damage:
types:
Asphyxiation: -1
+ Bloodloss: -0.5
- type: reagent
id: DexalinPlus
name: dexalin plus
group: Medicine
- desc: Used in treatment of extreme cases of oxygen deprivation. Even a single unit immediately counters all oxygen loss, which is hugely useful in many circumstances. Any dose beyond this will continue to counter oxygen loss until it is metabolized, essentially removing the need to breathe.
+ desc: Used in treatment of extreme cases of oxygen deprivation. Effective at healing blood loss damage.
physicalDesc: cloudy
color: "#4da0bd"
metabolisms:
@@ -183,6 +184,7 @@
damage:
types:
Asphyxiation: -3
+ Bloodloss: -2
- type: reagent
id: Ethylredoxrazine
@@ -299,7 +301,7 @@
id: Inaprovaline
name: inaprovaline
group: Medicine
- desc: Inaprovaline is a synaptic stimulant and cardiostimulant. Commonly used to stabilize patients- it stops oxygen loss when the patient is in critical health. It'll also slow down bleeding (internal or external) by half while in the body. Acts as a decent painkiller.
+ desc: Inaprovaline is a synaptic stimulant and cardiostimulant. Commonly used to stabilize patients- it stops oxygen loss when the patient is in critical health. It'll also slow down bleeding by a good amount. Acts as a decent painkiller.
physicalDesc: opaque
color: "#731024"
metabolisms:
@@ -313,6 +315,9 @@
damage:
types:
Asphyxiation: -5
+ - !type:ModifyBleedAmount
+ scaled: true
+ amount: -0.25
- type: reagent
id: Kelotane
@@ -443,6 +448,28 @@
physicalDesc: strong-smelling
color: "#2f6ed4"
+- type: reagent
+ id: TranexamicAcid
+ name: tranexamic acid
+ group: Medicine
+ desc: A blood clotting medicine for preventing heavy bleeding. Very dangerous in large quantities.
+ physicalDesc: viscous
+ color: "#ba7d7d"
+ metabolisms:
+ Medicine:
+ effects:
+ # Medium-large quantities can hurt you instead,
+ # but still technically stop your bleeding.
+ - !type:ModifyBleedAmount
+ scaled: true
+ - !type:HealthChange
+ conditions:
+ - !type:ReagentThreshold
+ min: 15
+ damage:
+ types:
+ Bloodloss: 3
+
- type: reagent
id: Tricordrazine
name: tricordrazine
diff --git a/Resources/Prototypes/Recipes/Reactions/medicine.yml b/Resources/Prototypes/Recipes/Reactions/medicine.yml
index 0ab9f3582a..5346c34e90 100644
--- a/Resources/Prototypes/Recipes/Reactions/medicine.yml
+++ b/Resources/Prototypes/Recipes/Reactions/medicine.yml
@@ -212,6 +212,18 @@
products:
Inaprovaline: 3
+- type: reaction
+ id: TranexamicAcid
+ reactants:
+ Inaprovaline:
+ amount: 1
+ SulfuricAcid:
+ amount: 1
+ Glucose:
+ amount: 1
+ products:
+ TranexamicAcid: 3
+
- type: reaction
id: Kelotane
reactants:
diff --git a/Resources/Prototypes/SoundCollections/gib.yml b/Resources/Prototypes/SoundCollections/gib.yml
index 5c39aacb3f..e4d00914ab 100644
--- a/Resources/Prototypes/SoundCollections/gib.yml
+++ b/Resources/Prototypes/SoundCollections/gib.yml
@@ -4,3 +4,9 @@
- /Audio/Effects/gib1.ogg
- /Audio/Effects/gib2.ogg
- /Audio/Effects/gib3.ogg
+
+- type: soundCollection
+ id: blood
+ files:
+ - /Audio/Effects/Fluids/blood1.ogg
+ - /Audio/Effects/Fluids/blood2.ogg