From e65c64af14cab22cc64d429f1dd4d5743677a5d3 Mon Sep 17 00:00:00 2001 From: ike709 Date: Fri, 17 Jul 2020 15:41:19 -0500 Subject: [PATCH] Adds the ChemMaster 4000 (#1398) --- Content.Client/EntryPoint.cs | 1 + .../ChemMasterBoundUserInterface.cs | 91 ++++ .../Chemistry/ChemMaster/ChemMasterWindow.cs | 417 ++++++++++++++++++ .../ReagentDispenserBoundUserInterface.cs | 0 .../ReagentDispenserWindow.cs | 0 .../Chemistry/ChemMasterComponent.cs | 410 +++++++++++++++++ .../Components/Chemistry/PillComponent.cs | 117 +++++ .../Components/Nutrition/FoodComponent.cs | 2 +- .../ChemMaster/SharedChemMasterComponent.cs | 116 +++++ .../ReagentDispenserInventoryPrototype.cs | 0 .../SharedReagentDispenserComponent.cs | 0 .../Entities/Buildings/chem_master.yml | 33 ++ .../Prototypes/Entities/Items/chemistry.yml | 30 ++ 13 files changed, 1216 insertions(+), 1 deletion(-) create mode 100644 Content.Client/GameObjects/Components/Chemistry/ChemMaster/ChemMasterBoundUserInterface.cs create mode 100644 Content.Client/GameObjects/Components/Chemistry/ChemMaster/ChemMasterWindow.cs rename Content.Client/GameObjects/Components/Chemistry/{ => ReagentDispenser}/ReagentDispenserBoundUserInterface.cs (100%) rename Content.Client/GameObjects/Components/Chemistry/{ => ReagentDispenser}/ReagentDispenserWindow.cs (100%) create mode 100644 Content.Server/GameObjects/Components/Chemistry/ChemMasterComponent.cs create mode 100644 Content.Server/GameObjects/Components/Chemistry/PillComponent.cs create mode 100644 Content.Shared/GameObjects/Components/Chemistry/ChemMaster/SharedChemMasterComponent.cs rename Content.Shared/GameObjects/Components/Chemistry/{ => ReagentDispenser}/ReagentDispenserInventoryPrototype.cs (100%) rename Content.Shared/GameObjects/Components/Chemistry/{ => ReagentDispenser}/SharedReagentDispenserComponent.cs (100%) create mode 100644 Resources/Prototypes/Entities/Buildings/chem_master.yml diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 28c04b08a2..af240a8cd5 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -67,6 +67,7 @@ namespace Content.Client factory.Register(); factory.Register(); factory.Register(); + factory.Register(); factory.Register(); factory.Register(); diff --git a/Content.Client/GameObjects/Components/Chemistry/ChemMaster/ChemMasterBoundUserInterface.cs b/Content.Client/GameObjects/Components/Chemistry/ChemMaster/ChemMasterBoundUserInterface.cs new file mode 100644 index 0000000000..27ab3de256 --- /dev/null +++ b/Content.Client/GameObjects/Components/Chemistry/ChemMaster/ChemMasterBoundUserInterface.cs @@ -0,0 +1,91 @@ +#nullable enable +using Content.Client.GameObjects.Components.Chemistry.ChemMaster; +using Content.Shared.GameObjects.Components.Chemistry; +using JetBrains.Annotations; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Localization; +using static Content.Shared.GameObjects.Components.Chemistry.SharedChemMasterComponent; + +namespace Content.Client.GameObjects.Components.Chemistry +{ + /// + /// Initializes a and updates it when new server messages are received. + /// + [UsedImplicitly] + public class ChemMasterBoundUserInterface : BoundUserInterface + { + private ChemMasterWindow? _window; + + public ChemMasterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + + } + + /// + /// Called each time a chem master UI instance is opened. Generates the window and fills it with + /// relevant info. Sets the actions for static buttons. + /// + protected override void Open() + { + base.Open(); + + //Setup window layout/elements + _window = new ChemMasterWindow + { + Title = Loc.GetString("ChemMaster 4000"), + }; + + _window.OpenCentered(); + _window.OnClose += Close; + + //Setup static button actions. + _window.EjectButton.OnPressed += _ => PrepareData(UiAction.Eject, null, null, null); + _window.BufferTransferButton.OnPressed += _ => PrepareData(UiAction.Transfer, null, null, null); + _window.BufferDiscardButton.OnPressed += _ => PrepareData(UiAction.Discard, null, null, null); + _window.CreatePills.OnPressed += _ => PrepareData(UiAction.CreatePills, null, _window.PillAmount.Value, null); + _window.CreateBottles.OnPressed += _ => PrepareData(UiAction.CreateBottles, null, null, _window.BottleAmount.Value); + + _window.OnChemButtonPressed += (args, button) => PrepareData(UiAction.ChemButton, button, null, null); + } + + /// + /// Update the ui each time new state data is sent from the server. + /// + /// + /// Data of the that this ui represents. + /// Sent from the server. + /// + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + var castState = (ChemMasterBoundUserInterfaceState)state; + + _window?.UpdateState(castState); //Update window state + } + + private void PrepareData(UiAction action, ChemButton? button, int? pillAmount, int? bottleAmount) + { + if (button != null) + { + SendMessage(new UiActionMessage(action, button.Amount, button.Id, button.isBuffer, null, null)); + } + else + { + SendMessage(new UiActionMessage(action, null, null, null, pillAmount, bottleAmount)); + } + + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _window?.Dispose(); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/Chemistry/ChemMaster/ChemMasterWindow.cs b/Content.Client/GameObjects/Components/Chemistry/ChemMaster/ChemMasterWindow.cs new file mode 100644 index 0000000000..a204c63124 --- /dev/null +++ b/Content.Client/GameObjects/Components/Chemistry/ChemMaster/ChemMasterWindow.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Client.UserInterface; +using Content.Client.UserInterface.Stylesheets; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Chemistry; +using Robust.Client.Graphics.Drawing; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using static Content.Shared.GameObjects.Components.Chemistry.SharedChemMasterComponent; + +namespace Content.Client.GameObjects.Components.Chemistry.ChemMaster +{ + /// + /// Client-side UI used to control a + /// + public class ChemMasterWindow : SS14Window + { + /// Contains info about the reagent container such as it's contents, if one is loaded into the dispenser. + private readonly VBoxContainer ContainerInfo; + + private readonly VBoxContainer BufferInfo; + + private readonly VBoxContainer PackagingInfo; + + /// Ejects the reagent container from the dispenser. + public Button EjectButton { get; } + + public Button BufferTransferButton { get; } + public Button BufferDiscardButton { get; } + + public bool BufferModeTransfer = true; + + public event Action OnChemButtonPressed; + + public HBoxContainer PillInfo { get; set; } + public HBoxContainer BottleInfo { get; set; } + public SpinBox PillAmount { get; set; } + public SpinBox BottleAmount { get; set; } + public Button CreatePills { get; } + public Button CreateBottles { get; } + +#pragma warning disable 649 + [Dependency] private readonly IPrototypeManager _prototypeManager; + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + + protected override Vector2? CustomSize => (400, 200); + + /// + /// Create and initialize the chem master UI client-side. Creates the basic layout, + /// actual data isn't filled in until the server sends data about the chem master. + /// + public ChemMasterWindow() + { + IoCManager.InjectDependencies(this); + + Contents.AddChild(new VBoxContainer + { + Children = + { + //Container + new HBoxContainer + { + Children = + { + new Label {Text = _localizationManager.GetString("Container")}, + new Control {SizeFlagsHorizontal = SizeFlags.FillExpand}, + (EjectButton = new Button {Text = _localizationManager.GetString("Eject")}) + } + }, + //Wrap the container info in a PanelContainer so we can color it's background differently. + new PanelContainer + { + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsStretchRatio = 6, + CustomMinimumSize = (0, 200), + PanelOverride = new StyleBoxFlat + { + BackgroundColor = new Color(27, 27, 30) + }, + Children = + { + //Currently empty, when server sends state data this will have container contents and fill volume. + (ContainerInfo = new VBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new Label + { + Text = _localizationManager.GetString("No container loaded.") + } + } + }), + } + }, + + //Padding + new Control {CustomMinimumSize = (0.0f, 10.0f)}, + + //Buffer + new HBoxContainer + { + Children = + { + new Label {Text = _localizationManager.GetString("Buffer")}, + new Control {SizeFlagsHorizontal = SizeFlags.FillExpand}, + (BufferTransferButton = new Button {Text = _localizationManager.GetString("Transfer"), Pressed = BufferModeTransfer, StyleClasses = { StyleBase.ButtonOpenRight }}), + (BufferDiscardButton = new Button {Text = _localizationManager.GetString("Discard"), Pressed = !BufferModeTransfer, StyleClasses = { StyleBase.ButtonOpenLeft }}) + } + }, + + //Wrap the buffer info in a PanelContainer so we can color it's background differently. + new PanelContainer + { + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsStretchRatio = 6, + CustomMinimumSize = (0, 100), + PanelOverride = new StyleBoxFlat + { + BackgroundColor = new Color(27, 27, 30) + }, + Children = + { + //Buffer reagent list + (BufferInfo = new VBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new Label + { + Text = _localizationManager.GetString("Buffer empty.") + } + } + }), + } + }, + + //Padding + new Control {CustomMinimumSize = (0.0f, 10.0f)}, + + //Packaging + new HBoxContainer + { + Children = + { + new Label {Text = _localizationManager.GetString("Packaging ")}, + } + }, + + //Wrap the packaging info in a PanelContainer so we can color it's background differently. + new PanelContainer + { + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsStretchRatio = 6, + CustomMinimumSize = (0, 100), + PanelOverride = new StyleBoxFlat + { + BackgroundColor = new Color(27, 27, 30) + }, + Children = + { + //Packaging options + (PackagingInfo = new VBoxContainer + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + }), + + } + }, + } + }); + + //Pills + PillInfo = new HBoxContainer + { + Children = + { + new Label + { + Text = _localizationManager.GetString("Pills:") + }, + + }, + + }; + PackagingInfo.AddChild(PillInfo); + + var pillPadding = new Control {SizeFlagsHorizontal = SizeFlags.FillExpand}; + PillInfo.AddChild(pillPadding); + + PillAmount = new SpinBox + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Value = 1 + }; + PillAmount.InitDefaultButtons(); + PillAmount.IsValid = (n) => (n > 0 && n <= 10); + PillInfo.AddChild(PillAmount); + + var pillVolume = new Label + { + Text = " max 50u/each ", + StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} + }; + PillInfo.AddChild((pillVolume)); + + CreatePills = new Button {Text = _localizationManager.GetString("Create")}; + PillInfo.AddChild(CreatePills); + + //Bottles + BottleInfo = new HBoxContainer + { + Children = + { + new Label + { + Text = _localizationManager.GetString("Bottles:") + }, + + }, + + }; + PackagingInfo.AddChild(BottleInfo); + + var bottlePadding = new Control {SizeFlagsHorizontal = SizeFlags.FillExpand}; + BottleInfo.AddChild(bottlePadding); + + BottleAmount = new SpinBox + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + Value = 1 + }; + BottleAmount.InitDefaultButtons(); + BottleAmount.IsValid = (n) => (n > 0 && n <= 10); + BottleInfo.AddChild(BottleAmount); + + var bottleVolume = new Label + { + Text = " max 30u/each ", + StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} + }; + BottleInfo.AddChild((bottleVolume)); + + CreateBottles = new Button {Text = _localizationManager.GetString("Create")}; + BottleInfo.AddChild(CreateBottles); + } + + private ChemButton MakeChemButton(string text, ReagentUnit amount, string id, bool isBuffer, string styleClass) + { + var button = new ChemButton(text, amount, id, isBuffer, styleClass); + button.OnPressed += args + => OnChemButtonPressed?.Invoke(args, button); + return button; + } + + /// + /// Update the UI state when new state data is received from the server. + /// + /// State data sent by the server. + public void UpdateState(BoundUserInterfaceState state) + { + var castState = (ChemMasterBoundUserInterfaceState) state; + Title = castState.DispenserName; + UpdatePanelInfo(castState); + } + + /// + /// Update the container, buffer, and packaging panels. + /// + /// State data for the dispenser. + private void UpdatePanelInfo(ChemMasterBoundUserInterfaceState state) + { + BufferModeTransfer = state.BufferModeTransfer; + BufferTransferButton.Pressed = BufferModeTransfer; + BufferDiscardButton.Pressed = !BufferModeTransfer; + + ContainerInfo.Children.Clear(); + + if (!state.HasBeaker) + { + ContainerInfo.Children.Add(new Label {Text = _localizationManager.GetString("No container loaded.")}); + return; + } + + ContainerInfo.Children.Add(new HBoxContainer // Name of the container and its fill status (Ex: 44/100u) + { + Children = + { + new Label {Text = $"{state.ContainerName}: "}, + new Label + { + Text = $"{state.BeakerCurrentVolume}/{state.BeakerMaxVolume}", + StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} + } + } + }); + + foreach (var reagent in state.ContainerReagents) + { + var name = _localizationManager.GetString("Unknown reagent"); + //Try to the prototype for the given reagent. This gives us it's name. + if (_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto)) + { + name = proto.Name; + } + + if (proto != null) + { + ContainerInfo.Children.Add(new HBoxContainer + { + Children = + { + new Label {Text = $"{name}: "}, + new Label + { + Text = $"{reagent.Quantity}u", + StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} + }, + + //Padding + new Control {SizeFlagsHorizontal = SizeFlags.FillExpand}, + + MakeChemButton("1", ReagentUnit.New(1), reagent.ReagentId, false, StyleBase.ButtonOpenRight), + MakeChemButton("5", ReagentUnit.New(5), reagent.ReagentId, false, StyleBase.ButtonOpenBoth), + MakeChemButton("10", ReagentUnit.New(10), reagent.ReagentId, false, StyleBase.ButtonOpenBoth), + MakeChemButton("25", ReagentUnit.New(25), reagent.ReagentId, false, StyleBase.ButtonOpenBoth), + MakeChemButton("All", ReagentUnit.New(-1), reagent.ReagentId, false, StyleBase.ButtonOpenLeft), + } + }); + } + } + + BufferInfo.Children.Clear(); + + if (!state.BufferReagents.Any()) + { + BufferInfo.Children.Add(new Label {Text = _localizationManager.GetString("Buffer empty.")}); + return; + } + + var bufferHBox = new HBoxContainer(); + BufferInfo.AddChild(bufferHBox); + + var bufferLabel = new Label {Text = "buffer: "}; + bufferHBox.AddChild(bufferLabel); + var bufferVol = new Label + { + Text = $"{state.BufferCurrentVolume}/{state.BufferMaxVolume}", + StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} + }; + bufferHBox.AddChild(bufferVol); + + foreach (var reagent in state.BufferReagents) + { + var name = _localizationManager.GetString("Unknown reagent"); + //Try to the prototype for the given reagent. This gives us it's name. + if (_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto)) + { + name = proto.Name; + } + + if (proto != null) + { + BufferInfo.Children.Add(new HBoxContainer + { + //SizeFlagsHorizontal = SizeFlags.ShrinkEnd, + Children = + { + new Label {Text = $"{name}: "}, + new Label + { + Text = $"{reagent.Quantity}u", + StyleClasses = {StyleNano.StyleClassLabelSecondaryColor} + }, + + //Padding + new Control {SizeFlagsHorizontal = SizeFlags.FillExpand}, + + MakeChemButton("1", ReagentUnit.New(1), reagent.ReagentId, true, StyleBase.ButtonOpenRight), + MakeChemButton("5", ReagentUnit.New(5), reagent.ReagentId, true, StyleBase.ButtonOpenBoth), + MakeChemButton("10", ReagentUnit.New(10), reagent.ReagentId, true, StyleBase.ButtonOpenBoth), + MakeChemButton("25", ReagentUnit.New(25), reagent.ReagentId, true, StyleBase.ButtonOpenBoth), + MakeChemButton("All", ReagentUnit.New(-1), reagent.ReagentId, true, StyleBase.ButtonOpenLeft), + } + }); + } + } + } + } + + public class ChemButton : Button + { + public ReagentUnit Amount { get; set; } + public bool isBuffer = true; + public string Id { get; set; } + public ChemButton(string _text, ReagentUnit _amount, string _id, bool _isBuffer, string _styleClass) + { + AddStyleClass(_styleClass); + Text = _text; + Amount = _amount; + Id = _id; + isBuffer = _isBuffer; + } + + } +} diff --git a/Content.Client/GameObjects/Components/Chemistry/ReagentDispenserBoundUserInterface.cs b/Content.Client/GameObjects/Components/Chemistry/ReagentDispenser/ReagentDispenserBoundUserInterface.cs similarity index 100% rename from Content.Client/GameObjects/Components/Chemistry/ReagentDispenserBoundUserInterface.cs rename to Content.Client/GameObjects/Components/Chemistry/ReagentDispenser/ReagentDispenserBoundUserInterface.cs diff --git a/Content.Client/GameObjects/Components/Chemistry/ReagentDispenserWindow.cs b/Content.Client/GameObjects/Components/Chemistry/ReagentDispenser/ReagentDispenserWindow.cs similarity index 100% rename from Content.Client/GameObjects/Components/Chemistry/ReagentDispenserWindow.cs rename to Content.Client/GameObjects/Components/Chemistry/ReagentDispenser/ReagentDispenserWindow.cs diff --git a/Content.Server/GameObjects/Components/Chemistry/ChemMasterComponent.cs b/Content.Server/GameObjects/Components/Chemistry/ChemMasterComponent.cs new file mode 100644 index 0000000000..3a73ee3444 --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/ChemMasterComponent.cs @@ -0,0 +1,410 @@ +using System; +using System.Linq; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Sound; +using Content.Server.Interfaces.GameObjects.Components.Interaction; +using Content.Server.GameObjects.Components.Power; +using Content.Server.Interfaces; +using Content.Server.Interfaces.GameObjects; +using Content.Server.Utility; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Chemistry; +using Content.Shared.GameObjects.EntitySystems; +using Robust.Server.GameObjects.Components.Container; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Shared.GameObjects.Systems; +using Content.Server.GameObjects.Components.Power.ApcNetComponents; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.Log; +using Robust.Shared.Maths; +using Robust.Shared.Random; + +namespace Content.Server.GameObjects.Components.Chemistry +{ + /// + /// Contains all the server-side logic for chem masters. See also . + /// This includes initializing the component based on prototype data, and sending and receiving messages from the client. + /// Messages sent to the client are used to update update the user interface for a component instance. + /// Messages sent from the client are used to handle ui button presses. + /// + [RegisterComponent] + [ComponentReference(typeof(IActivate))] + [ComponentReference(typeof(IInteractUsing))] + public class ChemMasterComponent : SharedChemMasterComponent, IActivate, IInteractUsing, ISolutionChange + { +#pragma warning disable 649 + [Dependency] private readonly IServerNotifyManager _notifyManager; + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + + [ViewVariables] private BoundUserInterface _userInterface; + [ViewVariables] private ContainerSlot _beakerContainer; + [ViewVariables] private string _packPrototypeId; + + [ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null; + + [ViewVariables] private bool BufferModeTransfer = true; + + private PowerReceiverComponent _powerReceiver; + private bool Powered => _powerReceiver.Powered; + + private readonly SolutionComponent BufferSolution = new SolutionComponent(); + + + /// + /// Shows the serializer how to save/load this components yaml prototype. + /// + /// Yaml serializer + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _packPrototypeId, "pack", string.Empty); + } + + /// + /// Called once per instance of this component. Gets references to any other components needed + /// by this component and initializes it's UI and other data. + /// + public override void Initialize() + { + base.Initialize(); + _userInterface = Owner.GetComponent() + .GetBoundUserInterface(ChemMasterUiKey.Key); + _userInterface.OnReceiveMessage += OnUiReceiveMessage; + + _beakerContainer = + ContainerManagerComponent.Ensure($"{Name}-reagentContainerContainer", Owner); + _powerReceiver = Owner.GetComponent(); + + //BufferSolution = Owner.BufferSolution + BufferSolution.Solution = new Solution(); + BufferSolution.MaxVolume = ReagentUnit.New(1000); + + UpdateUserInterface(); + } + + /// + /// Handles ui messages from the client. For things such as button presses + /// which interact with the world and require server action. + /// + /// A user interface message from the client. + private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj) + { + if (!PlayerCanUseChemMaster(obj.Session.AttachedEntity)) + return; + + var msg = (UiActionMessage) obj.Message; + switch (msg.action) + { + case UiAction.Eject: + TryEject(obj.Session.AttachedEntity); + break; + case UiAction.ChemButton: + TransferReagent(msg.id, msg.amount, msg.isBuffer); + break; + case UiAction.Transfer: + BufferModeTransfer = true; + UpdateUserInterface(); + break; + case UiAction.Discard: + BufferModeTransfer = false; + UpdateUserInterface(); + break; + case UiAction.CreatePills: + case UiAction.CreateBottles: + TryCreatePackage(obj.Session.AttachedEntity, msg.action, msg.pillAmount, msg.bottleAmount); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + ClickSound(); + } + + /// + /// Checks whether the player entity is able to use the chem master. + /// + /// The player entity. + /// Returns true if the entity can use the chem master, and false if it cannot. + private bool PlayerCanUseChemMaster(IEntity playerEntity) + { + //Need player entity to check if they are still able to use the chem master + if (playerEntity == null) + return false; + //Check if player can interact in their current state + if (!ActionBlockerSystem.CanInteract(playerEntity) || !ActionBlockerSystem.CanUse(playerEntity)) + return false; + //Check if device is powered + if (!Powered) + return false; + + return true; + } + + /// + /// Gets component data to be used to update the user interface client-side. + /// + /// Returns a + private ChemMasterBoundUserInterfaceState GetUserInterfaceState() + { + var beaker = _beakerContainer.ContainedEntity; + if (beaker == null) + { + return new ChemMasterBoundUserInterfaceState(false, ReagentUnit.New(0), ReagentUnit.New(0), + "", Owner.Name, null, BufferSolution.ReagentList.ToList(), BufferModeTransfer, BufferSolution.CurrentVolume, BufferSolution.MaxVolume); + } + + var solution = beaker.GetComponent(); + return new ChemMasterBoundUserInterfaceState(true, solution.CurrentVolume, solution.MaxVolume, + beaker.Name, Owner.Name, solution.ReagentList.ToList(), BufferSolution.ReagentList.ToList(), BufferModeTransfer, BufferSolution.CurrentVolume, BufferSolution.MaxVolume); + } + + private void UpdateUserInterface() + { + var state = GetUserInterfaceState(); + _userInterface.SetState(state); + } + + /// + /// If this component contains an entity with a , eject it. + /// Tries to eject into user's hands first, then ejects onto chem master if both hands are full. + /// + private void TryEject(IEntity user) + { + if (!HasBeaker) + return; + + var beaker = _beakerContainer.ContainedEntity; + _beakerContainer.Remove(_beakerContainer.ContainedEntity); + UpdateUserInterface(); + + if(!user.TryGetComponent(out var hands) || !beaker.TryGetComponent(out var item)) + return; + if (hands.CanPutInHand(item)) + hands.PutInHand(item); + } + + private void TransferReagent(string id, ReagentUnit amount, bool isBuffer) + { + if (!HasBeaker && BufferModeTransfer) return; + var beaker = _beakerContainer.ContainedEntity; + var beakerSolution = beaker.GetComponent(); + if (isBuffer) + { + foreach (var reagent in BufferSolution.Solution.Contents) + { + if (reagent.ReagentId == id) + { + ReagentUnit actualAmount; + if (amount == ReagentUnit.New(-1)) + { + actualAmount = ReagentUnit.Min(reagent.Quantity, beakerSolution.EmptyVolume); + } + else + { + actualAmount = ReagentUnit.Min(reagent.Quantity, amount, beakerSolution.EmptyVolume); + } + + BufferSolution.Solution.RemoveReagent(id, actualAmount); + if (BufferModeTransfer) + { + beakerSolution.Solution.AddReagent(id, actualAmount); + } + break; + } + + } + } + else + { + foreach (var reagent in beakerSolution.Solution.Contents) + { + if (reagent.ReagentId == id) + { + ReagentUnit actualAmount; + if (amount == ReagentUnit.New(-1)) + { + actualAmount = ReagentUnit.Min(reagent.Quantity, BufferSolution.EmptyVolume); + } + else + { + actualAmount = ReagentUnit.Min(reagent.Quantity, amount, BufferSolution.EmptyVolume); + } + beakerSolution.TryRemoveReagent(id, actualAmount); + BufferSolution.Solution.AddReagent(id, actualAmount); + break; + } + } + } + + UpdateUserInterface(); + } + + private void TryCreatePackage(IEntity user, UiAction action, int pillAmount, int bottleAmount) + { + var random = IoCManager.Resolve(); + + if (action == UiAction.CreateBottles) + { + var individualVolume = BufferSolution.CurrentVolume / ReagentUnit.New(bottleAmount); + var actualVolume = ReagentUnit.Min(individualVolume, ReagentUnit.New(30)); + for (int i = 0; i < bottleAmount; i++) + { + var bottle = Owner.EntityManager.SpawnEntity("bottle", Owner.Transform.GridPosition); + + var bufferSolution = BufferSolution.Solution.SplitSolution(actualVolume); + + bottle.TryGetComponent(out var bottleSolution); + bottleSolution?.Solution.AddSolution(bufferSolution); + + //Try to give them the bottle + if (user.TryGetComponent(out var hands) && + bottle.TryGetComponent(out var item)) + { + if (hands.CanPutInHand(item)) + { + hands.PutInHand(item); + continue; + } + + } + + //Put it on the floor + bottle.Transform.GridPosition = user.Transform.GridPosition; + //Give it an offset + var x_negative = random.Prob(0.5f) ? -1 : 1; + var y_negative = random.Prob(0.5f) ? -1 : 1; + bottle.Transform.LocalPosition += new Vector2(random.NextFloat() * 0.2f * x_negative, random.NextFloat() * 0.2f * y_negative); + } + + } + else //Pills + { + var individualVolume = BufferSolution.CurrentVolume / ReagentUnit.New(pillAmount); + var actualVolume = ReagentUnit.Min(individualVolume, ReagentUnit.New(50)); + for (int i = 0; i < pillAmount; i++) + { + var pill = Owner.EntityManager.SpawnEntity("pill", Owner.Transform.GridPosition); + + var bufferSolution = BufferSolution.Solution.SplitSolution(actualVolume); + + pill.TryGetComponent(out var pillSolution); + pillSolution?.Solution.AddSolution(bufferSolution); + + //Try to give them the bottle + if (user.TryGetComponent(out var hands) && + pill.TryGetComponent(out var item)) + { + if (hands.CanPutInHand(item)) + { + hands.PutInHand(item); + continue; + } + + } + + //Put it on the floor + pill.Transform.GridPosition = user.Transform.GridPosition; + //Give it an offset + var x_negative = random.Prob(0.5f) ? -1 : 1; + var y_negative = random.Prob(0.5f) ? -1 : 1; + pill.Transform.LocalPosition += new Vector2(random.NextFloat() * 0.2f * x_negative, random.NextFloat() * 0.2f * y_negative); + } + } + + UpdateUserInterface(); + } + + /// + /// Called when you click the owner entity with an empty hand. Opens the UI client-side if possible. + /// + /// Data relevant to the event such as the actor which triggered it. + void IActivate.Activate(ActivateEventArgs args) + { + if (!args.User.TryGetComponent(out IActorComponent actor)) + { + return; + } + + if (!args.User.TryGetComponent(out IHandsComponent hands)) + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User, + _localizationManager.GetString("You have no hands.")); + return; + } + + if (!Powered) + return; + + var activeHandEntity = hands.GetActiveHand?.Owner; + if (activeHandEntity == null) + { + _userInterface.Open(actor.playerSession); + } + } + + /// + /// Called when you click the owner entity with something in your active hand. If the entity in your hand + /// contains a , if you have hands, and if the chem master doesn't already + /// hold a container, it will be added to the chem master. + /// + /// Data relevant to the event such as the actor which triggered it. + /// + bool IInteractUsing.InteractUsing(InteractUsingEventArgs args) + { + if (!args.User.TryGetComponent(out IHandsComponent hands)) + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User, + _localizationManager.GetString("You have no hands.")); + return true; + } + + var activeHandEntity = hands.GetActiveHand.Owner; + if (activeHandEntity.TryGetComponent(out var solution)) + { + if (HasBeaker) + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User, + _localizationManager.GetString("This ChemMaster already has a container in it.")); + } + else if ((solution.Capabilities & SolutionCaps.FitsInDispenser) == 0) //Close enough to a chem master... + { + //If it can't fit in the chem master, don't put it in. For example, buckets and mop buckets can't fit. + _notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User, + _localizationManager.GetString("That can't fit in the ChemMaster.")); + } + else + { + _beakerContainer.Insert(activeHandEntity); + UpdateUserInterface(); + } + } + else + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User, + _localizationManager.GetString("You can't put this in the ChemMaster.")); + } + + return true; + } + + void ISolutionChange.SolutionChanged(SolutionChangeEventArgs eventArgs) => UpdateUserInterface(); + + private void ClickSound() + { + + EntitySystem.Get().PlayFromEntity("/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f)); + } + } +} diff --git a/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs b/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs new file mode 100644 index 0000000000..b41f462c5c --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/PillComponent.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using Content.Server.GameObjects.Components.Chemistry; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Nutrition; +using Content.Server.GameObjects.Components.Utensil; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Utility; +using Content.Server.GameObjects.Components.Sound; +using Content.Server.Interfaces.GameObjects.Components.Interaction; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Utensil; +using Content.Shared.Interfaces; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Chemistry +{ + [RegisterComponent] + [ComponentReference(typeof(IAfterInteract))] + public class PillComponent : FoodComponent, IUse, IAfterInteract + { +#pragma warning disable 649 + [Dependency] private readonly IEntitySystemManager _entitySystem; +#pragma warning restore 649 + public override string Name => "Pill"; + + [ViewVariables] + private string _useSound; + [ViewVariables] + private string _trashPrototype; + [ViewVariables] + private SolutionComponent _contents; + [ViewVariables] + private ReagentUnit _transferAmount; + + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(ref _useSound, "useSound", null); + serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(1000)); + serializer.DataField(ref _trashPrototype, "trash", null); + } + + public override void Initialize() + { + base.Initialize(); + _contents = Owner.GetComponent(); + _transferAmount = _contents.CurrentVolume; + + } + + bool IUse.UseEntity(UseEntityEventArgs eventArgs) + { + return TryUseFood(eventArgs.User, null); + } + + // Feeding someone else + void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + { + if (eventArgs.Target == null) + { + return; + } + + TryUseFood(eventArgs.User, eventArgs.Target); + } + + public override bool TryUseFood(IEntity user, IEntity target, UtensilComponent utensilUsed = null) + { + if (user == null) + { + return false; + } + + var trueTarget = target ?? user; + + if (!trueTarget.TryGetComponent(out StomachComponent stomach)) + { + return false; + } + + if (!InteractionChecks.InRangeUnobstructed(user, trueTarget.Transform.MapPosition)) + { + return false; + } + + var transferAmount = ReagentUnit.Min(_transferAmount, _contents.CurrentVolume); + var split = _contents.SplitSolution(transferAmount); + if (!stomach.TryTransferSolution(split)) + { + _contents.TryAddSolution(split); + trueTarget.PopupMessage(user, Loc.GetString("You can't eat any more!")); + return false; + } + + if (_useSound != null) + { + _entitySystem.GetEntitySystem() + .PlayFromEntity(_useSound, trueTarget, AudioParams.Default.WithVolume(-1f)); + } + + trueTarget.PopupMessage(user, Loc.GetString("You swallow the pill.")); + + Owner.Delete(); + return true; + } + } +} diff --git a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs index 89c492beaa..f7e286aafd 100644 --- a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs @@ -92,7 +92,7 @@ namespace Content.Server.GameObjects.Components.Nutrition TryUseFood(eventArgs.User, eventArgs.Target); } - public bool TryUseFood(IEntity user, IEntity target, UtensilComponent utensilUsed = null) + public virtual bool TryUseFood(IEntity user, IEntity target, UtensilComponent utensilUsed = null) { if (user == null) { diff --git a/Content.Shared/GameObjects/Components/Chemistry/ChemMaster/SharedChemMasterComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/ChemMaster/SharedChemMasterComponent.cs new file mode 100644 index 0000000000..793e842886 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Chemistry/ChemMaster/SharedChemMasterComponent.cs @@ -0,0 +1,116 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Content.Shared.Chemistry; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Chemistry +{ + + /// + /// Shared class for ChemMasterComponent. Provides a way for entities to split reagents from a beaker and produce pills and bottles via a user interface. + /// + public class SharedChemMasterComponent : Component + { + public override string Name => "ChemMaster"; + + [Serializable, NetSerializable] + public class ChemMasterBoundUserInterfaceState : BoundUserInterfaceState + { + public readonly bool HasBeaker; + public readonly ReagentUnit BeakerCurrentVolume; + public readonly ReagentUnit BeakerMaxVolume; + public readonly string ContainerName; + + /// + /// A list of the reagents and their amounts within the beaker/reagent container, if applicable. + /// + public readonly List ContainerReagents; + /// + /// A list of the reagents and their amounts within the buffer, if applicable. + /// + public readonly List BufferReagents; + public readonly string DispenserName; + + public readonly bool BufferModeTransfer; + + public readonly ReagentUnit BufferCurrentVolume; + public readonly ReagentUnit BufferMaxVolume; + + public ChemMasterBoundUserInterfaceState(bool hasBeaker, ReagentUnit beakerCurrentVolume, ReagentUnit beakerMaxVolume, string containerName, + string dispenserName, List containerReagents, List bufferReagents, bool bufferModeTransfer, ReagentUnit bufferCurrentVolume, ReagentUnit bufferMaxVolume) + { + HasBeaker = hasBeaker; + BeakerCurrentVolume = beakerCurrentVolume; + BeakerMaxVolume = beakerMaxVolume; + ContainerName = containerName; + DispenserName = dispenserName; + ContainerReagents = containerReagents; + BufferReagents = bufferReagents; + BufferModeTransfer = bufferModeTransfer; + BufferCurrentVolume = bufferCurrentVolume; + BufferMaxVolume = bufferMaxVolume; + } + } + + /// + /// Message data sent from client to server when a ChemMaster ui button is pressed. + /// + [Serializable, NetSerializable] + public class UiActionMessage : BoundUserInterfaceMessage + { + public readonly UiAction action; + public readonly ReagentUnit amount; + public readonly string id = ""; + public readonly bool isBuffer; + public readonly int pillAmount; + public readonly int bottleAmount; + + public UiActionMessage(UiAction _action, ReagentUnit? _amount, string? _id, bool? _isBuffer, int? _pillAmount, int? _bottleAmount) + { + action = _action; + if (action == UiAction.ChemButton) + { + amount = _amount.GetValueOrDefault(); + if (_id == null) + { + id = "null"; + } + else + { + id = _id; + } + + isBuffer = _isBuffer.GetValueOrDefault(); + } + else + { + pillAmount = _pillAmount.GetValueOrDefault(); + bottleAmount = _bottleAmount.GetValueOrDefault(); + } + } + } + + [Serializable, NetSerializable] + public enum ChemMasterUiKey + { + Key + } + + /// + /// Used in to specify which button was pressed. + /// + public enum UiAction + { + Eject, + Transfer, + Discard, + ChemButton, + CreatePills, + CreateBottles + } + + } +} diff --git a/Content.Shared/GameObjects/Components/Chemistry/ReagentDispenserInventoryPrototype.cs b/Content.Shared/GameObjects/Components/Chemistry/ReagentDispenser/ReagentDispenserInventoryPrototype.cs similarity index 100% rename from Content.Shared/GameObjects/Components/Chemistry/ReagentDispenserInventoryPrototype.cs rename to Content.Shared/GameObjects/Components/Chemistry/ReagentDispenser/ReagentDispenserInventoryPrototype.cs diff --git a/Content.Shared/GameObjects/Components/Chemistry/SharedReagentDispenserComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/ReagentDispenser/SharedReagentDispenserComponent.cs similarity index 100% rename from Content.Shared/GameObjects/Components/Chemistry/SharedReagentDispenserComponent.cs rename to Content.Shared/GameObjects/Components/Chemistry/ReagentDispenser/SharedReagentDispenserComponent.cs diff --git a/Resources/Prototypes/Entities/Buildings/chem_master.yml b/Resources/Prototypes/Entities/Buildings/chem_master.yml new file mode 100644 index 0000000000..771143b657 --- /dev/null +++ b/Resources/Prototypes/Entities/Buildings/chem_master.yml @@ -0,0 +1,33 @@ +- type: entity + id: chem_master + name: ChemMaster 4000 + description: An industrial grade chemical manipulator with pill and bottle production included. + components: + - type: Sprite + texture: Constructible/Power/mixer.rsi/mixer_loaded.png + - type: Icon + texture: Constructible/Power/mixer.rsi/mixer_loaded.png + - type: ChemMaster + - type: PowerReceiver + - type: Clickable + - type: InteractionOutline + - type: Anchorable + - type: Collidable + shapes: + - !type:PhysShapeAabb + bounds: "-0.4,-0.25,0.4,0.25" + layer: + - Opaque + - Impassable + - MobImpassable + - VaultImpassable + IsScrapingFloor: true + - type: Physics + mass: 25 + Anchored: true + - type: SnapGrid + offset: Center + - type: UserInterface + interfaces: + - key: enum.ChemMasterUiKey.Key + type: ChemMasterBoundUserInterface diff --git a/Resources/Prototypes/Entities/Items/chemistry.yml b/Resources/Prototypes/Entities/Items/chemistry.yml index c8dc8607a2..7528a15332 100644 --- a/Resources/Prototypes/Entities/Items/chemistry.yml +++ b/Resources/Prototypes/Entities/Items/chemistry.yml @@ -77,3 +77,33 @@ - type: Injector injectOnly: false - type: CanSpill + +- type: entity + name: bottle + parent: BaseItem + id: bottle + components: + - type: Drink + - type: Solution + maxVol: 30 + - type: Pourable + transferAmount: 5 + - type: Sprite + texture: Objects/Specific/Chemistry/bottle.rsi/bottle.png + - type: Icon + texture: Objects/Specific/Chemistry/bottle.rsi/bottle.png + - type: CanSpill + +- type: entity + name: pill + parent: FoodBase + id: pill + description: It's not a suppository. + components: + - type: Pill + - type: Solution + maxVol: 50 + - type: Sprite + texture: Objects/Specific/Chemistry/pills.rsi/pill.png + - type: Icon + texture: Objects/Specific/Chemistry/pills.rsi/pill.png