diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs
index 7e5dd43715..a13ef741be 100644
--- a/Content.Client/EntryPoint.cs
+++ b/Content.Client/EntryPoint.cs
@@ -138,7 +138,11 @@ namespace Content.Client
"TransformableContainer",
"Mind",
"MovementSpeedModifier",
- "StorageFill"
+ "StorageFill",
+ "Mop",
+ "Bucket",
+ "Puddle",
+ "CanSpill",
};
foreach (var ignoreName in registerIgnore)
diff --git a/Content.Server/GameObjects/Components/Fluids/BucketComponent.cs b/Content.Server/GameObjects/Components/Fluids/BucketComponent.cs
new file mode 100644
index 0000000000..98b9cf9891
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Fluids/BucketComponent.cs
@@ -0,0 +1,120 @@
+using System;
+using Content.Server.GameObjects.Components.Chemistry;
+using Content.Server.GameObjects.Components.Sound;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.Chemistry;
+using Content.Shared.Interfaces;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Serialization;
+
+namespace Content.Server.GameObjects.Components.Fluids
+{
+ ///
+ /// Can a mop click on this entity and dump its fluids
+ ///
+ [RegisterComponent]
+ public class BucketComponent : Component, IAttackBy
+ {
+#pragma warning disable 649
+ [Dependency] private readonly ILocalizationManager _localizationManager;
+#pragma warning restore 649
+
+ public override string Name => "Bucket";
+
+ public ReagentUnit MaxVolume
+ {
+ get => _contents.MaxVolume;
+ set => _contents.MaxVolume = value;
+ }
+
+ public ReagentUnit CurrentVolume => _contents.CurrentVolume;
+
+ private SolutionComponent _contents;
+
+ private string _sound;
+
+ ///
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ serializer.DataFieldCached(ref _sound, "sound", "/Audio/effects/Fluids/watersplash.ogg");
+ }
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+ _contents = Owner.GetComponent();
+ }
+
+ private bool TryGiveToMop(MopComponent mopComponent)
+ {
+ // Let's fill 'er up
+ // If this is called the mop should be empty but just in case we'll do Max - Current
+ var transferAmount = ReagentUnit.Min(mopComponent.MaxVolume - mopComponent.CurrentVolume, CurrentVolume);
+ var solution = _contents.SplitSolution(transferAmount);
+ if (!mopComponent.Contents.TryAddSolution(solution) || mopComponent.CurrentVolume == 0)
+ {
+ return false;
+ }
+
+ if (_sound == null)
+ {
+ return true;
+ }
+
+ Owner.TryGetComponent(out SoundComponent soundComponent);
+ soundComponent?.Play(_sound);
+
+ return true;
+ }
+
+ public bool AttackBy(AttackByEventArgs eventArgs)
+ {
+ if (!eventArgs.AttackWith.TryGetComponent(out MopComponent mopComponent))
+ {
+ return false;
+ }
+
+ // Give to the mop if it's empty
+ if (mopComponent.CurrentVolume == 0)
+ {
+ if (!TryGiveToMop(mopComponent))
+ {
+ return false;
+ }
+
+ Owner.PopupMessage(eventArgs.User, _localizationManager.GetString("Splish"));
+ return true;
+ }
+
+ var transferAmount = ReagentUnit.Min(mopComponent.CurrentVolume, MaxVolume - CurrentVolume);
+ if (transferAmount == 0)
+ {
+ return false;
+ }
+
+ var solution = mopComponent.Contents.SplitSolution(transferAmount);
+ if (!_contents.TryAddSolution(solution))
+ {
+ //This really shouldn't happen
+ throw new InvalidOperationException();
+ }
+
+ // Give some visual feedback shit's happening (for anyone who can't hear sound)
+ Owner.PopupMessage(eventArgs.User, _localizationManager.GetString("Sploosh"));
+
+ if (_sound == null)
+ {
+ return true;
+ }
+
+ Owner.TryGetComponent(out SoundComponent soundComponent);
+ soundComponent?.Play(_sound);
+
+ return true;
+
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Fluids/CanSpillComponent.cs b/Content.Server/GameObjects/Components/Fluids/CanSpillComponent.cs
new file mode 100644
index 0000000000..b48fca2312
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Fluids/CanSpillComponent.cs
@@ -0,0 +1,47 @@
+using System;
+using Content.Server.GameObjects.Components.Chemistry;
+using Content.Shared.Chemistry;
+using Content.Shared.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.GameObjects.Components.Fluids
+{
+ [RegisterComponent]
+ public class CanSpillComponent : Component
+ {
+ public override string Name => "CanSpill";
+ // TODO: If the Owner doesn't have a SolutionComponent straight up just have this remove itself?
+
+ ///
+ /// Transfers solution from the held container to the target container.
+ ///
+ [Verb]
+ private sealed class FillTargetVerb : Verb
+ {
+ protected override string GetText(IEntity user, CanSpillComponent component)
+ {
+ return "Spill liquid";
+ }
+
+ protected override VerbVisibility GetVisibility(IEntity user, CanSpillComponent component)
+ {
+ if (component.Owner.TryGetComponent(out SolutionComponent solutionComponent))
+ {
+ return solutionComponent.CurrentVolume > ReagentUnit.Zero ? VerbVisibility.Visible : VerbVisibility.Disabled;
+ }
+
+ return VerbVisibility.Invisible;
+ }
+
+ protected override void Activate(IEntity user, CanSpillComponent component)
+ {
+ var solutionComponent = component.Owner.GetComponent();
+ // Need this as when we split the component's owner may be deleted
+ var entityLocation = component.Owner.Transform.GridPosition;
+ var solution = solutionComponent.SplitSolution(solutionComponent.CurrentVolume);
+ SpillHelper.SpillAt(entityLocation, solution, "PuddleSmear");
+ }
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Fluids/MopComponent.cs b/Content.Server/GameObjects/Components/Fluids/MopComponent.cs
new file mode 100644
index 0000000000..22f722ea9c
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Fluids/MopComponent.cs
@@ -0,0 +1,112 @@
+using System;
+using Content.Server.GameObjects.Components.Chemistry;
+using Content.Server.GameObjects.Components.Sound;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.Chemistry;
+using Content.Shared.Interfaces;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Localization;
+using Robust.Shared.Serialization;
+
+namespace Content.Server.GameObjects.Components.Fluids
+{
+ ///
+ /// For cleaning up puddles
+ ///
+ [RegisterComponent]
+ public class MopComponent : Component, IAfterAttack
+ {
+#pragma warning disable 649
+ [Dependency] private readonly ILocalizationManager _localizationManager;
+#pragma warning restore 649
+
+ public override string Name => "Mop";
+ internal SolutionComponent Contents => _contents;
+ private SolutionComponent _contents;
+
+ public ReagentUnit MaxVolume
+ {
+ get => _contents.MaxVolume;
+ set => _contents.MaxVolume = value;
+ }
+
+ public ReagentUnit CurrentVolume => _contents.CurrentVolume;
+
+ // Currently there's a separate amount for pickup and dropoff so
+ // Picking up a puddle requires multiple clicks
+ // Dumping in a bucket requires 1 click
+ // Long-term you'd probably use a cooldown and start the pickup once we have some form of global cooldown
+ public ReagentUnit PickupAmount => _pickupAmount;
+ private ReagentUnit _pickupAmount;
+
+ private string _pickupSound;
+
+ ///
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ serializer.DataFieldCached(ref _pickupSound, "pickup_sound", "/Audio/effects/Fluids/slosh.ogg");
+ // The turbo mop will pickup more
+ serializer.DataFieldCached(ref _pickupAmount, "pickup_amount", ReagentUnit.New(5));
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _contents = Owner.GetComponent();
+
+ }
+
+ void IAfterAttack.AfterAttack(AfterAttackEventArgs eventArgs)
+ {
+ Solution solution;
+ if (eventArgs.Attacked == null)
+ {
+ if (CurrentVolume <= 0)
+ {
+ return;
+ }
+ // Drop the liquid on the mop on to the ground I guess? Potentially change by design
+ // Maybe even use a toggle mode instead of "Pickup" and "dropoff"
+ solution = _contents.SplitSolution(CurrentVolume);
+ SpillHelper.SpillAt(eventArgs.ClickLocation, solution, "PuddleSmear");
+
+ return;
+ }
+
+ if (!eventArgs.Attacked.TryGetComponent(out PuddleComponent puddleComponent))
+ {
+ return;
+ }
+ // Essentially pickup either:
+ // - _pickupAmount,
+ // - whatever's left in the puddle, or
+ // - whatever we can still hold (whichever's smallest)
+ var transferAmount = ReagentUnit.Min(ReagentUnit.New(5), puddleComponent.CurrentVolume, MaxVolume - CurrentVolume);
+ if (transferAmount == 0)
+ {
+ return;
+ }
+
+ solution = puddleComponent.SplitSolution(transferAmount);
+ // Probably don't recolor a mop? Could work, if we layered it maybe
+ if (!_contents.TryAddSolution(solution, false, true))
+ {
+ // I can't imagine why this would happen
+ throw new InvalidOperationException();
+ }
+
+ // Give some visual feedback shit's happening (for anyone who can't hear sound)
+ Owner.PopupMessage(eventArgs.User, _localizationManager.GetString("Swish"));
+
+ if (_pickupSound == null)
+ {
+ return;
+ }
+
+ Owner.TryGetComponent(out SoundComponent soundComponent);
+ soundComponent?.Play(_pickupSound);
+
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Fluids/PuddleComponent.cs b/Content.Server/GameObjects/Components/Fluids/PuddleComponent.cs
new file mode 100644
index 0000000000..ebc8d8b3be
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Fluids/PuddleComponent.cs
@@ -0,0 +1,333 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Content.Server.GameObjects.Components.Chemistry;
+using Content.Shared.Chemistry;
+using Content.Shared.Physics;
+using Robust.Server.GameObjects;
+using Robust.Server.GameObjects.EntitySystems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Components;
+using Robust.Shared.GameObjects.Components.Transform;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Random;
+using Robust.Shared.IoC;
+using Robust.Shared.Maths;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
+using Timer = Robust.Shared.Timers.Timer;
+
+namespace Content.Server.GameObjects.Components.Fluids
+{
+ ///
+ /// Puddle on a floor
+ ///
+ [RegisterComponent]
+ public class PuddleComponent : Component
+ {
+ // Current design: Something calls the SpillHelper.Spill, that will either
+ // A) Add to an existing puddle at the location (normalised to tile-center) or
+ // B) add a new one
+ // From this every time a puddle is spilt on it will try and overflow to its neighbours if possible,
+ // and also update its appearance based on volume level (opacity) and chemistry color
+ // Small puddles will evaporate after a set delay
+
+ // TODO: 'leaves fluidtracks', probably in a separate component for stuff like gibb chunks?;
+ // TODO: Add stuff like slipping -> probably in a separate component (for stuff like bananas) and using BumpEntMsg
+
+ // based on behaviour (e.g. someone being punched vs slashed with a sword would have different blood sprite)
+ // to check for low volumes for evaporation or whatever
+
+ public override string Name => "Puddle";
+
+ private CancellationTokenSource _evaporationToken;
+ private ReagentUnit _evaporateThreshold; // How few we can hold prior to self-destructing
+ private float _evaporateTime;
+ private string _spillSound;
+ private DateTime _lastOverflow = DateTime.Now;
+ private SpriteComponent _spriteComponent;
+
+ private SnapGridComponent _snapGrid;
+
+ public ReagentUnit MaxVolume
+ {
+ get => _contents.MaxVolume;
+ set => _contents.MaxVolume = value;
+ }
+
+ [ViewVariables]
+ public ReagentUnit CurrentVolume => _contents.CurrentVolume;
+
+ // Volume at which the fluid will try to spill to adjacent components
+ // Currently a random number, potentially change
+ public ReagentUnit OverflowVolume => _overflowVolume;
+ [ViewVariables]
+ private ReagentUnit _overflowVolume;
+
+ private SolutionComponent _contents;
+ private int _spriteVariants;
+ // Whether the underlying solution color should be used
+ private bool _recolor;
+
+ ///
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ serializer.DataFieldCached(ref _spillSound, "spill_sound", "/Audio/effects/Fluids/splat.ogg");
+ serializer.DataField(ref _overflowVolume, "overflow_volume", ReagentUnit.New(20));
+ serializer.DataField(ref _evaporateTime, "evaporate_time", 600.0f);
+ // Long-term probably have this based on the underlying reagents
+ serializer.DataField(ref _evaporateThreshold, "evaporate_threshold", ReagentUnit.New(2));
+ serializer.DataField(ref _spriteVariants, "variants", 1);
+ serializer.DataField(ref _recolor, "recolor", false);
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ if (Owner.TryGetComponent(out SolutionComponent solutionComponent))
+ {
+ _contents = solutionComponent;
+ }
+ else
+ {
+ _contents = Owner.AddComponent();
+ _contents.Initialize();
+ }
+
+ _snapGrid = Owner.GetComponent();
+
+ // Smaller than 1m^3 for now but realistically this shouldn't be hit
+ MaxVolume = ReagentUnit.New(1000);
+
+ // Random sprite state set server-side so it's consistent across all clients
+ _spriteComponent = Owner.GetComponent();
+ var robustRandom = IoCManager.Resolve();
+ var randomVariant = robustRandom.Next(0, _spriteVariants - 1);
+ var baseName = new ResourcePath(_spriteComponent.BaseRSIPath).FilenameWithoutExtension;
+
+ _spriteComponent.LayerSetState(0, $"{baseName}-{randomVariant}"); // TODO: Remove hardcode
+ _spriteComponent.Rotation = Angle.FromDegrees(robustRandom.Next(0, 359));
+ // UpdateAppearance should get called soon after this so shouldn't need to call Dirty() here
+ }
+
+ // Flow rate should probably be controlled globally so this is it for now
+ internal bool TryAddSolution(Solution solution, bool sound = true, bool checkForEvaporate = true)
+ {
+ if (solution.TotalVolume == 0)
+ {
+ return false;
+ }
+ var result = _contents.TryAddSolution(solution);
+ if (!result)
+ {
+ return false;
+ }
+
+ UpdateStatus();
+ CheckOverflow();
+ if (checkForEvaporate)
+ {
+ CheckEvaporate();
+ }
+
+ UpdateAppearance();
+ if (!sound)
+ {
+ return true;
+ }
+
+ var entitySystemManager = IoCManager.Resolve();
+ entitySystemManager.GetEntitySystem().Play(_spillSound);
+ return true;
+ }
+
+ internal Solution SplitSolution(ReagentUnit quantity)
+ {
+ var split = _contents.SplitSolution(quantity);
+ CheckEvaporate();
+ UpdateAppearance();
+ return split;
+ }
+
+ public void CheckEvaporate()
+ {
+ if (CurrentVolume == 0)
+ {
+ Owner.Delete();
+ }
+ }
+
+ private void UpdateStatus()
+ {
+ // If UpdateStatus is getting called again it means more fluid has been updated so let's just wait
+ _evaporationToken?.Cancel();
+
+ if (CurrentVolume > _evaporateThreshold)
+ {
+ return;
+ }
+
+ _evaporationToken = new CancellationTokenSource();
+
+ // KYS to evaporate
+ Timer.Spawn(TimeSpan.FromSeconds(_evaporateTime), CheckEvaporate, _evaporationToken.Token);
+ }
+
+ private void UpdateAppearance()
+ {
+ if (Owner.Deleted)
+ {
+ return;
+ }
+ // Opacity based on level of fullness to overflow
+ // Hard-cap lower bound for visibility reasons
+ var volumeScale = (CurrentVolume.Float() / OverflowVolume.Float()) * 0.75f + 0.25f;
+ var cappedScale = Math.Min(1.0f, volumeScale);
+ // Color based on the underlying solutioncomponent
+ Color newColor;
+ if (_recolor)
+ {
+ newColor = _contents.SubstanceColor.WithAlpha(cappedScale);
+ }
+ else
+ {
+ newColor = _spriteComponent.Color.WithAlpha(cappedScale);
+ }
+
+ _spriteComponent.Color = newColor;
+
+ _spriteComponent.Dirty();
+
+ }
+
+ ///
+ /// Will overflow this entity to neighboring entities if required
+ ///
+ private void CheckOverflow()
+ {
+ if (CurrentVolume <= _overflowVolume)
+ {
+ return;
+ }
+
+ // Essentially:
+ // Spill at least 1 solution to each neighbor (so most of the time each puddle is getting 1 max)
+ // If there's no puddle at the neighbor then add one.
+
+ // Setup
+ // If there's more neighbors to spill to then there are reagents to go around (coz integers)
+ var overflowAmount = CurrentVolume - OverflowVolume;
+
+ var neighborPuddles = new List(8);
+
+ // Will overflow to each neighbor; if it already has a puddle entity then add to that
+
+ foreach (var direction in RandomDirections())
+ {
+ // Can't spill < 1 reagent so stop overflowing
+ if ((ReagentUnit.Epsilon * neighborPuddles.Count) == overflowAmount)
+ {
+ break;
+ }
+
+ // If we found an existing puddle on that tile then we don't need to spawn a new one
+ var noSpawn = false;
+
+ foreach (var entity in _snapGrid.GetInDir(direction))
+ {
+ // Don't overflow to walls
+ if (entity.TryGetComponent(out CollidableComponent collidableComponent) &&
+ collidableComponent.CollisionLayer == (int) CollisionGroup.Impassable)
+ {
+ noSpawn = true;
+ break;
+ }
+
+ if (!entity.TryGetComponent(out PuddleComponent puddleComponent))
+ {
+ continue;
+ }
+
+ // If we've overflowed recently don't include it
+ noSpawn = true;
+ // TODO: PauseManager
+ if ((DateTime.Now - puddleComponent._lastOverflow).TotalSeconds < 1)
+ {
+ break;
+ }
+
+ neighborPuddles.Add(entity);
+ break;
+ }
+
+ if (noSpawn)
+ {
+ continue;
+ }
+
+ var grid = _snapGrid.DirectionToGrid(direction);
+ // We'll just add the co-ordinates as we need to figure out how many puddles we need to spawn first
+ var entityManager = IoCManager.Resolve();
+ neighborPuddles.Add(entityManager.SpawnEntity(Owner.Prototype.ID, grid));
+ }
+
+ if (neighborPuddles.Count == 0)
+ {
+ return;
+ }
+
+ var spillAmount = overflowAmount / ReagentUnit.New(neighborPuddles.Count);
+
+ SpillToNeighbours(neighborPuddles, spillAmount);
+ }
+
+ // TODO: Move the below to SnapGrid?
+ ///
+ /// Will yield a random direction until none are left
+ ///
+ ///
+ private static IEnumerable RandomDirections()
+ {
+ var directions = new[]
+ {
+ Direction.East,
+ Direction.SouthEast,
+ Direction.South,
+ Direction.SouthWest,
+ Direction.West,
+ Direction.NorthWest,
+ Direction.North,
+ Direction.NorthEast,
+ };
+
+ var robustRandom = IoCManager.Resolve();
+ var n = directions.Length;
+
+ while (n > 1)
+ {
+ n--;
+ var k = robustRandom.Next(n + 1);
+ var value = directions[k];
+ directions[k] = directions[n];
+ directions[n] = value;
+ }
+
+ foreach (var direction in directions)
+ {
+ yield return direction;
+ }
+ }
+
+ private void SpillToNeighbours(IEnumerable neighbors, ReagentUnit spillAmount)
+ {
+ foreach (var neighborPuddle in neighbors)
+ {
+ var solution = _contents.SplitSolution(spillAmount);
+
+ neighborPuddle.GetComponent().TryAddSolution(solution, false, false);
+
+ }
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Fluids/SpillHelper.cs b/Content.Server/GameObjects/Components/Fluids/SpillHelper.cs
new file mode 100644
index 0000000000..e064a61e80
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Fluids/SpillHelper.cs
@@ -0,0 +1,90 @@
+using Content.Shared.Chemistry;
+using Robust.Server.Interfaces.GameObjects;
+
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Map;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+
+namespace Content.Server.GameObjects.Components.Fluids
+{
+ public static class SpillHelper
+ {
+
+ ///
+ /// Spills the specified solution at the entity's location if possible.
+ ///
+ /// Entity location to spill at
+ /// Initial solution for the prototype
+ /// Prototype to use
+ internal static void SpillAt(IEntity entity, Solution solution, string prototype)
+ {
+ var entityLocation = entity.Transform.GridPosition;
+ SpillAt(entityLocation, solution, prototype);
+ }
+
+ // Other functions will be calling this one
+
+ ///
+ /// Spills solution at the specified grid co-ordinates
+ ///
+ ///
+ /// Initial solution for the prototype
+ /// Prototype to use
+ internal static void SpillAt(GridCoordinates gridCoordinates, Solution solution, string prototype)
+ {
+ if (solution.TotalVolume == 0)
+ {
+ return;
+ }
+
+ var mapManager = IoCManager.Resolve();
+ var entityManager = IoCManager.Resolve();
+ var serverEntityManager = IoCManager.Resolve();
+
+ var mapGrid = mapManager.GetGrid(gridCoordinates.GridID);
+
+ // If space return early, let that spill go out into the void
+ var tileRef = mapGrid.GetTileRef(gridCoordinates);
+ if (tileRef.Tile.IsEmpty)
+ {
+ return;
+ }
+
+ // Get normalized co-ordinate for spill location and spill it in the centre
+ // TODO: Does SnapGrid or something else already do this?
+ var spillTileMapGrid = mapManager.GetGrid(gridCoordinates.GridID);
+ var spillTileRef = spillTileMapGrid.GetTileRef(gridCoordinates).GridIndices;
+ var spillGridCoords = spillTileMapGrid.GridTileToLocal(spillTileRef);
+
+ var spilt = false;
+
+ foreach (var spillEntity in entityManager.GetEntitiesAt(spillTileMapGrid.ParentMapId, spillGridCoords.Position))
+ {
+ if (!spillEntity.TryGetComponent(out PuddleComponent puddleComponent))
+ {
+ continue;
+ }
+
+ if (!puddleComponent.TryAddSolution(solution))
+ {
+ continue;
+ }
+
+ spilt = true;
+ break;
+ }
+
+ // Did we add to an existing puddle
+ if (spilt)
+ {
+ return;
+ }
+
+ var puddle = serverEntityManager.SpawnEntity(prototype, spillGridCoords);
+ puddle.GetComponent().TryAddSolution(solution);
+ }
+
+ }
+
+}
diff --git a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs
index 3a3886f52b..f7741b8f44 100644
--- a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs
+++ b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.Sound;
using Content.Server.GameObjects.EntitySystems;
@@ -7,6 +8,7 @@ using Content.Shared.GameObjects.Components.Nutrition;
using Content.Shared.Interfaces;
using Content.Shared.Maths;
using Robust.Server.GameObjects;
+using Robust.Server.GameObjects.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC;
@@ -119,9 +121,12 @@ namespace Content.Server.GameObjects.Components.Nutrition
var split = _contents.SplitSolution(transferAmount);
if (stomachComponent.TryTransferSolution(split))
{
+ // When we split Finish gets called which may delete the can so need to use the entity system for sound
if (_useSound != null)
{
- Owner.GetComponent()?.Play(_useSound);
+ var entitySystemManager = IoCManager.Resolve();
+ var audioSystem = entitySystemManager.GetEntitySystem();
+ audioSystem.Play(_useSound);
user.PopupMessage(user, _localizationManager.GetString("Slurp"));
}
}
diff --git a/Content.Server/GameObjects/EntitySystems/PuddleSystem.cs b/Content.Server/GameObjects/EntitySystems/PuddleSystem.cs
new file mode 100644
index 0000000000..4cb51c3f6e
--- /dev/null
+++ b/Content.Server/GameObjects/EntitySystems/PuddleSystem.cs
@@ -0,0 +1,47 @@
+using Content.Server.GameObjects.Components.Fluids;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Components.Transform;
+using Robust.Shared.GameObjects.Systems;
+using Robust.Shared.Interfaces.Map;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+
+namespace Content.Server.GameObjects.EntitySystems
+{
+ public class PuddleSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ EntityQuery = new TypeEntityQuery(typeof(PuddleComponent));
+ var mapManager = IoCManager.Resolve();
+ mapManager.TileChanged += HandleTileChanged;
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ var mapManager = IoCManager.Resolve();
+ mapManager.TileChanged -= HandleTileChanged;
+ }
+
+ private void HandleTileChanged(object sender, TileChangedEventArgs eventArgs)
+ {
+ // If this gets hammered you could probably queue up all the tile changes every tick but I doubt that would ever happen.
+ var entities = EntityManager.GetEntities(EntityQuery);
+
+ foreach (var entity in entities)
+ {
+ // If the tile becomes space then delete it (potentially change by design)
+ if (eventArgs.NewTile.GridIndex == entity.Transform.GridID &&
+ entity.TryGetComponent(out SnapGridComponent snapGridComponent) &&
+ snapGridComponent.Position == eventArgs.NewTile.GridIndices &&
+ eventArgs.NewTile.Tile.IsEmpty)
+ {
+ entity.Delete();
+ break; // Currently it's one puddle per tile, if that changes remove this
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Chemistry/ReagentUnit.cs b/Content.Shared/Chemistry/ReagentUnit.cs
index 43c2354f03..e74bd41fc8 100644
--- a/Content.Shared/Chemistry/ReagentUnit.cs
+++ b/Content.Shared/Chemistry/ReagentUnit.cs
@@ -1,7 +1,5 @@
using Robust.Shared.Interfaces.Serialization;
-using Robust.Shared.Serialization;
using System;
-using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
@@ -13,7 +11,9 @@ namespace Content.Shared.Chemistry
private int _value;
private static readonly int Shift = 2;
- public static ReagentUnit MaxValue => new ReagentUnit(int.MaxValue);
+ public static ReagentUnit MaxValue { get; } = new ReagentUnit(int.MaxValue);
+ public static ReagentUnit Epsilon { get; } = new ReagentUnit(1);
+ public static ReagentUnit Zero { get; } = new ReagentUnit(0);
private double ShiftDown()
{
@@ -131,6 +131,16 @@ namespace Content.Shared.Chemistry
return a.ShiftDown() != b;
}
+ public static bool operator ==(ReagentUnit a, ReagentUnit b)
+ {
+ return a.Equals(b);
+ }
+
+ public static bool operator !=(ReagentUnit a, ReagentUnit b)
+ {
+ return !a.Equals(b);
+ }
+
public static bool operator <=(ReagentUnit a, ReagentUnit b)
{
return a._value <= b._value;
@@ -204,12 +214,12 @@ namespace Content.Shared.Chemistry
return ToString();
}
- public bool Equals([AllowNull] ReagentUnit other)
+ public bool Equals(ReagentUnit other)
{
return _value == other._value;
}
- public int CompareTo([AllowNull] ReagentUnit other)
+ public int CompareTo(ReagentUnit other)
{
if(other._value > _value)
{
diff --git a/Content.Tests/Shared/Chemistry/Solution_Tests.cs b/Content.Tests/Shared/Chemistry/Solution_Tests.cs
index d5d101353e..26d40328c9 100644
--- a/Content.Tests/Shared/Chemistry/Solution_Tests.cs
+++ b/Content.Tests/Shared/Chemistry/Solution_Tests.cs
@@ -12,7 +12,7 @@ namespace Content.Tests.Shared.Chemistry
var solution = new Solution();
solution.AddReagent("water", ReagentUnit.New(1000));
var quantity = solution.GetReagentQuantity("water");
-
+
Assert.That(quantity.Int(), Is.EqualTo(1000));
}
@@ -302,6 +302,27 @@ namespace Content.Tests.Shared.Chemistry
Assert.That(splitSolution.TotalVolume.Int(), Is.EqualTo(0));
}
+ [Test]
+ public void SplitSolutionZero()
+ {
+ var solution = new Solution();
+ solution.AddReagent("chem.Impedrezene", ReagentUnit.New(0.01 + 0.19));
+ solution.AddReagent("chem.Thermite", ReagentUnit.New(0.01 + 0.39));
+ solution.AddReagent("chem.Li", ReagentUnit.New(0.01 + 0.17));
+ solution.AddReagent("chem.F", ReagentUnit.New(0.01 + 0.17));
+ solution.AddReagent("chem.Na", ReagentUnit.New(0 + 0.13));
+ solution.AddReagent("chem.Hg", ReagentUnit.New(0.15 + 4.15));
+ solution.AddReagent("chem.Cu", ReagentUnit.New(0 + 0.13));
+ solution.AddReagent("chem.U", ReagentUnit.New(0.76 + 20.77));
+ solution.AddReagent("chem.Fe", ReagentUnit.New(0.01 + 0.36));
+ solution.AddReagent("chem.SpaceDrugs", ReagentUnit.New(0.02 + 0.41));
+ solution.AddReagent("chem.Al", ReagentUnit.New(0));
+ solution.AddReagent("chem.Glucose", ReagentUnit.New(0));
+ solution.AddReagent("chem.O", ReagentUnit.New(0));
+
+ solution.SplitSolution(ReagentUnit.New(0.98));
+ }
+
[Test]
public void AddSolution()
{
diff --git a/Resources/Audio/effects/Fluids/slosh.ogg b/Resources/Audio/effects/Fluids/slosh.ogg
new file mode 100644
index 0000000000..78df794db1
Binary files /dev/null and b/Resources/Audio/effects/Fluids/slosh.ogg differ
diff --git a/Resources/Audio/effects/Fluids/splat.ogg b/Resources/Audio/effects/Fluids/splat.ogg
new file mode 100644
index 0000000000..bc84113d34
Binary files /dev/null and b/Resources/Audio/effects/Fluids/splat.ogg differ
diff --git a/Resources/Audio/effects/Fluids/watersplash.ogg b/Resources/Audio/effects/Fluids/watersplash.ogg
new file mode 100644
index 0000000000..cad20d6354
Binary files /dev/null and b/Resources/Audio/effects/Fluids/watersplash.ogg differ
diff --git a/Resources/Prototypes/Entities/Fluids/puddle.yml b/Resources/Prototypes/Entities/Fluids/puddle.yml
new file mode 100644
index 0000000000..dfedada852
--- /dev/null
+++ b/Resources/Prototypes/Entities/Fluids/puddle.yml
@@ -0,0 +1,119 @@
+# TODO: Add the other mess types
+- type: entity
+ id: PuddleBase
+ abstract: true
+ components:
+ - type: SnapGrid
+ offset: Center
+ - type: Sprite
+ drawdepth: FloorObjects
+ - type: Solution
+ caps: 1
+ - type: Puddle
+ spill_sound: /Audio/effects/Fluids/splat.ogg
+ recolor: true
+ - type: Sound
+ - type: InteractionOutline
+ - type: Clickable
+ - type: Collidable
+ shapes:
+ - !type:PhysShapeAabb
+ bounds: "-0.5,-0.5,0.5,0.5"
+ hard: false
+
+- type: entity
+ name: Puddle
+ id: PuddleGeneric
+ parent: PuddleSmear
+
+- type: entity
+ name: Gibblets
+ id: PuddleGibblet
+ parent: PuddleBase
+ description: Holds spilt milk
+ components:
+ - type: Sprite
+ sprite: Fluids/gibblet.rsi # Placeholder
+ state: gibblet-0
+ - type: Icon
+ icon: Fluids/gibblet.rsi
+ state: gibblet-0
+ - type: Puddle
+ variants: 5
+
+- type: entity
+ name: Smear
+ id: PuddleSmear
+ parent: PuddleBase
+ description: Holds spilt milk
+ components:
+ - type: Sprite
+ sprite: Fluids/smear.rsi # Placeholder
+ state: smear-0
+ - type: Icon
+ icon: Fluids/smear.rsi
+ state: smear-0
+ - type: Puddle
+ variants: 7
+
+- type: entity
+ name: Splatter
+ id: PuddleSplatter
+ parent: PuddleBase
+ description: Holds spilt milk
+ components:
+ - type: Sprite
+ sprite: Fluids/splatter.rsi # Placeholder
+ state: splatter-0
+ - type: Icon
+ icon: Fluids/splatter.rsi
+ state: splatter-0
+ - type: Puddle
+ variants: 6
+
+- type: entity
+ name: Vomit
+ id: PuddleVomit
+ parent: PuddleBase
+ description:
+ components:
+ - type: Sprite
+ sprite: Fluids/vomit.rsi
+ state: vomit-0
+ - type: Icon
+ icon: Fluids/vomit.rsi
+ state: vomit-0
+ - type: Puddle
+ variants: 4
+ recolor: false
+
+- type: entity
+ name: Toxins vomit
+ id: PuddleVomitToxin
+ parent: PuddleBase
+ description: You probably don't want to get too close to this
+ components:
+ - type: Sprite
+ sprite: Fluids/vomit_toxin.rsi
+ state: vomit_toxin-0
+ - type: Icon
+ icon: Fluids/vomit_toxin.rsi
+ state: vomit_toxin-0
+ - type: Puddle
+ variants: 4
+ recolor: false
+
+- type: entity
+ name: Writing
+ id: PuddleWriting
+ parent: PuddleBase
+ description: Holds spilt milk
+ components:
+ - type: Sprite
+ sprite: Fluids/writing.rsi # Placeholder
+ state: writing-0
+ - type: Icon
+ icon: Fluids/writing.rsi
+ state: writing-0
+ - type: Puddle
+ variants: 5
diff --git a/Resources/Prototypes/Entities/Janitor.yml b/Resources/Prototypes/Entities/Janitor.yml
index b10acbc4a6..fbc1c0bf9a 100644
--- a/Resources/Prototypes/Entities/Janitor.yml
+++ b/Resources/Prototypes/Entities/Janitor.yml
@@ -1,5 +1,5 @@
- type: entity
- parent: ReagentItem
+ parent: BaseItem
name: "Extra-Grip™ Mop"
id: MopItem
description: A mop that can't be stopped, viscera cleanup detail awaits.
@@ -10,36 +10,68 @@
texture: Objects/Janitorial/mop.png
- type: Item
Size: 10
+ - type: Mop
- type: Solution
maxVol: 10
caps: 1
+ - type: Sound
- type: entity
- parent: ReagentItem
+ parent: BaseItem
name: Mop Bucket
id: MopBucket
description: Holds water and the tears of the janitor.
components:
+ - type: Clickable
- type: Sprite
texture: Objects/Janitorial/mopbucket.png
+ drawdepth: Objects
- type: Icon
texture: Objects/Janitorial/mopbucket.png
- type: Clickable
- type: InteractionOutline
+ - type: Bucket
+ - type: Sound
- type: Solution
maxVol: 500
caps: 3
+ - type: Collidable
+ shapes:
+ - !type:PhysShapeAabb
+ bounds: "-0.25,-0.25,0.25,0.25"
+ mask: 3
+ layer: 1
+ IsScrapingFloor: true
+ - type: Physics
+ mass: 5
+ Anchored: false
+ - type: Sound
- type: entity
- parent: ReagentItem
+ parent: BaseItem
name: Bucket
id: Bucket
description: "It's a bucket."
components:
+ - type: Clickable
- type: Sprite
texture: Objects/Janitorial/bucket.png
+ drawdepth: Objects
- type: Icon
texture: Objects/Janitorial/bucket.png
+ - type: Bucket
+ - type: Sound
- type: Solution
maxVol: 500
caps: 3
+ - type: Collidable
+ shapes:
+ - !type:PhysShapeAabb
+ bounds: "-0.25,-0.25,0.25,0.25"
+ mask: 3
+ layer: 1
+ IsScrapingFloor: true
+ - type: Physics
+ mass: 5
+ Anchored: false
+ - type: Sound
diff --git a/Resources/Prototypes/Reagents/chemicals.yml b/Resources/Prototypes/Reagents/chemicals.yml
index e8d0b18dd3..2886f5accd 100644
--- a/Resources/Prototypes/Reagents/chemicals.yml
+++ b/Resources/Prototypes/Reagents/chemicals.yml
@@ -1,7 +1,7 @@
- type: reagent
id: chem.Nutriment
name: Nutriment
- desc: Generic nutrition
+ desc: All the vitamins, minerals, and carbohydrates the body needs in pure form.
color: "#664330"
metabolism:
- !type:DefaultFood
@@ -17,7 +17,7 @@
id: chem.H2O
name: Water
desc: A tasty colorless liquid.
- color: "#808080"
+ color: "#DEF7F5"
metabolism:
- !type:DefaultDrink
rate: 1
diff --git a/Resources/Textures/Fluids/gibblet.rsi/gibblet-0.png b/Resources/Textures/Fluids/gibblet.rsi/gibblet-0.png
new file mode 100644
index 0000000000..d43552e57f
Binary files /dev/null and b/Resources/Textures/Fluids/gibblet.rsi/gibblet-0.png differ
diff --git a/Resources/Textures/Fluids/gibblet.rsi/gibblet-1.png b/Resources/Textures/Fluids/gibblet.rsi/gibblet-1.png
new file mode 100644
index 0000000000..601c6217bf
Binary files /dev/null and b/Resources/Textures/Fluids/gibblet.rsi/gibblet-1.png differ
diff --git a/Resources/Textures/Fluids/gibblet.rsi/gibblet-2.png b/Resources/Textures/Fluids/gibblet.rsi/gibblet-2.png
new file mode 100644
index 0000000000..c4253b1561
Binary files /dev/null and b/Resources/Textures/Fluids/gibblet.rsi/gibblet-2.png differ
diff --git a/Resources/Textures/Fluids/gibblet.rsi/gibblet-3.png b/Resources/Textures/Fluids/gibblet.rsi/gibblet-3.png
new file mode 100644
index 0000000000..05ea5903a2
Binary files /dev/null and b/Resources/Textures/Fluids/gibblet.rsi/gibblet-3.png differ
diff --git a/Resources/Textures/Fluids/gibblet.rsi/gibblet-4.png b/Resources/Textures/Fluids/gibblet.rsi/gibblet-4.png
new file mode 100644
index 0000000000..2b952839d7
Binary files /dev/null and b/Resources/Textures/Fluids/gibblet.rsi/gibblet-4.png differ
diff --git a/Resources/Textures/Fluids/gibblet.rsi/meta.json b/Resources/Textures/Fluids/gibblet.rsi/meta.json
new file mode 100644
index 0000000000..9e665591fa
--- /dev/null
+++ b/Resources/Textures/Fluids/gibblet.rsi/meta.json
@@ -0,0 +1,31 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
+ "states": [
+ {
+ "name": "gibblet-0",
+ "directions": 1
+ },
+ {
+ "name": "gibblet-1",
+ "directions": 1
+ },
+ {
+ "name": "gibblet-2",
+ "directions": 1
+ },
+ {
+ "name": "gibblet-3",
+ "directions": 1
+ },
+ {
+ "name": "gibblet-4",
+ "directions": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Fluids/smear.rsi/meta.json b/Resources/Textures/Fluids/smear.rsi/meta.json
new file mode 100644
index 0000000000..3450d9e4de
--- /dev/null
+++ b/Resources/Textures/Fluids/smear.rsi/meta.json
@@ -0,0 +1,39 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
+ "states": [
+ {
+ "name": "smear-0",
+ "directions": 1
+ },
+ {
+ "name": "smear-1",
+ "directions": 1
+ },
+ {
+ "name": "smear-2",
+ "directions": 1
+ },
+ {
+ "name": "smear-3",
+ "directions": 1
+ },
+ {
+ "name": "smear-4",
+ "directions": 1
+ },
+ {
+ "name": "smear-5",
+ "directions": 1
+ },
+ {
+ "name": "smear-6",
+ "directions": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Fluids/smear.rsi/smear-0.png b/Resources/Textures/Fluids/smear.rsi/smear-0.png
new file mode 100644
index 0000000000..3fe54f28de
Binary files /dev/null and b/Resources/Textures/Fluids/smear.rsi/smear-0.png differ
diff --git a/Resources/Textures/Fluids/smear.rsi/smear-1.png b/Resources/Textures/Fluids/smear.rsi/smear-1.png
new file mode 100644
index 0000000000..12a5a3430a
Binary files /dev/null and b/Resources/Textures/Fluids/smear.rsi/smear-1.png differ
diff --git a/Resources/Textures/Fluids/smear.rsi/smear-2.png b/Resources/Textures/Fluids/smear.rsi/smear-2.png
new file mode 100644
index 0000000000..8dc6eaab76
Binary files /dev/null and b/Resources/Textures/Fluids/smear.rsi/smear-2.png differ
diff --git a/Resources/Textures/Fluids/smear.rsi/smear-3.png b/Resources/Textures/Fluids/smear.rsi/smear-3.png
new file mode 100644
index 0000000000..b34a145a15
Binary files /dev/null and b/Resources/Textures/Fluids/smear.rsi/smear-3.png differ
diff --git a/Resources/Textures/Fluids/smear.rsi/smear-4.png b/Resources/Textures/Fluids/smear.rsi/smear-4.png
new file mode 100644
index 0000000000..1065e9be33
Binary files /dev/null and b/Resources/Textures/Fluids/smear.rsi/smear-4.png differ
diff --git a/Resources/Textures/Fluids/smear.rsi/smear-5.png b/Resources/Textures/Fluids/smear.rsi/smear-5.png
new file mode 100644
index 0000000000..b6f2acc421
Binary files /dev/null and b/Resources/Textures/Fluids/smear.rsi/smear-5.png differ
diff --git a/Resources/Textures/Fluids/smear.rsi/smear-6.png b/Resources/Textures/Fluids/smear.rsi/smear-6.png
new file mode 100644
index 0000000000..e466049782
Binary files /dev/null and b/Resources/Textures/Fluids/smear.rsi/smear-6.png differ
diff --git a/Resources/Textures/Fluids/splatter.rsi/meta.json b/Resources/Textures/Fluids/splatter.rsi/meta.json
new file mode 100644
index 0000000000..5fcce5c890
--- /dev/null
+++ b/Resources/Textures/Fluids/splatter.rsi/meta.json
@@ -0,0 +1,35 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
+ "states": [
+ {
+ "name": "splatter-0",
+ "directions": 1
+ },
+ {
+ "name": "splatter-1",
+ "directions": 1
+ },
+ {
+ "name": "splatter-2",
+ "directions": 1
+ },
+ {
+ "name": "splatter-3",
+ "directions": 1
+ },
+ {
+ "name": "splatter-4",
+ "directions": 1
+ },
+ {
+ "name": "splatter-5",
+ "directions": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Fluids/splatter.rsi/splatter-0.png b/Resources/Textures/Fluids/splatter.rsi/splatter-0.png
new file mode 100644
index 0000000000..7c186e741c
Binary files /dev/null and b/Resources/Textures/Fluids/splatter.rsi/splatter-0.png differ
diff --git a/Resources/Textures/Fluids/splatter.rsi/splatter-1.png b/Resources/Textures/Fluids/splatter.rsi/splatter-1.png
new file mode 100644
index 0000000000..8db525cfd8
Binary files /dev/null and b/Resources/Textures/Fluids/splatter.rsi/splatter-1.png differ
diff --git a/Resources/Textures/Fluids/splatter.rsi/splatter-2.png b/Resources/Textures/Fluids/splatter.rsi/splatter-2.png
new file mode 100644
index 0000000000..8247205626
Binary files /dev/null and b/Resources/Textures/Fluids/splatter.rsi/splatter-2.png differ
diff --git a/Resources/Textures/Fluids/splatter.rsi/splatter-3.png b/Resources/Textures/Fluids/splatter.rsi/splatter-3.png
new file mode 100644
index 0000000000..aeef196d67
Binary files /dev/null and b/Resources/Textures/Fluids/splatter.rsi/splatter-3.png differ
diff --git a/Resources/Textures/Fluids/splatter.rsi/splatter-4.png b/Resources/Textures/Fluids/splatter.rsi/splatter-4.png
new file mode 100644
index 0000000000..29b051d95e
Binary files /dev/null and b/Resources/Textures/Fluids/splatter.rsi/splatter-4.png differ
diff --git a/Resources/Textures/Fluids/splatter.rsi/splatter-5.png b/Resources/Textures/Fluids/splatter.rsi/splatter-5.png
new file mode 100644
index 0000000000..92f9804430
Binary files /dev/null and b/Resources/Textures/Fluids/splatter.rsi/splatter-5.png differ
diff --git a/Resources/Textures/Fluids/vomit.rsi/meta.json b/Resources/Textures/Fluids/vomit.rsi/meta.json
new file mode 100644
index 0000000000..f4fd5064d3
--- /dev/null
+++ b/Resources/Textures/Fluids/vomit.rsi/meta.json
@@ -0,0 +1,27 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
+ "states": [
+ {
+ "name": "vomit-0",
+ "directions": 1
+ },
+ {
+ "name": "vomit-1",
+ "directions": 1
+ },
+ {
+ "name": "vomit-2",
+ "directions": 1
+ },
+ {
+ "name": "vomit-3",
+ "directions": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Fluids/vomit.rsi/vomit-0.png b/Resources/Textures/Fluids/vomit.rsi/vomit-0.png
new file mode 100644
index 0000000000..abb777ec22
Binary files /dev/null and b/Resources/Textures/Fluids/vomit.rsi/vomit-0.png differ
diff --git a/Resources/Textures/Fluids/vomit.rsi/vomit-1.png b/Resources/Textures/Fluids/vomit.rsi/vomit-1.png
new file mode 100644
index 0000000000..dc6499e5e3
Binary files /dev/null and b/Resources/Textures/Fluids/vomit.rsi/vomit-1.png differ
diff --git a/Resources/Textures/Fluids/vomit.rsi/vomit-2.png b/Resources/Textures/Fluids/vomit.rsi/vomit-2.png
new file mode 100644
index 0000000000..e1e2bcd6a5
Binary files /dev/null and b/Resources/Textures/Fluids/vomit.rsi/vomit-2.png differ
diff --git a/Resources/Textures/Fluids/vomit.rsi/vomit-3.png b/Resources/Textures/Fluids/vomit.rsi/vomit-3.png
new file mode 100644
index 0000000000..13ed9cb3d7
Binary files /dev/null and b/Resources/Textures/Fluids/vomit.rsi/vomit-3.png differ
diff --git a/Resources/Textures/Fluids/vomit_toxin.rsi/meta.json b/Resources/Textures/Fluids/vomit_toxin.rsi/meta.json
new file mode 100644
index 0000000000..2a60f9e2db
--- /dev/null
+++ b/Resources/Textures/Fluids/vomit_toxin.rsi/meta.json
@@ -0,0 +1,27 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
+ "states": [
+ {
+ "name": "vomit_toxin-0",
+ "directions": 1
+ },
+ {
+ "name": "vomit_toxin-1",
+ "directions": 1
+ },
+ {
+ "name": "vomit_toxin-2",
+ "directions": 1
+ },
+ {
+ "name": "vomit_toxin-3",
+ "directions": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-0.png b/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-0.png
new file mode 100644
index 0000000000..7931469583
Binary files /dev/null and b/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-0.png differ
diff --git a/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-1.png b/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-1.png
new file mode 100644
index 0000000000..e63513eeb5
Binary files /dev/null and b/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-1.png differ
diff --git a/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-2.png b/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-2.png
new file mode 100644
index 0000000000..92328a5ac2
Binary files /dev/null and b/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-2.png differ
diff --git a/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-3.png b/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-3.png
new file mode 100644
index 0000000000..6f5b3d845a
Binary files /dev/null and b/Resources/Textures/Fluids/vomit_toxin.rsi/vomit_toxin-3.png differ
diff --git a/Resources/Textures/Fluids/writing.rsi/meta.json b/Resources/Textures/Fluids/writing.rsi/meta.json
new file mode 100644
index 0000000000..0d24adffa6
--- /dev/null
+++ b/Resources/Textures/Fluids/writing.rsi/meta.json
@@ -0,0 +1,31 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "https://github.com/discordia-space/CEV-Eris/raw/aff0d780742ca3902d8b05f854c212c8cda32c4f/icons/effects/blood.dmi",
+ "states": [
+ {
+ "name": "writing-0",
+ "directions": 1
+ },
+ {
+ "name": "writing-1",
+ "directions": 1
+ },
+ {
+ "name": "writing-2",
+ "directions": 1
+ },
+ {
+ "name": "writing-3",
+ "directions": 1
+ },
+ {
+ "name": "writing-4",
+ "directions": 1
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Fluids/writing.rsi/writing-0.png b/Resources/Textures/Fluids/writing.rsi/writing-0.png
new file mode 100644
index 0000000000..591daa76b7
Binary files /dev/null and b/Resources/Textures/Fluids/writing.rsi/writing-0.png differ
diff --git a/Resources/Textures/Fluids/writing.rsi/writing-1.png b/Resources/Textures/Fluids/writing.rsi/writing-1.png
new file mode 100644
index 0000000000..9e0f3fc91d
Binary files /dev/null and b/Resources/Textures/Fluids/writing.rsi/writing-1.png differ
diff --git a/Resources/Textures/Fluids/writing.rsi/writing-2.png b/Resources/Textures/Fluids/writing.rsi/writing-2.png
new file mode 100644
index 0000000000..04bfcbf093
Binary files /dev/null and b/Resources/Textures/Fluids/writing.rsi/writing-2.png differ
diff --git a/Resources/Textures/Fluids/writing.rsi/writing-3.png b/Resources/Textures/Fluids/writing.rsi/writing-3.png
new file mode 100644
index 0000000000..c65c4aaec6
Binary files /dev/null and b/Resources/Textures/Fluids/writing.rsi/writing-3.png differ
diff --git a/Resources/Textures/Fluids/writing.rsi/writing-4.png b/Resources/Textures/Fluids/writing.rsi/writing-4.png
new file mode 100644
index 0000000000..ad7e11ec13
Binary files /dev/null and b/Resources/Textures/Fluids/writing.rsi/writing-4.png differ