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