]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
multi reagent bloodstream (#41489)
authorIgnaz "Ian" Kraft <ignaz.k@live.de>
Mon, 1 Dec 2025 05:35:21 +0000 (06:35 +0100)
committerGitHub <noreply@github.com>
Mon, 1 Dec 2025 05:35:21 +0000 (05:35 +0000)
* multi reagent bloodstream

* pluralize the comments

* fix TryModifyBloodLevel return logic

* now with quantity

* now with solution

* implement suggestions

* fix forensics

* minor thing

* Nevermind undo that caps matters.

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
25 files changed:
Content.Server/Body/Systems/BloodstreamSystem.cs
Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs
Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs
Content.Server/Zombies/ZombieSystem.Transform.cs
Content.Server/Zombies/ZombieSystem.cs
Content.Shared/Body/Components/BloodstreamComponent.cs
Content.Shared/Body/Systems/SharedBloodstreamSystem.cs
Content.Shared/Chemistry/Components/Solution.cs
Content.Shared/Chemistry/Reagent/DNAData.cs
Content.Shared/Chemistry/Reagent/ReagentQuantity.cs
Content.Shared/Zombies/ZombieComponent.cs
Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml
Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml
Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml
Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml
Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml
Resources/Prototypes/Entities/Mobs/NPCs/space.yml
Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml
Resources/Prototypes/Entities/Mobs/Species/arachnid.yml
Resources/Prototypes/Entities/Mobs/Species/diona.yml
Resources/Prototypes/Entities/Mobs/Species/gingerbread.yml
Resources/Prototypes/Entities/Mobs/Species/moth.yml
Resources/Prototypes/Entities/Mobs/Species/slime.yml
Resources/Prototypes/Entities/Mobs/Species/vox.yml

index c2185750af1637c39db87e5e95d2aa18ca9bc284..a58deec49424fd74c0222c8d41b68403b4916333 100644 (file)
@@ -37,7 +37,10 @@ public sealed class BloodstreamSystem : SharedBloodstreamSystem
 
         // Fill blood solution with BLOOD
         // The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
-        bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
+        var solution = entity.Comp.BloodReagents.Clone();
+        solution.ScaleTo(entity.Comp.BloodMaxVolume - bloodSolution.Volume);
+        solution.SetReagentData(GetEntityBloodData(entity.Owner));
+        bloodSolution.AddSolution(solution, PrototypeManager);
     }
 
     // forensics is not predicted yet
index 61d36f98b960604ee6b34f87f27ca5f220ac3f0d..bc295545b0beac53ea911c99a42b6909193e7ba7 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Chemistry.Components;
 using Content.Shared.Storage;
 
 namespace Content.Server.Medical.BiomassReclaimer
@@ -34,10 +35,10 @@ namespace Content.Server.Medical.BiomassReclaimer
         public float CurrentExpectedYield = 0f;
 
         /// <summary>
-        /// The reagent that will be spilled while processing a mob.
+        /// The reagents that will be spilled while processing a mob.
         /// </summary>
         [ViewVariables]
-        public string? BloodReagent;
+        public Solution? BloodReagents = null;
 
         /// <summary>
         /// Entities that can be randomly spawned while processing a mob.
index e029071574bcbce5227beb1e86ffc9ae4aadee50..83dcd9cf7c836c3ad71c7e0deac5ecc65527e1eb 100644 (file)
@@ -7,7 +7,7 @@ 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.Chemistry.EntitySystems;
 using Content.Shared.Climbing.Events;
 using Content.Shared.Construction.Components;
 using Content.Shared.Database;
@@ -45,6 +45,7 @@ namespace Content.Server.Medical.BiomassReclaimer
         [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
         [Dependency] private readonly SharedPopupSystem _popup = default!;
         [Dependency] private readonly PuddleSystem _puddleSystem = default!;
+        [Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
         [Dependency] private readonly ThrowingSystem _throwing = default!;
         [Dependency] private readonly IRobustRandom _robustRandom = default!;
         [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
@@ -68,10 +69,8 @@ namespace Content.Server.Medical.BiomassReclaimer
 
                 if (reclaimer.RandomMessTimer <= 0)
                 {
-                    if (_robustRandom.Prob(0.2f) && reclaimer.BloodReagent is not null)
+                    if (_robustRandom.Prob(0.2f) && reclaimer.BloodReagents is { } blood)
                     {
-                        Solution blood = new();
-                        blood.AddReagent(reclaimer.BloodReagent, 50);
                         _puddleSystem.TrySpillAt(uid, blood, out _);
                     }
                     if (_robustRandom.Prob(0.03f) && reclaimer.SpawnedEntities.Count > 0)
@@ -92,7 +91,7 @@ namespace Content.Server.Medical.BiomassReclaimer
                 reclaimer.CurrentExpectedYield = reclaimer.CurrentExpectedYield - actualYield; // store non-integer leftovers
                 _material.SpawnMultipleFromMaterial(actualYield, BiomassPrototype, Transform(uid).Coordinates);
 
-                reclaimer.BloodReagent = null;
+                reclaimer.BloodReagents = null;
                 reclaimer.SpawnedEntities.Clear();
                 RemCompDeferred<ActiveBiomassReclaimerComponent>(uid);
             }
@@ -208,9 +207,11 @@ namespace Content.Server.Medical.BiomassReclaimer
             var component = ent.Comp;
             AddComp<ActiveBiomassReclaimerComponent>(ent);
 
-            if (TryComp<BloodstreamComponent>(toProcess, out var stream))
+            if (TryComp<BloodstreamComponent>(toProcess, out var stream) &&
+                _solution.ResolveSolution(toProcess, stream.BloodSolutionName, ref stream.BloodSolution, out var solution))
             {
-                component.BloodReagent = stream.BloodReagent;
+                component.BloodReagents = solution.Clone();
+                component.BloodReagents.ScaleSolution(50 / component.BloodReagents.Volume);
             }
             if (TryComp<ButcherableComponent>(toProcess, out var butcherableComponent))
             {
index 43a06f2614ed840fd8cae7ab1cd78d78fa887e24..cfa22b2a141b659639c7a45143ba50e9e786db80 100644 (file)
@@ -16,7 +16,6 @@ using Content.Server.Speech.Components;
 using Content.Shared.Body.Components;
 using Content.Shared.CombatMode;
 using Content.Shared.CombatMode.Pacification;
-using Content.Shared.Damage.Components;
 using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Humanoid;
@@ -194,8 +193,8 @@ public sealed partial class ZombieSystem
             zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
             zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
             zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
-            if (TryComp<BloodstreamComponent>(target, out var stream))
-                zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
+            if (TryComp<BloodstreamComponent>(target, out var stream) && stream.BloodReagents is { } reagents)
+                zombiecomp.BeforeZombifiedBloodReagents = reagents.Clone();
 
             _humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
 
@@ -229,7 +228,7 @@ public sealed partial class ZombieSystem
         //NOTE: they are supposed to bleed, just not take damage
         _bloodstream.SetBloodLossThreshold(target, 0f);
         //Give them zombie blood
-        _bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
+        _bloodstream.ChangeBloodReagents(target, zombiecomp.NewBloodReagents);
 
         //This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
         _inventory.TryUnequip(target, "gloves", true, true);
index 72c52369303fc3291f5f1b51f0496aedfbe9c793..b840e7203e20f986103556cad1ecd3c37547f85f 100644 (file)
@@ -305,7 +305,7 @@ namespace Content.Server.Zombies
                 appcomp.EyeColor = zombiecomp.BeforeZombifiedEyeColor;
             }
             _humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
-            _bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
+            _bloodstream.ChangeBloodReagents(target, zombiecomp.BeforeZombifiedBloodReagents);
 
             return true;
         }
index 51814eaba9d52446bc1287437d0d3fed392d4259..2ebfae6f33ac93d9ca914b2fe75b0ca0efe51542 100644 (file)
@@ -1,7 +1,6 @@
 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;
@@ -152,13 +151,13 @@ public sealed partial class BloodstreamComponent : Component
     public FixedPoint2 BloodMaxVolume = FixedPoint2.New(300);
 
     /// <summary>
-    /// Which reagent is considered this entities 'blood'?
+    /// Which reagents are considered this entities 'blood'?
     /// </summary>
     /// <remarks>
     /// Slime-people might use slime as their blood or something like that.
     /// </remarks>
     [DataField, AutoNetworkedField]
-    public ProtoId<ReagentPrototype> BloodReagent = "Blood";
+    public Solution BloodReagents = new([new("Blood", 1)]);
 
     /// <summary>
     /// Name/Key that <see cref="BloodSolution"/> is indexed by.
index 688e3ccb9266c5220b67357d2401d259502317a7..e108cbfb276172a6a6ed47faebb0c9483f4a0b10 100644 (file)
@@ -29,9 +29,9 @@ public abstract class SharedBloodstreamSystem : EntitySystem
 {
     public static readonly EntProtoId Bloodloss = "StatusEffectBloodloss";
 
+    [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
     [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!;
@@ -193,7 +193,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
         }
 
         // TODO probably cache this or something. humans get hurt a lot
-        if (!_prototypeManager.Resolve(ent.Comp.DamageBleedModifiers, out var modifiers))
+        if (!PrototypeManager.Resolve(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
@@ -366,11 +366,18 @@ public abstract class SharedBloodstreamSystem : EntitySystem
     public bool TryModifyBloodLevel(Entity<BloodstreamComponent?> ent, FixedPoint2 amount)
     {
         if (!Resolve(ent, ref ent.Comp, logMissing: false)
-            || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution))
+            || !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
             return false;
 
         if (amount >= 0)
-            return SolutionContainer.TryAddReagent(ent.Comp.BloodSolution.Value, ent.Comp.BloodReagent, amount, null, GetEntityBloodData(ent));
+        {
+            var min = FixedPoint2.Min(bloodSolution.AvailableVolume, amount);
+            var solution = ent.Comp.BloodReagents.Clone();
+            solution.ScaleTo(min);
+            solution.SetReagentData(GetEntityBloodData(ent));
+            SolutionContainer.AddSolution(ent.Comp.BloodSolution.Value, solution);
+            return min == amount;
+        }
 
         // Removal is more involved,
         // since we also wanna handle moving it to the temporary solution
@@ -380,7 +387,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
         if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution))
             return true;
 
-        tempSolution.AddSolution(newSol, _prototypeManager);
+        tempSolution.AddSolution(newSol, PrototypeManager);
 
         if (tempSolution.Volume > ent.Comp.BleedPuddleThreshold)
         {
@@ -388,7 +395,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
             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);
+                tempSolution.AddSolution(temp, PrototypeManager);
             }
 
             _puddle.TrySpillAt(ent.Owner, tempSolution, out _, sound: false);
@@ -439,21 +446,21 @@ public abstract class SharedBloodstreamSystem : EntitySystem
         if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
         {
             tempSol.MaxVolume += bloodSolution.MaxVolume;
-            tempSol.AddSolution(bloodSolution, _prototypeManager);
+            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);
+            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);
+            tempSol.AddSolution(tempSolution, PrototypeManager);
             SolutionContainer.RemoveAllSolution(ent.Comp.TemporarySolution.Value);
         }
 
@@ -463,27 +470,45 @@ public abstract class SharedBloodstreamSystem : EntitySystem
     /// <summary>
     /// Change what someone's blood is made of, on the fly.
     /// </summary>
+    [Obsolete("ChangeBloodReagent is obsolete, please use ChangeBloodReagents.")]
     public void ChangeBloodReagent(Entity<BloodstreamComponent?> ent, ProtoId<ReagentPrototype> reagent)
     {
-        if (!Resolve(ent, ref ent.Comp, logMissing: false)
-            || reagent == ent.Comp.BloodReagent)
+        ChangeBloodReagents(ent, new([new(reagent, 1)]));
+    }
+
+    /// <summary>
+    /// Change what someone's blood is made of, on the fly.
+    /// </summary>
+    public void ChangeBloodReagents(Entity<BloodstreamComponent?> ent, Solution reagents)
+    {
+        if (!Resolve(ent, ref ent.Comp, logMissing: false))
         {
             return;
         }
 
         if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
         {
-            ent.Comp.BloodReagent = reagent;
+            ent.Comp.BloodReagents = reagents.Clone();
+            DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReagents));
             return;
         }
 
-        var currentVolume = bloodSolution.RemoveReagent(ent.Comp.BloodReagent, bloodSolution.Volume, ignoreReagentData: true);
+        var currentVolume = FixedPoint2.Zero;
+        foreach (var reagent in ent.Comp.BloodReagents)
+        {
+            currentVolume += bloodSolution.RemoveReagent(reagent.Reagent, quantity: bloodSolution.Volume, ignoreReagentData: true);
+        }
+
+        ent.Comp.BloodReagents = reagents.Clone();
+        DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReagents));
 
-        ent.Comp.BloodReagent = reagent;
-        DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReagent));
+        if (currentVolume == FixedPoint2.Zero)
+            return;
 
-        if (currentVolume > 0)
-            SolutionContainer.TryAddReagent(ent.Comp.BloodSolution.Value, ent.Comp.BloodReagent, currentVolume, null, GetEntityBloodData(ent));
+        var solution = ent.Comp.BloodReagents.Clone();
+        solution.ScaleSolution(currentVolume / solution.Volume);
+        solution.SetReagentData(GetEntityBloodData(ent));
+        SolutionContainer.AddSolution(ent.Comp.BloodSolution.Value, solution);
     }
 
     /// <summary>
index df09f7f3f64f03a93b79799fd68ba3ab5e35e5e3..cd0314419a0978bf6e70dc384395045c1c0dd2c5 100644 (file)
@@ -167,7 +167,12 @@ namespace Content.Shared.Chemistry.Components
 
         public Solution(Solution solution)
         {
-            Contents = solution.Contents.ShallowClone();
+            Contents = new(solution.Contents.Count);
+            foreach (var item in solution.Contents)
+            {
+                Contents.Add(item.Clone());
+            }
+
             Volume = solution.Volume;
             MaxVolume = solution.MaxVolume;
             Temperature = solution.Temperature;
@@ -474,6 +479,37 @@ namespace Content.Shared.Chemistry.Components
             ValidateSolution();
         }
 
+        /// <summary>
+        ///     Scales the amount of solution.
+        /// </summary>
+        /// <param name="scale">The scalar to modify the solution by.</param>
+        public void ScaleSolution(FixedPoint2 scale)
+        {
+            ScaleSolution(scale.Float());
+        }
+
+        /// <summary>
+        ///     Scales the amount of solution to a given target.
+        /// </summary>
+        /// <param name="target">The volume the solutions should have after scaling.</param>
+        public void ScaleTo(FixedPoint2 target)
+        {
+            if (Volume == FixedPoint2.Zero || Volume == target)
+                return;
+
+            if (target == FixedPoint2.Zero)
+            {
+                RemoveAllSolution();
+                return;
+            }
+
+            var nearestIntScale = (int)Math.Ceiling(target.Float() / Volume.Float());
+            ScaleSolution(nearestIntScale);
+
+            var overflow = Volume - target;
+            RemoveSolution(overflow);
+        }
+
         /// <summary>
         ///     Attempts to remove an amount of reagent from the solution.
         /// </summary>
@@ -965,5 +1001,15 @@ namespace Content.Shared.Chemistry.Components
             }
             return dict;
         }
+
+        public void SetReagentData(List<ReagentData>? data)
+        {
+            for (var i = 0; i < Contents.Count; i++)
+            {
+                var old = Contents[i];
+                Contents[i] = new ReagentQuantity(new ReagentId(old.Reagent.Prototype, data), old.Quantity);
+            }
+            ValidateSolution();
+        }
     }
 }
index e75e994eceb6b4d03718338c75ff3805ad9a8aef..5a88c0b41cf09dbb35edccf721d138c55a2fa4a6 100644 (file)
@@ -1,4 +1,3 @@
-using Content.Shared.FixedPoint;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Chemistry.Reagent;
@@ -7,9 +6,15 @@ namespace Content.Shared.Chemistry.Reagent;
 public sealed partial class DnaData : ReagentData
 {
     [DataField]
-    public string DNA = String.Empty;
+    public string DNA = string.Empty;
 
-    public override ReagentData Clone() => this;
+    public override ReagentData Clone()
+    {
+        return new DnaData
+        {
+            DNA = DNA,
+        };
+    }
 
     public override bool Equals(ReagentData? other)
     {
index cc5b52fbe2edf7e3a0dfb0ef1e797478c17cf551..22c1a3e0e55f4fb15f516a1fdee1feb1e6fcd464 100644 (file)
@@ -8,9 +8,9 @@ namespace Content.Shared.Chemistry.Reagent;
 /// </summary>
 [Serializable, NetSerializable]
 [DataDefinition]
-public partial struct ReagentQuantity : IEquatable<ReagentQuantity>
+public partial struct ReagentQuantity : IEquatable<ReagentQuantity>, IRobustCloneable<ReagentQuantity>
 {
-    [DataField("Quantity", required:true)]
+    [DataField("Quantity", required: true)]
     public FixedPoint2 Quantity { get; private set; }
 
     [IncludeDataField]
@@ -28,6 +28,28 @@ public partial struct ReagentQuantity : IEquatable<ReagentQuantity>
         Quantity = quantity;
     }
 
+    public ReagentQuantity(ReagentQuantity reagentQuantity)
+    {
+        Quantity = reagentQuantity.Quantity;
+        if (reagentQuantity.Reagent.Data is not { } data)
+        {
+            Reagent = new ReagentId(reagentQuantity.Reagent.Prototype, null);
+            return;
+        }
+
+        List<ReagentData> copy = new(data.Count);
+        foreach (var item in data)
+        {
+            copy.Add(item.Clone());
+        }
+        Reagent = new ReagentId(reagentQuantity.Reagent.Prototype, copy);
+    }
+
+    public readonly ReagentQuantity Clone()
+    {
+        return new ReagentQuantity(this);
+    }
+
     public ReagentQuantity() : this(default, default)
     {
     }
index 2a69a7553f0ce2c18c71445fc0ee30bdb207c9bd..4c941f176771799fa44cbe4cf95367bfbec8fc1f 100644 (file)
@@ -1,7 +1,7 @@
 using Content.Shared.Chat.Prototypes;
+using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.Reagent;
 using Content.Shared.Damage;
-using Content.Shared.FixedPoint;
 using Content.Shared.Humanoid;
 using Content.Shared.Roles;
 using Content.Shared.StatusIcon;
@@ -165,14 +165,14 @@ public sealed partial class ZombieComponent : Component
     public SoundSpecifier BiteSound = new SoundPathSpecifier("/Audio/Effects/bite.ogg");
 
     /// <summary>
-    /// The blood reagent of the humanoid to restore in case of cloning
+    /// The blood reagents of the humanoid to restore in case of cloning
     /// </summary>
-    [DataField("beforeZombifiedBloodReagent")]
-    public string BeforeZombifiedBloodReagent = string.Empty;
+    [DataField("beforeZombifiedBloodReagents")]
+    public Solution BeforeZombifiedBloodReagents = new();
 
     /// <summary>
-    /// The blood reagent to give the zombie. In case you want zombies that bleed milk, or something.
+    /// The blood reagents to give the zombie. In case you want zombies that bleed milk, or something.
     /// </summary>
-    [DataField("newBloodReagent", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentPrototype>))]
-    public string NewBloodReagent = "ZombieBlood";
+    [DataField("newBloodReagents")]
+    public Solution NewBloodReagents = new([new("ZombieBlood", 1)]);
 }
index d3d1b23dcc13abb1344f3937991acdb8d0317454..9965167256572ab9cf3674df1c1461c23fe204b7 100644 (file)
     - Bee
     - Trash
   - type: Bloodstream
-    bloodReagent: InsectBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: InsectBlood
+        Quantity: 1
     bloodMaxVolume: 0.1
   - type: MobPrice
     price: 50
       Dead:
         Base: cockroach_dead
   - type: Bloodstream
-    bloodReagent: InsectBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: InsectBlood
+        Quantity: 1
     bloodMaxVolume: 20
   - type: Edible
   - type: FlavorProfile
     damageContainer: Biological
     damageModifierSet: Moth
   - type: Bloodstream
-    bloodReagent: InsectBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: InsectBlood
+        Quantity: 1
   - type: Respirator
     damage:
       types:
     tags:
     - Trash
   - type: Bloodstream
-    bloodReagent: InsectBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: InsectBlood
+        Quantity: 1
     bloodMaxVolume: 0.1
   - type: MobPrice
     price: 50
     accent: crab
   - type: Bloodstream
     bloodMaxVolume: 50
-    bloodReagent: CopperBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: CopperBlood
+        Quantity: 1
   - type: Tag
     tags:
     - VimPilot
   - type: RadiationSource
     intensity: 0.3
   - type: Bloodstream
-    bloodReagent: UnstableMutagen
+    bloodReagents:
+      reagents:
+      - ReagentId: UnstableMutagen
+        Quantity: 1
   - type: SolutionContainerManager
     solutions:
       food:
   - type: IgnoreSpiderWeb
   - type: Bloodstream
     bloodMaxVolume: 150
-    bloodReagent: CopperBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: CopperBlood
+        Quantity: 1
   - type: Speech
     speechVerb: Arachnid
     speechSounds: Arachnid
       speechVerb: Cluwne
     - type: Bloodstream
       bloodMaxVolume: 150
-      bloodReagent: Laughter
-
+      bloodReagents:
+        reagents:
+        - ReagentId: Laughter
+          Quantity: 1
 - type: entity
   name: wizard spider
   parent: MobGiantSpider
     attributes:
       gender: epicene
   - type: Bloodstream
-    bloodReagent: DemonsBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: DemonsBlood
+        Quantity: 1
   - type: Damageable
     damageContainer: BiologicalMetaphysical
     damageModifierSet: Infernal
     speciesId: cat
     templateId: pet
   - type: Bloodstream
-    bloodReagent: Sap
+    bloodReagents:
+      reagents:
+      - ReagentId: Sap
+        Quantity: 1
     bloodMaxVolume: 60
   - type: DamageStateVisuals
     states:
index 36ddf71d4b41904b5b13e081480d4afedc6e049e..4c9f851ba0c8d144d95244a4e5bc14389f9346a6 100644 (file)
   - type: ReplacementAccent
     accent: xeno
   - type: Bloodstream
-    bloodReagent: FerrochromicAcid
+    bloodReagents:
+      reagents:
+      - ReagentId: FerrochromicAcid
+        Quantity: 1
     bloodMaxVolume: 75 #we don't want the map to become pools of blood
     bloodlossDamage:
       types:
index 1cb00ef7608ae44ba90c96049d1614cba7dbb9c9..23e32309377a68159a5697a0789c8e97bf5bc5b9 100644 (file)
       context: "human"
     - type: Bloodstream
       bloodMaxVolume: 300
-      bloodReagent: Laughter
-
+      bloodReagents:
+        reagents:
+        - ReagentId: Laughter
+          Quantity: 1
 - type: entity
   name: behonker
   parent: BaseMobBehonker
index eef5f48838ff06b8afc55359b15d0fed9cb564bf..79df1005983f2b322c60ea2404b498cd8da1f836 100644 (file)
     speedModifierThresholds:
       50: 0.4
   - type: Bloodstream
-    bloodReagent: Water
+    bloodReagents:
+      reagents:
+      - ReagentId: Water
+        Quantity: 1
     chemicalMaxVolume: 100
   - type: StatusEffects
     allowed:
   suffix: Beer
   components:
   - type: Bloodstream
-    bloodReagent: Beer
+    bloodReagents:
+      reagents:
+      - ReagentId: Beer
+        Quantity: 1
   - type: PointLight
     color: "#cfa85f"
   - type: Sprite
   suffix: Pax
   components:
   - type: Bloodstream
-    bloodReagent: Pax
+    bloodReagents:
+      reagents:
+      - ReagentId: Pax
+        Quantity: 1
   - type: PointLight
     color: "#AAAAAA"
   - type: Sprite
   suffix: Nocturine
   components:
   - type: Bloodstream
-    bloodReagent: Nocturine
+    bloodReagents:
+      reagents:
+      - ReagentId: Nocturine
+        Quantity: 1
   - type: PointLight
     color: "#128e80"
   - type: Sprite
   suffix: THC
   components:
   - type: Bloodstream
-    bloodReagent: THC
+    bloodReagents:
+      reagents:
+      - ReagentId: THC
+        Quantity: 1
   - type: PointLight
     color: "#808080"
   - type: Sprite
   suffix: Bicaridine
   components:
   - type: Bloodstream
-    bloodReagent: Bicaridine
+    bloodReagents:
+      reagents:
+      - ReagentId: Bicaridine
+        Quantity: 1
   - type: PointLight
     color: "#ffaa00"
   - type: Sprite
   suffix: Toxin
   components:
   - type: Bloodstream
-    bloodReagent: Toxin
+    bloodReagents:
+      reagents:
+      - ReagentId: Toxin
+        Quantity: 1
   - type: PointLight
     color: "#cf3600"
   - type: Sprite
   suffix: Napalm
   components:
   - type: Bloodstream
-    bloodReagent: Napalm
+    bloodReagents:
+      reagents:
+      - ReagentId: Napalm
+        Quantity: 1
   - type: PointLight
     color: "#FA00AF"
   - type: Sprite
   suffix: Omnizine
   components:
   - type: Bloodstream
-    bloodReagent: Omnizine
+    bloodReagents:
+      reagents:
+      - ReagentId: Omnizine
+        Quantity: 1
   - type: PointLight
     color: "#fcf7f9"
   - type: Sprite
   suffix: Mute Toxin
   components:
   - type: Bloodstream
-    bloodReagent: MuteToxin
+    bloodReagents:
+      reagents:
+      - ReagentId: MuteToxin
+        Quantity: 1
   - type: PointLight
     color: "#0f0f0f"
   - type: Sprite
   suffix: Norepinephric Acid
   components:
   - type: Bloodstream
-    bloodReagent: NorepinephricAcid
+    bloodReagents:
+      reagents:
+      - ReagentId: NorepinephricAcid
+        Quantity: 1
   - type: PointLight
     color: "#96a8b5"
   - type: Sprite
   suffix: Ephedrine
   components:
   - type: Bloodstream
-    bloodReagent: Ephedrine
+    bloodReagents:
+      reagents:
+      - ReagentId: Ephedrine
+        Quantity: 1
   - type: PointLight
     color: "#D2FFFA"
   - type: Sprite
   suffix: Robust Harvest
   components:
   - type: Bloodstream
-    bloodReagent: RobustHarvest
+    bloodReagents:
+      reagents:
+      - ReagentId: RobustHarvest
+        Quantity: 1
   - type: PointLight
     color: "#3e901c"
   - type: Sprite
index 00aa20db18720394552f8b0644978f26a57ce831..a4ba443046f856fe9aef71635a425c143462bc7b 100644 (file)
       - map: [ "enum.DamageStateVisualLayers.Base" ]
         state: alive
   - type: Bloodstream
-    bloodReagent: JuiceTomato
+    bloodReagents:
+      reagents:
+      - ReagentId: JuiceTomato
+        Quantity: 1
     bloodMaxVolume: 50
     chemicalMaxVolume: 30
   - type: DamageStateVisuals
index 95cf95bcbcd5b6395a8e1bb7a20fc8dbbca7debd..74c6bb1468df4dfe53887f73c3977319bed0af21 100644 (file)
     damageContainer: Biological
     damageModifierSet: Slime
   - type: Bloodstream
-    bloodReagent: Slime
+    bloodReagents:
+      reagents:
+      - ReagentId: Slime
+        Quantity: 1
     bloodlossDamage:
       types:
         Bloodloss:
index 45c0b46d238d6f13cf89c8002b6128f6cafe6fd1..8acc571ccf5d1fb25ec68f269829a9fdeca051b6 100644 (file)
   - type: MovementAlwaysTouching
   - type: Bloodstream
     bloodMaxVolume: 300
-    bloodReagent: Cryoxadone
+    bloodReagents:
+      reagents:
+      - ReagentId: Cryoxadone
+        Quantity: 1
   - type: CombatMode
   - type: Temperature
     heatDamageThreshold: 500
       prob: 0.5
   - type: Bloodstream
     bloodMaxVolume: 250
-    bloodReagent: Cryoxadone
+    bloodReagents:
+      reagents:
+      - ReagentId: Cryoxadone
+        Quantity: 1
   - type: Fixtures
     fixtures:
       fix1:
           prob: 0.3
     - type: Bloodstream
       bloodMaxVolume: 200
-      bloodReagent: Cryoxadone
+      bloodReagents:
+        reagents:
+        - ReagentId: Cryoxadone
+          Quantity: 1
     - type: Fixtures
       fixtures:
         fix1:
     combatToggleAction: ActionCombatModeToggleOff
   - type: Bloodstream
     bloodMaxVolume: 30
-    bloodReagent: Cryoxadone
+    bloodReagents:
+      reagents:
+      - ReagentId: Cryoxadone
+        Quantity: 1
   - type: CanEscapeInventory
   - type: MobPrice
     price: 50
index ba8ee70b1a7361607b7328bb32cbc0302458c054..f1c860364bced37ce4b444de68b66800120f49fb 100644 (file)
   - type: Stamina
     critThreshold: 200
   - type: Bloodstream
-    bloodReagent: FluorosulfuricAcid
+    bloodReagents:
+      reagents:
+      - ReagentId: FluorosulfuricAcid
+        Quantity: 1
     bloodMaxVolume: 650
   - type: MeleeWeapon
     altDisarm: false
   - type: Stamina
     critThreshold: 200
   - type: Bloodstream
-    bloodReagent: FluorosulfuricAcid
+    bloodReagents:
+      reagents:
+      - ReagentId: FluorosulfuricAcid
+        Quantity: 1
     bloodMaxVolume: 300
   - type: MeleeWeapon
     altDisarm: false
index 0624e9670a4a66db83128135517180e865645ab9..e38e93a9300c3e6d436ae1f1d75b1456806785a2 100644 (file)
       - !type:WashCreamPie
   # Damage (Self)
   - type: Bloodstream
-    bloodReagent: CopperBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: CopperBlood
+        Quantity: 1
   # Damage (Others)
   - type: MeleeWeapon
     animation: WeaponArcBite
index 6cad3bdfc59a8d7f7ffebadcdfed9f11cfbd415f..4dafbf99b90edc5eafdfcedd474b9dc8af3018fa 100644 (file)
       - id: FoodMeatPlant
         amount: 5
   - type: Bloodstream
-    bloodReagent: Sap
+    bloodReagents:
+      reagents:
+      - ReagentId: Sap
+        Quantity: 1
   - type: Reactive
     groups:
       Flammable: [ Touch ]
index cc4b506dc858479c609098ff996b5c5f2deeab5c..7844c8008274f360c83fcef96dcd752a7c21afd1 100644 (file)
       - id: FoodBakedCookie #should be replaced with gingerbread sheets or something... provided you're willing to make a full spriteset of those.
         amount: 5
   - type: Bloodstream
-    bloodReagent: Sugar
+    bloodReagents:
+      reagents:
+      - ReagentId: Sugar
+        Quantity: 1
+      - ReagentId: Butter
+        Quantity: 2
   - type: Fixtures
     fixtures:
       fix1:
index 9503c2f56d795234aeeda80b4610753dc4181b4d..d0adbcc28f8b3a8fe2f597f240847e1c8b4e65c1 100644 (file)
     - id: FoodMeat
       amount: 5
   - type: Bloodstream
-    bloodReagent: InsectBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: InsectBlood
+        Quantity: 1
   - type: DamageVisuals
     damageOverlayGroups:
       Brute:
index 1ca590d49658f6d65cb27dd1383e7defe47b7cb1..89115dd1c27de9d206c8d9c92fc0063695a15aff 100644 (file)
       Burn:
         sprite: Mobs/Effects/burn_damage.rsi
   - type: Bloodstream
-    bloodReagent: Slime # TODO Color slime blood based on their slime color or smth
+    bloodReagents: # TODO Color slime blood based on their slime color or smth
+      reagents:
+      - ReagentId: Slime
+        Quantity: 1
   - type: Barotrauma
     damage:
       types:
index d6ff814970cd6b1b3cb759ceb75a55a5bbebb301..ecc0cea5cd2672407620cff965ab3950b212dc04 100644 (file)
       Burn:
         sprite: Mobs/Effects/burn_damage.rsi
   - type: Bloodstream
-    bloodReagent: AmmoniaBlood
+    bloodReagents:
+      reagents:
+      - ReagentId: AmmoniaBlood
+        Quantity: 1
   - type: MeleeWeapon
     soundHit:
       collection: AlienClaw