diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 009a666727..bca658c3d3 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -128,7 +128,8 @@ namespace Content.Client "UtilityBeltClothingFill", "ShuttleController", "HumanInventoryController", - "UseDelay" + "UseDelay", + "Pourable" }; foreach (var ignoreName in registerIgnore) diff --git a/Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs b/Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs new file mode 100644 index 0000000000..f2785ca210 --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/PourableComponent.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Content.Server.GameObjects.Components.Nutrition; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Chemistry +{ + /// + /// Gives an entity click behavior for pouring reagents into + /// other entities and being poured into. The entity must have + /// a SolutionComponent or DrinkComponent for this to work. + /// (DrinkComponent adds a SolutionComponent if one isn't present). + /// + [RegisterComponent] + class PourableComponent : Component, IAttackBy + { +#pragma warning disable 649 + [Dependency] private readonly IServerNotifyManager _notifyManager; + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + + public override string Name => "Pourable"; + + private int _transferAmount; + + /// + /// The amount of solution to be transferred from this solution when clicking on other solutions with it. + /// + [ViewVariables] + public int TransferAmount + { + get => _transferAmount; + set => _transferAmount = value; + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(ref _transferAmount, "transferAmount", 5); + } + + /// + /// Called when the owner of this component is clicked on with another entity. + /// The owner of this component is the target. + /// The entity used to click on this one is the attacker. + /// + /// Attack event args + /// + bool IAttackBy.AttackBy(AttackByEventArgs eventArgs) + { + //Get target and check if it can be poured into + if (!Owner.TryGetComponent(out var targetSolution)) + return false; + if (!targetSolution.CanPourIn) + return false; + + //Get attack entity and check if it can pour out. + var attackEntity = eventArgs.AttackWith; + if (!attackEntity.TryGetComponent(out var attackSolution) || !attackSolution.CanPourOut) + return false; + if (!attackEntity.TryGetComponent(out var attackPourable)) + return false; + + //Get transfer amount. May be smaller than _transferAmount if not enough room + int realTransferAmount = Math.Min(attackPourable.TransferAmount, targetSolution.EmptyVolume); + if (realTransferAmount <= 0) //Special message if container is full + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, eventArgs.User, + _localizationManager.GetString("Container is full")); + return false; + } + //Remove transfer amount from attacker + if (!attackSolution.TryRemoveSolution(realTransferAmount, out var removedSolution)) + return false; + + //Add poured solution to this solution + if (!targetSolution.TryAddSolution(removedSolution)) + return false; + + _notifyManager.PopupMessage(Owner.Transform.GridPosition, eventArgs.User, + _localizationManager.GetString("Transferred {0}u", removedSolution.TotalVolume)); + + return true; + } + } +} diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs index ffd687b476..20d187a227 100644 --- a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.ComponentModel.Design; using Content.Server.Chemistry; +using Content.Server.GameObjects.Components.Nutrition; using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces; using Content.Shared.Chemistry; using Content.Shared.GameObjects; using Robust.Server.GameObjects.EntitySystems; diff --git a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs index f3209b7b2a..8b7c9728c6 100644 --- a/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/DrinkComponent.cs @@ -80,10 +80,18 @@ namespace Content.Server.GameObjects.Components.Nutrition else { _contents = Owner.AddComponent(); + //Ensure SolutionComponent supports click transferring if custom one not set + _contents.Capabilities = SolutionCaps.PourIn + | SolutionCaps.PourOut + | SolutionCaps.Injectable; + + var pourable = Owner.AddComponent(); + pourable.TransferAmount = 5; } } _contents.MaxVolume = _initialContents.TotalVolume; + _contents.SolutionChanged += HandleSolutionChangedEvent; } protected override void Startup() @@ -117,7 +125,7 @@ namespace Content.Server.GameObjects.Components.Nutrition UseDrink(eventArgs.Attacked); } - void UseDrink(IEntity user) + private void UseDrink(IEntity user) { if (user == null) { @@ -150,34 +158,53 @@ namespace Content.Server.GameObjects.Components.Nutrition } } + Finish(user); + } + + /// + /// Trigger finish behavior in the drink if applicable. + /// Depending on the drink this will either delete it, + /// or convert it to another entity, like an empty variant. + /// + /// The entity that is using the drink + public void Finish(IEntity user) + { // Drink containers are mostly transient. if (!_despawnOnFinish || UsesLeft() > 0) - { return; - } - + var gridPos = Owner.Transform.GridPosition; + _contents.SolutionChanged -= HandleSolutionChangedEvent; Owner.Delete(); - if (_finishPrototype != null) - { - var finisher = Owner.EntityManager.SpawnEntity(_finishPrototype, Owner.Transform.GridPosition); - if (user.TryGetComponent(out HandsComponent handsComponent) && finisher.TryGetComponent(out ItemComponent itemComponent)) - { - if (handsComponent.CanPutInHand(itemComponent)) - { - handsComponent.PutInHand(itemComponent); - return; - } - } - - finisher.Transform.GridPosition = user.Transform.GridPosition; - if (finisher.TryGetComponent(out DrinkComponent drinkComponent)) - { - drinkComponent.MaxVolume = MaxVolume; - } + if (_finishPrototype == null || user == null) return; + + var finisher = Owner.EntityManager.SpawnEntity(_finishPrototype, Owner.Transform.GridPosition); + if (user.TryGetComponent(out HandsComponent handsComponent) && finisher.TryGetComponent(out ItemComponent itemComponent)) + { + if (handsComponent.CanPutInHand(itemComponent)) + { + handsComponent.PutInHand(itemComponent); + return; + } } + + finisher.Transform.GridPosition = gridPos; + if (finisher.TryGetComponent(out DrinkComponent drinkComponent)) + { + drinkComponent.MaxVolume = MaxVolume; + } + } + + /// + /// Updates drink state when the solution is changed by something other + /// than this component. Without this some drinks won't properly delete + /// themselves without additional clicks/uses after them being emptied. + /// + private void HandleSolutionChangedEvent() + { + Finish(null); } } } diff --git a/Content.Shared/Chemistry/Solution.cs b/Content.Shared/Chemistry/Solution.cs index 2e996d3f33..2255691a1e 100644 --- a/Content.Shared/Chemistry/Solution.cs +++ b/Content.Shared/Chemistry/Solution.cs @@ -125,15 +125,23 @@ namespace Content.Shared.Chemistry } } - public void RemoveSolution(int quantity) + /// + /// Remove the specified quantity from this solution. + /// + /// The quantity of this solution to remove + /// Out arg. The removed solution. Useful for adding removed solution + /// into other solutions. For example, when pouring from one container to another. + public void RemoveSolution(int quantity, out Solution removedSolution) { - if(quantity <=0) + removedSolution = new Solution(); + if(quantity <= 0) return; var ratio = (float)(TotalVolume - quantity) / TotalVolume; if (ratio <= 0) { + removedSolution = this.Clone(); //Todo: Check if clone necessary RemoveAllSolution(); return; } @@ -148,6 +156,7 @@ namespace Content.Shared.Chemistry var newQuantity = (int)Math.Floor(oldQuantity * ratio); _contents[i] = new ReagentQuantity(reagent.ReagentId, newQuantity); + removedSolution.AddReagent(reagent.ReagentId, oldQuantity - newQuantity); } TotalVolume = (int)Math.Floor(TotalVolume * ratio); diff --git a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs index 2624d79cef..498a070332 100644 --- a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs +++ b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs @@ -42,6 +42,12 @@ namespace Content.Shared.GameObjects.Components.Chemistry [ViewVariables] public int CurrentVolume => _containedSolution.TotalVolume; + /// + /// The volume without reagents remaining in the container. + /// + [ViewVariables] + public int EmptyVolume => MaxVolume - CurrentVolume; + /// /// The current blended color of all the reagents in the container. /// @@ -60,6 +66,15 @@ namespace Content.Shared.GameObjects.Components.Chemistry public IReadOnlyList ReagentList => _containedSolution.Contents; + /// + /// Shortcut for Capabilities PourIn flag to avoid binary operators. + /// + public bool CanPourIn => (Capabilities & SolutionCaps.PourIn) != 0; + /// + /// Shortcut for Capabilities PourOut flag to avoid binary operators. + /// + public bool CanPourOut => (Capabilities & SolutionCaps.PourOut) != 0; + /// public override string Name => "Solution"; @@ -108,11 +123,20 @@ namespace Content.Shared.GameObjects.Components.Chemistry return true; } - public bool TryRemoveSolution(int quantity) + /// + /// Attempt to remove the specified quantity from this solution + /// + /// Quantity of this solution to remove + /// Out arg. The removed solution. Useful for adding removed solution + /// into other solutions. For example, when pouring from one container to another. + /// Whether or not the solution was successfully removed + public bool TryRemoveSolution(int quantity, out Solution removedSolution) { - if (CurrentVolume == 0) return false; + removedSolution = new Solution(); + if (CurrentVolume == 0) + return false; - _containedSolution.RemoveSolution(quantity); + _containedSolution.RemoveSolution(quantity, out removedSolution); OnSolutionChanged(); return true; } diff --git a/Content.Tests/Shared/Chemistry/Solution_Tests.cs b/Content.Tests/Shared/Chemistry/Solution_Tests.cs index d78dc39a2f..e2c82b7b4d 100644 --- a/Content.Tests/Shared/Chemistry/Solution_Tests.cs +++ b/Content.Tests/Shared/Chemistry/Solution_Tests.cs @@ -141,10 +141,14 @@ namespace Content.Tests.Shared.Chemistry { var solution = new Solution("water", 700); - solution.RemoveSolution(500); + solution.RemoveSolution(500, out var removedSolution); + //Check that edited solution is correct Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(200)); Assert.That(solution.TotalVolume, Is.EqualTo(200)); + //Check that removed solution is correct + Assert.That(removedSolution.GetReagentQuantity("water"), Is.EqualTo(500)); + Assert.That(removedSolution.TotalVolume, Is.EqualTo(500)); } [Test] @@ -152,10 +156,14 @@ namespace Content.Tests.Shared.Chemistry { var solution = new Solution("water", 800); - solution.RemoveSolution(1000); + solution.RemoveSolution(1000, out var removedSolution); + //Check that edited solution is correct Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(0)); Assert.That(solution.TotalVolume, Is.EqualTo(0)); + //Check that removed solution is correct + Assert.That(removedSolution.GetReagentQuantity("water"), Is.EqualTo(800)); + Assert.That(removedSolution.TotalVolume, Is.EqualTo(800)); } [Test] @@ -165,11 +173,15 @@ namespace Content.Tests.Shared.Chemistry solution.AddReagent("water", 1000); solution.AddReagent("fire", 2000); - solution.RemoveSolution(1500); + solution.RemoveSolution(1500, out var removedSolution); Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(500)); Assert.That(solution.GetReagentQuantity("fire"), Is.EqualTo(1000)); Assert.That(solution.TotalVolume, Is.EqualTo(1500)); + + Assert.That(removedSolution.GetReagentQuantity("water"), Is.EqualTo(500)); + Assert.That(removedSolution.GetReagentQuantity("fire"), Is.EqualTo(1000)); + Assert.That(removedSolution.TotalVolume, Is.EqualTo(1500)); } [Test] @@ -177,10 +189,13 @@ namespace Content.Tests.Shared.Chemistry { var solution = new Solution("water", 800); - solution.RemoveSolution(-200); + solution.RemoveSolution(-200, out var removedSolution); Assert.That(solution.GetReagentQuantity("water"), Is.EqualTo(800)); Assert.That(solution.TotalVolume, Is.EqualTo(800)); + + Assert.That(removedSolution.GetReagentQuantity("water"), Is.EqualTo(0)); + Assert.That(removedSolution.TotalVolume, Is.EqualTo(0)); } [Test] diff --git a/Resources/Prototypes/Entities/items/chemistry.yml b/Resources/Prototypes/Entities/items/chemistry.yml index bb2a883e50..ced1e17639 100644 --- a/Resources/Prototypes/Entities/items/chemistry.yml +++ b/Resources/Prototypes/Entities/items/chemistry.yml @@ -11,6 +11,8 @@ - type: Solution maxVol: 50 caps: 19 + - type: Pourable + transferAmount: 5 - type: entity name: Large Beaker @@ -25,6 +27,8 @@ - type: Solution maxVol: 100 caps: 19 + - type: Pourable + transferAmount: 5 - type: entity name: Dropper @@ -39,3 +43,5 @@ - type: Solution maxVol: 5 caps: 19 + - type: Pourable + transferAmount: 5