Add solution pouring / click-transfer (#574)
* Add click-based solution transfer For example, clicking on a beaker with a soda can to transfer the soda to the beaker. Works on plain solution containers like beakers and also on open drink containers like soda cans as long as they have the `PourIn` and `PourOut` solution capabilities. If no `SolutionComponent` is added to a drink entity, the `DrinkComponent` will give the entity one. This PR extends that behavior slightly by also giving these default `SolutionComponent`'s the proper capabilities for pouring in/out. * Improve fix for poured drinks not immediately disappearing Instead of making `DrinkComponent.Use` public this splits out the code important to both users and made that function public, leaving `Use` private. * Shorten solution transfer popup * Make code review changes - Move pouring code from SolutionComponent to new PourableComponent. Added PourableComponent to client ignore list and added to existing container prototypes. - Added EmptyVolume property to shared SolutionComponent for convenience. - Removed DrinkComponent fix from pouring AttackBy code. Instead DrinkComponent subscribes to the SolutionChanged action and updates its self when necessary. - Fixed pouring being able to add more than a containers max volume and sometimes deleting reagents. - Added message for when a container is full. * More code review changes - Remove IAttackBy ComponentReference attribute in PourableComponent - Remove _transferAmount from shared SolutionComponent. Left over var from previous commit not being used anymore.
This commit is contained in:
@@ -128,7 +128,8 @@ namespace Content.Client
|
|||||||
"UtilityBeltClothingFill",
|
"UtilityBeltClothingFill",
|
||||||
"ShuttleController",
|
"ShuttleController",
|
||||||
"HumanInventoryController",
|
"HumanInventoryController",
|
||||||
"UseDelay"
|
"UseDelay",
|
||||||
|
"Pourable"
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var ignoreName in registerIgnore)
|
foreach (var ignoreName in registerIgnore)
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The amount of solution to be transferred from this solution when clicking on other solutions with it.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public int TransferAmount
|
||||||
|
{
|
||||||
|
get => _transferAmount;
|
||||||
|
set => _transferAmount = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ExposeData(ObjectSerializer serializer)
|
||||||
|
{
|
||||||
|
base.ExposeData(serializer);
|
||||||
|
serializer.DataField(ref _transferAmount, "transferAmount", 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="eventArgs">Attack event args</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool IAttackBy.AttackBy(AttackByEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
//Get target and check if it can be poured into
|
||||||
|
if (!Owner.TryGetComponent<SolutionComponent>(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<SolutionComponent>(out var attackSolution) || !attackSolution.CanPourOut)
|
||||||
|
return false;
|
||||||
|
if (!attackEntity.TryGetComponent<PourableComponent>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.Design;
|
||||||
using Content.Server.Chemistry;
|
using Content.Server.Chemistry;
|
||||||
|
using Content.Server.GameObjects.Components.Nutrition;
|
||||||
using Content.Server.GameObjects.EntitySystems;
|
using Content.Server.GameObjects.EntitySystems;
|
||||||
|
using Content.Server.Interfaces;
|
||||||
using Content.Shared.Chemistry;
|
using Content.Shared.Chemistry;
|
||||||
using Content.Shared.GameObjects;
|
using Content.Shared.GameObjects;
|
||||||
using Robust.Server.GameObjects.EntitySystems;
|
using Robust.Server.GameObjects.EntitySystems;
|
||||||
|
|||||||
@@ -80,10 +80,18 @@ namespace Content.Server.GameObjects.Components.Nutrition
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_contents = Owner.AddComponent<SolutionComponent>();
|
_contents = Owner.AddComponent<SolutionComponent>();
|
||||||
|
//Ensure SolutionComponent supports click transferring if custom one not set
|
||||||
|
_contents.Capabilities = SolutionCaps.PourIn
|
||||||
|
| SolutionCaps.PourOut
|
||||||
|
| SolutionCaps.Injectable;
|
||||||
|
|
||||||
|
var pourable = Owner.AddComponent<PourableComponent>();
|
||||||
|
pourable.TransferAmount = 5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_contents.MaxVolume = _initialContents.TotalVolume;
|
_contents.MaxVolume = _initialContents.TotalVolume;
|
||||||
|
_contents.SolutionChanged += HandleSolutionChangedEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Startup()
|
protected override void Startup()
|
||||||
@@ -117,7 +125,7 @@ namespace Content.Server.GameObjects.Components.Nutrition
|
|||||||
UseDrink(eventArgs.Attacked);
|
UseDrink(eventArgs.Attacked);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UseDrink(IEntity user)
|
private void UseDrink(IEntity user)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
@@ -150,34 +158,53 @@ namespace Content.Server.GameObjects.Components.Nutrition
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Finish(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The entity that is using the drink</param>
|
||||||
|
public void Finish(IEntity user)
|
||||||
|
{
|
||||||
// Drink containers are mostly transient.
|
// Drink containers are mostly transient.
|
||||||
if (!_despawnOnFinish || UsesLeft() > 0)
|
if (!_despawnOnFinish || UsesLeft() > 0)
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
}
|
var gridPos = Owner.Transform.GridPosition;
|
||||||
|
_contents.SolutionChanged -= HandleSolutionChangedEvent;
|
||||||
Owner.Delete();
|
Owner.Delete();
|
||||||
|
|
||||||
if (_finishPrototype != null)
|
if (_finishPrototype == null || user == 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;
|
|
||||||
}
|
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private void HandleSolutionChangedEvent()
|
||||||
|
{
|
||||||
|
Finish(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,15 +125,23 @@ namespace Content.Shared.Chemistry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveSolution(int quantity)
|
/// <summary>
|
||||||
|
/// Remove the specified quantity from this solution.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="quantity">The quantity of this solution to remove</param>
|
||||||
|
/// <param name="removedSolution">Out arg. The removed solution. Useful for adding removed solution
|
||||||
|
/// into other solutions. For example, when pouring from one container to another.</param>
|
||||||
|
public void RemoveSolution(int quantity, out Solution removedSolution)
|
||||||
{
|
{
|
||||||
if(quantity <=0)
|
removedSolution = new Solution();
|
||||||
|
if(quantity <= 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var ratio = (float)(TotalVolume - quantity) / TotalVolume;
|
var ratio = (float)(TotalVolume - quantity) / TotalVolume;
|
||||||
|
|
||||||
if (ratio <= 0)
|
if (ratio <= 0)
|
||||||
{
|
{
|
||||||
|
removedSolution = this.Clone(); //Todo: Check if clone necessary
|
||||||
RemoveAllSolution();
|
RemoveAllSolution();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -148,6 +156,7 @@ namespace Content.Shared.Chemistry
|
|||||||
var newQuantity = (int)Math.Floor(oldQuantity * ratio);
|
var newQuantity = (int)Math.Floor(oldQuantity * ratio);
|
||||||
|
|
||||||
_contents[i] = new ReagentQuantity(reagent.ReagentId, newQuantity);
|
_contents[i] = new ReagentQuantity(reagent.ReagentId, newQuantity);
|
||||||
|
removedSolution.AddReagent(reagent.ReagentId, oldQuantity - newQuantity);
|
||||||
}
|
}
|
||||||
|
|
||||||
TotalVolume = (int)Math.Floor(TotalVolume * ratio);
|
TotalVolume = (int)Math.Floor(TotalVolume * ratio);
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ namespace Content.Shared.GameObjects.Components.Chemistry
|
|||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
public int CurrentVolume => _containedSolution.TotalVolume;
|
public int CurrentVolume => _containedSolution.TotalVolume;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The volume without reagents remaining in the container.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public int EmptyVolume => MaxVolume - CurrentVolume;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current blended color of all the reagents in the container.
|
/// The current blended color of all the reagents in the container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -60,6 +66,15 @@ namespace Content.Shared.GameObjects.Components.Chemistry
|
|||||||
|
|
||||||
public IReadOnlyList<Solution.ReagentQuantity> ReagentList => _containedSolution.Contents;
|
public IReadOnlyList<Solution.ReagentQuantity> ReagentList => _containedSolution.Contents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shortcut for Capabilities PourIn flag to avoid binary operators.
|
||||||
|
/// </summary>
|
||||||
|
public bool CanPourIn => (Capabilities & SolutionCaps.PourIn) != 0;
|
||||||
|
/// <summary>
|
||||||
|
/// Shortcut for Capabilities PourOut flag to avoid binary operators.
|
||||||
|
/// </summary>
|
||||||
|
public bool CanPourOut => (Capabilities & SolutionCaps.PourOut) != 0;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string Name => "Solution";
|
public override string Name => "Solution";
|
||||||
|
|
||||||
@@ -108,11 +123,20 @@ namespace Content.Shared.GameObjects.Components.Chemistry
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryRemoveSolution(int quantity)
|
/// <summary>
|
||||||
|
/// Attempt to remove the specified quantity from this solution
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="quantity">Quantity of this solution to remove</param>
|
||||||
|
/// <param name="removedSolution">Out arg. The removed solution. Useful for adding removed solution
|
||||||
|
/// into other solutions. For example, when pouring from one container to another.</param>
|
||||||
|
/// <returns>Whether or not the solution was successfully removed</returns>
|
||||||
|
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();
|
OnSolutionChanged();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,10 +141,14 @@ namespace Content.Tests.Shared.Chemistry
|
|||||||
{
|
{
|
||||||
var solution = new Solution("water", 700);
|
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.GetReagentQuantity("water"), Is.EqualTo(200));
|
||||||
Assert.That(solution.TotalVolume, 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]
|
[Test]
|
||||||
@@ -152,10 +156,14 @@ namespace Content.Tests.Shared.Chemistry
|
|||||||
{
|
{
|
||||||
var solution = new Solution("water", 800);
|
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.GetReagentQuantity("water"), Is.EqualTo(0));
|
||||||
Assert.That(solution.TotalVolume, 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]
|
[Test]
|
||||||
@@ -165,11 +173,15 @@ namespace Content.Tests.Shared.Chemistry
|
|||||||
solution.AddReagent("water", 1000);
|
solution.AddReagent("water", 1000);
|
||||||
solution.AddReagent("fire", 2000);
|
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("water"), Is.EqualTo(500));
|
||||||
Assert.That(solution.GetReagentQuantity("fire"), Is.EqualTo(1000));
|
Assert.That(solution.GetReagentQuantity("fire"), Is.EqualTo(1000));
|
||||||
Assert.That(solution.TotalVolume, Is.EqualTo(1500));
|
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]
|
[Test]
|
||||||
@@ -177,10 +189,13 @@ namespace Content.Tests.Shared.Chemistry
|
|||||||
{
|
{
|
||||||
var solution = new Solution("water", 800);
|
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.GetReagentQuantity("water"), Is.EqualTo(800));
|
||||||
Assert.That(solution.TotalVolume, 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]
|
[Test]
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
- type: Solution
|
- type: Solution
|
||||||
maxVol: 50
|
maxVol: 50
|
||||||
caps: 19
|
caps: 19
|
||||||
|
- type: Pourable
|
||||||
|
transferAmount: 5
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
name: Large Beaker
|
name: Large Beaker
|
||||||
@@ -25,6 +27,8 @@
|
|||||||
- type: Solution
|
- type: Solution
|
||||||
maxVol: 100
|
maxVol: 100
|
||||||
caps: 19
|
caps: 19
|
||||||
|
- type: Pourable
|
||||||
|
transferAmount: 5
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
name: Dropper
|
name: Dropper
|
||||||
@@ -39,3 +43,5 @@
|
|||||||
- type: Solution
|
- type: Solution
|
||||||
maxVol: 5
|
maxVol: 5
|
||||||
caps: 19
|
caps: 19
|
||||||
|
- type: Pourable
|
||||||
|
transferAmount: 5
|
||||||
|
|||||||
Reference in New Issue
Block a user