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 Content.Shared.Maths; 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 ReagentUnit TransferAmount => _transferAmount; [ViewVariables] private ReagentUnit _transferAmount = ReagentUnit.New(2); public ReagentUnit MaxVolume { get => _contents.MaxVolume; set => _contents.MaxVolume = value; } private Solution _initialContents; // This is just for loading from yaml private ReagentUnit _maxVolume; private bool _despawnOnFinish; private bool _drinking; public int UsesLeft() { // In case transfer amount exceeds volume left if (_contents.CurrentVolume == 0) { return 0; } return Math.Max(1, (int)Math.Ceiling((_contents.CurrentVolume / _transferAmount).Float())); } public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); serializer.DataField(ref _initialContents, "contents", null); serializer.DataField(ref _maxVolume, "max_volume", ReagentUnit.New(0)); 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(); //Ensure SolutionComponent supports click transferring if custom one not set _contents.Capabilities = SolutionCaps.PourIn | SolutionCaps.PourOut | SolutionCaps.Injectable; var pourable = Owner.AddComponent(); pourable.TransferAmount = ReagentUnit.New(5); } } _drinking = false; if (_maxVolume != 0) _contents.MaxVolume = _maxVolume; else _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)) { _drinking = true; var transferAmount = ReagentUnit.Min(_transferAmount, _contents.CurrentVolume); var split = _contents.SplitSolution(transferAmount); if (stomachComponent.TryTransferSolution(split)) { if (_useSound != null) { Owner.GetComponent()?.Play(_useSound); user.PopupMessage(user, _localizationManager.GetString("Slurp")); } } else { // Add it back in _contents.TryAddSolution(split); user.PopupMessage(user, _localizationManager.GetString("Can't drink")); } _drinking = false; } 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 (_drinking || !_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; } } /// /// 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); } } }