diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 2b8984fc11..01be9dbffd 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -20,6 +20,7 @@ using Content.Client.Interfaces.GameObjects; using Content.Client.Interfaces.Parallax; using Content.Client.Parallax; using Content.Client.UserInterface; +using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.Components.Markers; using Content.Shared.GameObjects.Components.Materials; using Content.Shared.GameObjects.Components.Mobs; @@ -111,6 +112,7 @@ namespace Content.Client factory.Register(); factory.Register(); factory.Register(); + factory.Register(); factory.Register(); factory.Register(); factory.RegisterReference(); diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index 612d1633a4..df458d5127 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -39,6 +39,7 @@ using Content.Server.GameObjects.Components.Weapon.Ranged; using Content.Server.GameTicking; using Content.Server.Interfaces; using Content.Server.Interfaces.GameTicking; +using Content.Shared.GameObjects.Components.Chemistry; using Content.Shared.GameObjects.Components.Materials; using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.GameObjects.Components.Markers; @@ -59,6 +60,7 @@ using Content.Server.GameObjects.Components.Explosive; using Content.Server.GameObjects.Components.Items; using Content.Server.GameObjects.Components.Triggers; using Content.Shared.GameObjects.Components.Movement; +using SolutionComponent = Content.Server.GameObjects.Components.Chemistry.SolutionComponent; namespace Content.Server { @@ -94,6 +96,8 @@ namespace Content.Server factory.Register(); factory.RegisterReference(); + factory.Register(); + //Power Components factory.Register(); factory.Register(); diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs new file mode 100644 index 0000000000..a052eea348 --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs @@ -0,0 +1,140 @@ +using System; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Server.GameObjects.Components.Chemistry +{ + /// + /// Shared ECS component that manages a liquid solution of reagents. + /// + internal class SolutionComponent : Shared.GameObjects.Components.Chemistry.SolutionComponent + { + /// + /// Transfers solution from the held container to the target container. + /// + [Verb] + private sealed class FillTargetVerb : Verb + { + protected override string GetText(IEntity user, SolutionComponent component) + { + if(!user.TryGetComponent(out var hands)) + return ""; + + if(hands.GetActiveHand == null) + return ""; + + var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? ""; + var myName = component.Owner.Prototype?.Name ?? ""; + + return $"Transfer liquid from [{heldEntityName}] to [{myName}]."; + } + + protected override VerbVisibility GetVisibility(IEntity user, SolutionComponent component) + { + if (user.TryGetComponent(out var hands)) + { + if (hands.GetActiveHand != null) + { + if (hands.GetActiveHand.Owner.TryGetComponent(out var solution)) + { + if ((solution.Capabilities & SolutionCaps.PourOut) != 0 && (component.Capabilities & SolutionCaps.PourIn) != 0) + return VerbVisibility.Visible; + } + } + } + + return VerbVisibility.Invisible; + } + + protected override void Activate(IEntity user, SolutionComponent component) + { + if (!user.TryGetComponent(out var hands)) + return; + + if (hands.GetActiveHand == null) + return; + + if (!hands.GetActiveHand.Owner.TryGetComponent(out var handSolutionComp)) + return; + + if ((handSolutionComp.Capabilities & SolutionCaps.PourOut) == 0 || (component.Capabilities & SolutionCaps.PourIn) == 0) + return; + + var transferQuantity = Math.Min(component.MaxVolume - component.CurrentVolume, handSolutionComp.CurrentVolume); + transferQuantity = Math.Min(transferQuantity, 10); + + // nothing to transfer + if (transferQuantity <= 0) + return; + + var transferSolution = handSolutionComp.SplitSolution(transferQuantity); + component.TryAddSolution(transferSolution); + + } + } + + /// + /// Transfers solution from a target container to the held container. + /// + [Verb] + private sealed class EmptyTargetVerb : Verb + { + protected override string GetText(IEntity user, SolutionComponent component) + { + if (!user.TryGetComponent(out var hands)) + return ""; + + if (hands.GetActiveHand == null) + return ""; + + var heldEntityName = hands.GetActiveHand.Owner?.Prototype?.Name ?? ""; + var myName = component.Owner.Prototype?.Name ?? ""; + + return $"Transfer liquid from [{myName}] to [{heldEntityName}]."; + } + + protected override VerbVisibility GetVisibility(IEntity user, SolutionComponent component) + { + if (user.TryGetComponent(out var hands)) + { + if (hands.GetActiveHand != null) + { + if (hands.GetActiveHand.Owner.TryGetComponent(out var solution)) + { + if ((solution.Capabilities & SolutionCaps.PourIn) != 0 && (component.Capabilities & SolutionCaps.PourOut) != 0) + return VerbVisibility.Visible; + } + } + } + + return VerbVisibility.Invisible; + } + + protected override void Activate(IEntity user, SolutionComponent component) + { + if (!user.TryGetComponent(out var hands)) + return; + + if (hands.GetActiveHand == null) + return; + + if(!hands.GetActiveHand.Owner.TryGetComponent(out var handSolutionComp)) + return; + + if ((handSolutionComp.Capabilities & SolutionCaps.PourIn) == 0 || (component.Capabilities & SolutionCaps.PourOut) == 0) + return; + + var transferQuantity = Math.Min(handSolutionComp.MaxVolume - handSolutionComp.CurrentVolume, component.CurrentVolume); + transferQuantity = Math.Min(transferQuantity, 10); + + // pulling from an empty container, pointless to continue + if (transferQuantity <= 0) + return; + + var transferSolution = component.SplitSolution(transferQuantity); + handSolutionComp.TryAddSolution(transferSolution); + } + } + } +} diff --git a/Content.Shared/Chemistry/ReagentPrototype.cs b/Content.Shared/Chemistry/ReagentPrototype.cs new file mode 100644 index 0000000000..b123489a2b --- /dev/null +++ b/Content.Shared/Chemistry/ReagentPrototype.cs @@ -0,0 +1,25 @@ +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +namespace Content.Shared.Chemistry +{ + [Prototype("reagent")] + public class ReagentPrototype : IPrototype, IIndexedPrototype + { + public string ID { get; private set; } + public string Name { get; private set; } + public string Description { get; private set; } + public Color SubstanceColor { get; private set; } + + public void LoadFrom(YamlMappingNode mapping) + { + ID = mapping.GetNode("id").AsString(); + Name = mapping.GetNode("name").ToString(); + Description = mapping.GetNode("desc").ToString(); + + SubstanceColor = mapping.TryGetNode("color", out var colorNode) ? colorNode.AsHexColor(Color.White) : Color.White; + } + } +} diff --git a/Content.Shared/Chemistry/Solution.cs b/Content.Shared/Chemistry/Solution.cs new file mode 100644 index 0000000000..ef305605fe --- /dev/null +++ b/Content.Shared/Chemistry/Solution.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.Chemistry +{ + /// + /// A solution of reagents. + /// + public class Solution : IExposeData, IEnumerable + { + // Most objects on the station hold only 1 or 2 reagents + [ViewVariables] + private List _contents = new List(2); + + /// + /// The calculated total volume of all reagents in the solution (ex. Total volume of liquid in beaker). + /// + [ViewVariables] + public int TotalVolume { get; private set; } + + /// + /// Constructs an empty solution (ex. an empty beaker). + /// + public Solution() { } + + /// + /// Constructs a solution containing 100% of a reagent (ex. A beaker of pure water). + /// + /// The prototype ID of the reagent to add. + /// The quantity in milli-units. + public Solution(string reagentId, int quantity) + { + AddReagent(reagentId, quantity); + } + + /// + public void ExposeData(ObjectSerializer serializer) + { + serializer.DataField(ref _contents, "reagents", new List()); + + if (serializer.Reading) + { + TotalVolume = 0; + foreach (var reagent in _contents) + { + TotalVolume += reagent.Quantity; + } + } + } + + /// + /// Adds a given quantity of a reagent directly into the solution. + /// + /// The prototype ID of the reagent to add. + /// The quantity in milli-units. + public void AddReagent(string reagentId, int quantity) + { + if(quantity <= 0) + return; + + for (var i = 0; i < _contents.Count; i++) + { + var reagent = _contents[i]; + if (reagent.ReagentId != reagentId) + continue; + + _contents[i] = new ReagentQuantity(reagentId, reagent.Quantity + quantity); + TotalVolume += quantity; + return; + } + + _contents.Add(new ReagentQuantity(reagentId, quantity)); + TotalVolume += quantity; + } + + /// + /// Returns the amount of a single reagent inside the solution. + /// + /// The prototype ID of the reagent to add. + /// The quantity in milli-units. + public int GetReagentQuantity(string reagentId) + { + for (var i = 0; i < _contents.Count; i++) + { + if (_contents[i].ReagentId == reagentId) + return _contents[i].Quantity; + } + + return 0; + } + + public void RemoveReagent(string reagentId, int quantity) + { + if(quantity <= 0) + return; + + for (var i = 0; i < _contents.Count; i++) + { + var reagent = _contents[i]; + if(reagent.ReagentId != reagentId) + continue; + + var curQuantity = reagent.Quantity; + + var newQuantity = curQuantity - quantity; + if (newQuantity <= 0) + { + _contents.RemoveSwap(i); + TotalVolume -= curQuantity; + } + else + { + _contents[i] = new ReagentQuantity(reagentId, newQuantity); + TotalVolume -= quantity; + } + + return; + } + } + + public void RemoveSolution(int quantity) + { + if(quantity <=0) + return; + + var ratio = (float)(TotalVolume - quantity) / TotalVolume; + + if (ratio <= 0) + { + RemoveAllSolution(); + return; + } + + for (var i = 0; i < _contents.Count; i++) + { + var reagent = _contents[i]; + var oldQuantity = reagent.Quantity; + + // quantity taken is always a little greedy, so fractional quantities get rounded up to the nearest + // whole unit. This should prevent little bits of chemical remaining because of float rounding errors. + var newQuantity = (int)Math.Floor(oldQuantity * ratio); + + _contents[i] = new ReagentQuantity(reagent.ReagentId, newQuantity); + } + + TotalVolume = (int)Math.Floor(TotalVolume * ratio); + } + + public void RemoveAllSolution() + { + _contents.Clear(); + TotalVolume = 0; + } + + public Solution SplitSolution(int quantity) + { + if (quantity <= 0) + return new Solution(); + + Solution newSolution; + + if (quantity >= TotalVolume) + { + newSolution = Clone(); + RemoveAllSolution(); + return newSolution; + } + + newSolution = new Solution(); + var newTotalVolume = 0; + var ratio = (float)(TotalVolume - quantity) / TotalVolume; + + for (var i = 0; i < _contents.Count; i++) + { + var reagent = _contents[i]; + + var newQuantity = (int)Math.Floor(reagent.Quantity * ratio); + var splitQuantity = reagent.Quantity - newQuantity; + + _contents[i] = new ReagentQuantity(reagent.ReagentId, newQuantity); + newSolution._contents.Add(new ReagentQuantity(reagent.ReagentId, splitQuantity)); + newTotalVolume += splitQuantity; + } + + TotalVolume = (int)Math.Floor(TotalVolume * ratio); + newSolution.TotalVolume = newTotalVolume; + + return newSolution; + } + + public void AddSolution(Solution otherSolution) + { + for (var i = 0; i < otherSolution._contents.Count; i++) + { + var otherReagent = otherSolution._contents[i]; + + var found = false; + for (var j = 0; j < _contents.Count; j++) + { + var reagent = _contents[j]; + if (reagent.ReagentId == otherReagent.ReagentId) + { + found = true; + _contents[j] = new ReagentQuantity(reagent.ReagentId, reagent.Quantity + otherReagent.Quantity); + break; + } + } + + if (!found) + { + _contents.Add(new ReagentQuantity(otherReagent.ReagentId, otherReagent.Quantity)); + } + } + + TotalVolume += otherSolution.TotalVolume; + } + + public Solution Clone() + { + var volume = 0; + var newSolution = new Solution(); + + for (var i = 0; i < _contents.Count; i++) + { + var reagent = _contents[i]; + newSolution._contents.Add(reagent); + volume += reagent.Quantity; + } + + newSolution.TotalVolume = volume; + return newSolution; + } + + [Serializable, NetSerializable] + public readonly struct ReagentQuantity + { + public readonly string ReagentId; + public readonly int Quantity; + + public ReagentQuantity(string reagentId, int quantity) + { + ReagentId = reagentId; + Quantity = quantity; + } + + [ExcludeFromCodeCoverage] + public override string ToString() + { + return $"{ReagentId}:{Quantity}"; + } + } + + #region Enumeration + + public IEnumerator GetEnumerator() + { + return _contents.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/Content.Shared/Chemistry/SolutionCaps.cs b/Content.Shared/Chemistry/SolutionCaps.cs new file mode 100644 index 0000000000..c5e4edf193 --- /dev/null +++ b/Content.Shared/Chemistry/SolutionCaps.cs @@ -0,0 +1,21 @@ +using System; +using Robust.Shared.Serialization; + +namespace Content.Shared.Chemistry +{ + /// + /// These are the defined capabilities of a container of a solution. + /// + [Flags] + [Serializable, NetSerializable] + public enum SolutionCaps + { + None = 0, + + PourIn = 1, + PourOut = 2, + + Injector = 4, + Injectable = 8, + } +} diff --git a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs new file mode 100644 index 0000000000..5ec42b038f --- /dev/null +++ b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs @@ -0,0 +1,166 @@ +using System; +using Content.Shared.Chemistry; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Shared.GameObjects.Components.Chemistry +{ + public class SolutionComponent : Component + { +#pragma warning disable 649 + [Dependency] private readonly IPrototypeManager _prototypeManager; +#pragma warning restore 649 + + [ViewVariables] + private Solution _containedSolution; + private int _maxVolume; + private SolutionCaps _capabilities; + + /// + /// The maximum volume of the container. + /// + [ViewVariables(VVAccess.ReadWrite)] + public int MaxVolume + { + get => _maxVolume; + set => _maxVolume = value; // Note that the contents won't spill out if the capacity is reduced. + } + + /// + /// The total volume of all the of the reagents in the container. + /// + [ViewVariables] + public int CurrentVolume => _containedSolution.TotalVolume; + + /// + /// The current blended color of all the reagents in the container. + /// + [ViewVariables(VVAccess.ReadWrite)] + public Color SubstanceColor { get; private set; } + + /// + /// The current capabilities of this container (is the top open to pour? can I inject it into another object?). + /// + [ViewVariables(VVAccess.ReadWrite)] + public SolutionCaps Capabilities + { + get => _capabilities; + set => _capabilities = value; + } + + /// + public override string Name => "Solution"; + + /// + public sealed override uint? NetID => ContentNetIDs.SOLUTION; + + /// + public sealed override Type StateType => typeof(SolutionComponentState); + + /// + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _maxVolume, "maxVol", 0); + serializer.DataField(ref _containedSolution, "contents", new Solution()); + serializer.DataField(ref _capabilities, "caps", SolutionCaps.None); + } + + public override void Startup() + { + base.Startup(); + + RecalculateColor(); + } + + public override void Shutdown() + { + base.Shutdown(); + + _containedSolution.RemoveAllSolution(); + _containedSolution = new Solution(); + } + + public bool TryAddReagent(string reagentId, int quantity, out int acceptedQuantity) + { + throw new NotImplementedException(); + } + + public bool TryAddSolution(Solution solution) + { + if (solution.TotalVolume > (_maxVolume - _containedSolution.TotalVolume)) + return false; + + _containedSolution.AddSolution(solution); + RecalculateColor(); + return true; + } + + public Solution SplitSolution(int quantity) + { + return _containedSolution.SplitSolution(quantity); + } + + private void RecalculateColor() + { + if(_containedSolution.TotalVolume == 0) + SubstanceColor = Color.White; + + Color mixColor = default; + float runningTotalQuantity = 0; + + foreach (var reagent in _containedSolution) + { + runningTotalQuantity += reagent.Quantity; + + if(!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto)) + continue; + + if (mixColor == default) + mixColor = proto.SubstanceColor; + + mixColor = BlendRGB(mixColor, proto.SubstanceColor, reagent.Quantity / runningTotalQuantity); + } + } + + private Color BlendRGB(Color rgb1, Color rgb2, float amount) + { + var r = (float)Math.Round(rgb1.R + (rgb2.R - rgb1.R) * amount, 1); + var g = (float)Math.Round(rgb1.G + (rgb2.G - rgb1.G) * amount, 1); + var b = (float)Math.Round(rgb1.B + (rgb2.B - rgb1.B) * amount, 1); + var alpha = (float)Math.Round(rgb1.A + (rgb2.A - rgb1.A) * amount, 1); + + return new Color(r, g, b, alpha); + } + + /// + public override ComponentState GetComponentState() + { + return new SolutionComponentState(); + } + + /// + public override void HandleComponentState(ComponentState curState, ComponentState nextState) + { + base.HandleComponentState(curState, nextState); + + if(curState == null) + return; + + var compState = (SolutionComponentState)curState; + + //TODO: Make me work! + } + + [Serializable, NetSerializable] + public class SolutionComponentState : ComponentState + { + public SolutionComponentState() : base(ContentNetIDs.SOLUTION) { } + } + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index a2b54f0dff..166a0cdb0e 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -7,6 +7,7 @@ public const uint DESTRUCTIBLE = 1001; public const uint TEMPERATURE = 1002; public const uint HANDS = 1003; + public const uint SOLUTION = 1004; public const uint STORAGE = 1005; public const uint INVENTORY = 1006; public const uint POWER_DEBUG_TOOL = 1007; diff --git a/Content.Shared/GameObjects/Verb.cs b/Content.Shared/GameObjects/Verb.cs index 7bc38b4dc3..4fd3fe288b 100644 --- a/Content.Shared/GameObjects/Verb.cs +++ b/Content.Shared/GameObjects/Verb.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using JetBrains.Annotations; using Robust.Shared.Interfaces.GameObjects; @@ -116,7 +117,7 @@ namespace Content.Shared.GameObjects foreach (var component in entity.GetComponentInstances()) { var type = component.GetType(); - foreach (var nestedType in type.GetNestedTypes()) + foreach (var nestedType in type.GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)) { if (!typeof(Verb).IsAssignableFrom(nestedType) || nestedType.IsAbstract) { diff --git a/Content.Tests/Shared/Chemistry/ReagentPrototype_Tests.cs b/Content.Tests/Shared/Chemistry/ReagentPrototype_Tests.cs new file mode 100644 index 0000000000..d78a67178c --- /dev/null +++ b/Content.Tests/Shared/Chemistry/ReagentPrototype_Tests.cs @@ -0,0 +1,42 @@ +using System.IO; +using Content.Shared.Chemistry; +using NUnit.Framework; +using Robust.Shared.Maths; +using Robust.Shared.Utility; +using YamlDotNet.RepresentationModel; + +namespace Content.Tests.Shared.Chemistry +{ + [TestFixture, Parallelizable, TestOf(typeof(ReagentPrototype))] + public class ReagentPrototype_Tests + { + [Test] + public void DeserializeReagentPrototype() + { + using (TextReader stream = new StringReader(YamlReagentPrototype)) + { + var yamlStream = new YamlStream(); + yamlStream.Load(stream); + var document = yamlStream.Documents[0]; + var rootNode = (YamlSequenceNode)document.RootNode; + var proto = (YamlMappingNode)rootNode[0]; + + var defType = proto.GetNode("type").AsString(); + var newReagent = new ReagentPrototype(); + newReagent.LoadFrom(proto); + + Assert.That(defType, Is.EqualTo("reagent")); + Assert.That(newReagent.ID, Is.EqualTo("chem.H2")); + Assert.That(newReagent.Name, Is.EqualTo("Hydrogen")); + Assert.That(newReagent.Description, Is.EqualTo("A light, flammable gas.")); + Assert.That(newReagent.SubstanceColor, Is.EqualTo(Color.Teal)); + } + } + + private const string YamlReagentPrototype = @"- type: reagent + id: chem.H2 + name: Hydrogen + desc: A light, flammable gas. + color: " + "\"#008080\""; + } +} diff --git a/Content.Tests/Shared/Chemistry/Solution_Tests.cs b/Content.Tests/Shared/Chemistry/Solution_Tests.cs new file mode 100644 index 0000000000..d78dc39a2f --- /dev/null +++ b/Content.Tests/Shared/Chemistry/Solution_Tests.cs @@ -0,0 +1,251 @@ +using Content.Shared.Chemistry; +using NUnit.Framework; + +namespace Content.Tests.Shared.Chemistry +{ + [TestFixture, Parallelizable, TestOf(typeof(Solution))] + public class Solution_Tests + { + [Test] + public void AddReagentAndGetSolution() + { + var solution = new Solution(); + solution.AddReagent("water", 1000); + var quantity = solution.GetReagentQuantity("water"); + + Assert.That(quantity, Is.EqualTo(1000)); + } + + [Test] + public void ConstructorAddReagent() + { + var solution = new Solution("water", 1000); + var quantity = solution.GetReagentQuantity("water"); + + Assert.That(quantity, Is.EqualTo(1000)); + } + + [Test] + public void NonExistingReagentReturnsZero() + { + var solution = new Solution(); + var quantity = solution.GetReagentQuantity("water"); + + Assert.That(quantity, Is.EqualTo(0)); + } + + [Test] + public void AddLessThanZeroReagentReturnsZero() + { + var solution = new Solution("water", -1000); + var quantity = solution.GetReagentQuantity("water"); + + Assert.That(quantity, Is.EqualTo(0)); + } + + [Test] + public void AddingReagentsSumsProperly() + { + var solution = new Solution(); + solution.AddReagent("water", 1000); + solution.AddReagent("water", 2000); + var quantity = solution.GetReagentQuantity("water"); + + Assert.That(quantity, Is.EqualTo(3000)); + } + + [Test] + public void ReagentQuantitiesStayUnique() + { + var solution = new Solution(); + solution.AddReagent("water", 1000); + solution.AddReagent("fire", 2000); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(1000)); + Assert.That(solution.GetReagentQuantity("fire"), Is.EqualTo(2000)); + } + + [Test] + public void TotalVolumeIsCorrect() + { + var solution = new Solution(); + solution.AddReagent("water", 1000); + solution.AddReagent("fire", 2000); + + Assert.That(solution.TotalVolume, Is.EqualTo(3000)); + } + + [Test] + public void CloningSolutionIsCorrect() + { + var solution = new Solution(); + solution.AddReagent("water", 1000); + solution.AddReagent("fire", 2000); + + var newSolution = solution.Clone(); + + Assert.That(newSolution.GetReagentQuantity("water"), Is.EqualTo(1000)); + Assert.That(newSolution.GetReagentQuantity("fire"), Is.EqualTo(2000)); + Assert.That(newSolution.TotalVolume, Is.EqualTo(3000)); + } + + [Test] + public void RemoveSolutionRecalculatesProperly() + { + var solution = new Solution(); + solution.AddReagent("water", 1000); + solution.AddReagent("fire", 2000); + + solution.RemoveReagent("water", 500); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(500)); + Assert.That(solution.GetReagentQuantity("fire"), Is.EqualTo(2000)); + Assert.That(solution.TotalVolume, Is.EqualTo(2500)); + } + + [Test] + public void RemoveLessThanOneQuantityDoesNothing() + { + var solution = new Solution("water", 100); + + solution.RemoveReagent("water", -100); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(100)); + Assert.That(solution.TotalVolume, Is.EqualTo(100)); + } + + [Test] + public void RemoveMoreThanTotalRemovesAllReagent() + { + var solution = new Solution("water", 100); + + solution.RemoveReagent("water", 1000); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(0)); + Assert.That(solution.TotalVolume, Is.EqualTo(0)); + } + + [Test] + public void RemoveNonExistReagentDoesNothing() + { + var solution = new Solution("water", 100); + + solution.RemoveReagent("fire", 1000); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(100)); + Assert.That(solution.TotalVolume, Is.EqualTo(100)); + } + + [Test] + public void RemoveSolution() + { + var solution = new Solution("water", 700); + + solution.RemoveSolution(500); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(200)); + Assert.That(solution.TotalVolume, Is.EqualTo(200)); + } + + [Test] + public void RemoveSolutionMoreThanTotalRemovesAll() + { + var solution = new Solution("water", 800); + + solution.RemoveSolution(1000); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(0)); + Assert.That(solution.TotalVolume, Is.EqualTo(0)); + } + + [Test] + public void RemoveSolutionRatioPreserved() + { + var solution = new Solution(); + solution.AddReagent("water", 1000); + solution.AddReagent("fire", 2000); + + solution.RemoveSolution(1500); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(500)); + Assert.That(solution.GetReagentQuantity("fire"), Is.EqualTo(1000)); + Assert.That(solution.TotalVolume, Is.EqualTo(1500)); + } + + [Test] + public void RemoveSolutionLessThanOneDoesNothing() + { + var solution = new Solution("water", 800); + + solution.RemoveSolution(-200); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(800)); + Assert.That(solution.TotalVolume, Is.EqualTo(800)); + } + + [Test] + public void SplitSolution() + { + var solution = new Solution(); + solution.AddReagent("water", 1000); + solution.AddReagent("fire", 2000); + + var splitSolution = solution.SplitSolution(750); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(750)); + Assert.That(solution.GetReagentQuantity("fire"), Is.EqualTo(1500)); + Assert.That(solution.TotalVolume, Is.EqualTo(2250)); + + Assert.That(splitSolution.GetReagentQuantity("water"), Is.EqualTo(250)); + Assert.That(splitSolution.GetReagentQuantity("fire"), Is.EqualTo(500)); + Assert.That(splitSolution.TotalVolume, Is.EqualTo(750)); + } + + [Test] + public void SplitSolutionMoreThanTotalRemovesAll() + { + var solution = new Solution("water", 800); + + var splitSolution = solution.SplitSolution(1000); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(0)); + Assert.That(solution.TotalVolume, Is.EqualTo(0)); + + Assert.That(splitSolution.GetReagentQuantity("water"), Is.EqualTo(800)); + Assert.That(splitSolution.TotalVolume, Is.EqualTo(800)); + } + + [Test] + public void SplitSolutionLessThanOneDoesNothing() + { + var solution = new Solution("water", 800); + + var splitSolution = solution.SplitSolution(-200); + + Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(800)); + Assert.That(solution.TotalVolume, Is.EqualTo(800)); + + Assert.That(splitSolution.GetReagentQuantity("water"), Is.EqualTo(0)); + Assert.That(splitSolution.TotalVolume, Is.EqualTo(0)); + } + + [Test] + public void AddSolution() + { + var solutionOne = new Solution(); + solutionOne.AddReagent("water", 1000); + solutionOne.AddReagent("fire", 2000); + + var solutionTwo = new Solution(); + solutionTwo.AddReagent("water", 500); + solutionTwo.AddReagent("earth", 1000); + + solutionOne.AddSolution(solutionTwo); + + Assert.That(solutionOne.GetReagentQuantity("water"), Is.EqualTo(1500)); + Assert.That(solutionOne.GetReagentQuantity("fire"), Is.EqualTo(2000)); + Assert.That(solutionOne.GetReagentQuantity("earth"), Is.EqualTo(1000)); + Assert.That(solutionOne.TotalVolume, Is.EqualTo(4500)); + } + } +} diff --git a/Resources/Prototypes/Entities/Chemistry.yml b/Resources/Prototypes/Entities/Chemistry.yml new file mode 100644 index 0000000000..bd7048b73f --- /dev/null +++ b/Resources/Prototypes/Entities/Chemistry.yml @@ -0,0 +1,8 @@ +- type: entity + parent: BaseItem + id: ReagentItem + name: "Reagent Item" + abstract: true + components: + - type: Solution + maxVol: 5 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Janitor.yml b/Resources/Prototypes/Entities/Janitor.yml new file mode 100644 index 0000000000..b5f799b554 --- /dev/null +++ b/Resources/Prototypes/Entities/Janitor.yml @@ -0,0 +1,46 @@ +- type: entity + parent: ReagentItem + name: "Extra-Grip™ Mop" + id: MopItem + description: A mop that cant be stopped, viscera cleanup detail awaits. + components: + - type: Sprite + texture: Objects/mop.png + - type: Icon + texture: Objects/mop.png + - type: Item + Size: 10 + - type: Solution + maxVol: 10 + caps: 1 + +- type: entity + parent: ReagentItem + name: Mop Bucket + id: MopBucket + description: Holds water and the tears of the janitor. + components: + - type: Sprite + texture: Objects/mopbucket.png + - type: Icon + texture: Objects/mopbucket.png + - type: Clickable + - type: BoundingBox + - type: Solution + maxVol: 500 + caps: 3 + +- type: entity + parent: ReagentItem + name: Bucket + id: Bucket + description: "It's a bucket." + components: + - type: Sprite + texture: Objects/bucket.png + - type: Icon + texture: Objects/bucket.png + - type: Solution + maxVol: 500 + caps: 3 + \ No newline at end of file diff --git a/Resources/Prototypes/Entities/water_tank.yml b/Resources/Prototypes/Entities/water_tank.yml new file mode 100644 index 0000000000..27b1620c11 --- /dev/null +++ b/Resources/Prototypes/Entities/water_tank.yml @@ -0,0 +1,46 @@ +- type: entity + parent: ReagentItem + id: watertank + name: Water Tank + description: "A water tank. It is used to store high amounts of water." + components: + - type: Sprite + texture: Buildings/watertank.png + + - type: Icon + texture: Buildings/watertank.png + + - type: Clickable + - type: BoundingBox + aabb: "-0.5,-0.25,0.5,0.25" + - type: Collidable + mask: 3 + layer: 1 + IsScrapingFloor: true + - type: Physics + mass: 15 + Anchored: false + + - type: Damageable + - type: Destructible + thresholdvalue: 10 + + - type: Solution + maxVol: 1500 + caps: 3 + + + placement: + snap: + - Wall + +- type: entity + parent: watertank + id: watertank_full + components: + - type: Solution + contents: + reagents: + - ReagentId: chem.H2O + Quantity: 1500 + \ No newline at end of file diff --git a/Resources/Prototypes/Reagents/chemicals.yml b/Resources/Prototypes/Reagents/chemicals.yml new file mode 100644 index 0000000000..f698560651 --- /dev/null +++ b/Resources/Prototypes/Reagents/chemicals.yml @@ -0,0 +1,9 @@ +- type: reagent + id: chem.H2SO4 + name: Sulfuric Acid + desc: A highly corrosive, oily, colorless liquid. + +- type: reagent + id: chem.H2O + name: Water + desc: A tasty colorless liquid. \ No newline at end of file diff --git a/Resources/Prototypes/Reagents/elements.yml b/Resources/Prototypes/Reagents/elements.yml new file mode 100644 index 0000000000..461a9c4d0a --- /dev/null +++ b/Resources/Prototypes/Reagents/elements.yml @@ -0,0 +1,16 @@ +- type: reagent + id: chem.H2 + name: Hydrogen + desc: A light, flammable gas. + +- type: reagent + id: chem.O2 + name: Oxygen + desc: An oxidizing, colorless gas. + +- type: reagent + id: chem.S8 + name: Sulfur + desc: A yellow, crystalline solid. + color: "#FFFACD" + \ No newline at end of file diff --git a/Resources/Textures/Buildings/watertank.png b/Resources/Textures/Buildings/watertank.png new file mode 100644 index 0000000000..56ee09de24 Binary files /dev/null and b/Resources/Textures/Buildings/watertank.png differ diff --git a/Resources/Textures/Objects/bucket.png b/Resources/Textures/Objects/bucket.png new file mode 100644 index 0000000000..e4510dbe16 Binary files /dev/null and b/Resources/Textures/Objects/bucket.png differ diff --git a/Resources/Textures/Objects/bucket_water.png b/Resources/Textures/Objects/bucket_water.png new file mode 100644 index 0000000000..5f97bfaa5e Binary files /dev/null and b/Resources/Textures/Objects/bucket_water.png differ diff --git a/Resources/Textures/Objects/mopbucket.png b/Resources/Textures/Objects/mopbucket.png new file mode 100644 index 0000000000..91e5829d07 Binary files /dev/null and b/Resources/Textures/Objects/mopbucket.png differ diff --git a/Resources/Textures/Objects/mopbucket_water.png b/Resources/Textures/Objects/mopbucket_water.png new file mode 100644 index 0000000000..9fae1353e1 Binary files /dev/null and b/Resources/Textures/Objects/mopbucket_water.png differ diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index d4086e1cd7..4e299bbb28 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -13,7 +13,9 @@ <data /> <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="*.UnitTesting" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> True + <data /> + <data><IncludeFilters /><ExcludeFilters><Filter ModuleMask="Lidgren.Network" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /></ExcludeFilters></data> True True True - True \ No newline at end of file + True