* 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.
211 lines
7.2 KiB
C#
211 lines
7.2 KiB
C#
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.GameObjects.Components.Nutrition;
|
|
using Content.Shared.Interfaces;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.Interfaces.GameObjects;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Localization;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.ViewVariables;
|
|
|
|
namespace Content.Server.GameObjects.Components.Nutrition
|
|
{
|
|
[RegisterComponent]
|
|
public class DrinkComponent : Component, IAfterAttack, IUse
|
|
{
|
|
#pragma warning disable 649
|
|
[Dependency] private readonly ILocalizationManager _localizationManager;
|
|
#pragma warning restore 649
|
|
public override string Name => "Drink";
|
|
[ViewVariables]
|
|
private SolutionComponent _contents;
|
|
|
|
private AppearanceComponent _appearanceComponent;
|
|
|
|
[ViewVariables]
|
|
private string _useSound;
|
|
[ViewVariables]
|
|
private string _finishPrototype;
|
|
|
|
public int TransferAmount => _transferAmount;
|
|
[ViewVariables]
|
|
private int _transferAmount = 2;
|
|
|
|
public int MaxVolume
|
|
{
|
|
get => _contents.MaxVolume;
|
|
set => _contents.MaxVolume = value;
|
|
}
|
|
|
|
private Solution _initialContents; // This is just for loading from yaml
|
|
|
|
private bool _despawnOnFinish;
|
|
|
|
public int UsesLeft()
|
|
{
|
|
// In case transfer amount exceeds volume left
|
|
if (_contents.CurrentVolume == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
return Math.Max(1, _contents.CurrentVolume / _transferAmount);
|
|
}
|
|
|
|
|
|
public override void ExposeData(ObjectSerializer serializer)
|
|
{
|
|
base.ExposeData(serializer);
|
|
serializer.DataField(ref _initialContents, "contents", null);
|
|
serializer.DataField(ref _useSound, "use_sound", "/Audio/items/drink.ogg");
|
|
// E.g. cola can when done or clear bottle, whatever
|
|
// Currently this will enforce it has the same volume but this may change.
|
|
serializer.DataField(ref _despawnOnFinish, "despawn_empty", true);
|
|
serializer.DataField(ref _finishPrototype, "spawn_on_finish", null);
|
|
}
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
if (_contents == null)
|
|
{
|
|
if (Owner.TryGetComponent(out SolutionComponent solutionComponent))
|
|
{
|
|
_contents = solutionComponent;
|
|
}
|
|
else
|
|
{
|
|
_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.SolutionChanged += HandleSolutionChangedEvent;
|
|
}
|
|
|
|
protected override void Startup()
|
|
{
|
|
base.Startup();
|
|
if (_initialContents != null)
|
|
{
|
|
_contents.TryAddSolution(_initialContents, true, true);
|
|
}
|
|
_initialContents = null;
|
|
Owner.TryGetComponent(out AppearanceComponent appearance);
|
|
_appearanceComponent = appearance;
|
|
_appearanceComponent?.SetData(SharedFoodComponent.FoodVisuals.MaxUses, MaxVolume);
|
|
_updateAppearance();
|
|
}
|
|
|
|
private void _updateAppearance()
|
|
{
|
|
_appearanceComponent?.SetData(SharedFoodComponent.FoodVisuals.Visual, UsesLeft());
|
|
}
|
|
|
|
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
|
{
|
|
UseDrink(eventArgs.User);
|
|
|
|
return true;
|
|
}
|
|
|
|
void IAfterAttack.AfterAttack(AfterAttackEventArgs eventArgs)
|
|
{
|
|
UseDrink(eventArgs.Attacked);
|
|
}
|
|
|
|
private void UseDrink(IEntity user)
|
|
{
|
|
if (user == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (UsesLeft() == 0 && !_despawnOnFinish)
|
|
{
|
|
user.PopupMessage(user, _localizationManager.GetString("Empty"));
|
|
return;
|
|
}
|
|
|
|
if (user.TryGetComponent(out StomachComponent stomachComponent))
|
|
{
|
|
var transferAmount = Math.Min(_transferAmount, _contents.CurrentVolume);
|
|
var split = _contents.SplitSolution(transferAmount);
|
|
if (stomachComponent.TryTransferSolution(split))
|
|
{
|
|
if (_useSound != null)
|
|
{
|
|
Owner.GetComponent<SoundComponent>()?.Play(_useSound);
|
|
user.PopupMessage(user, _localizationManager.GetString("Slurp"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Add it back in
|
|
_contents.TryAddSolution(split);
|
|
user.PopupMessage(user, _localizationManager.GetString("Can't drink"));
|
|
}
|
|
}
|
|
|
|
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.
|
|
if (!_despawnOnFinish || UsesLeft() > 0)
|
|
return;
|
|
|
|
var gridPos = Owner.Transform.GridPosition;
|
|
_contents.SolutionChanged -= HandleSolutionChangedEvent;
|
|
Owner.Delete();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
}
|