diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 4e4ec61789..ae73f0f92a 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -126,6 +126,8 @@ namespace Content.Client factory.Register(); factory.Register(); + factory.Register(); + prototypes.RegisterIgnore("material"); IoCManager.Register(); diff --git a/Content.Client/GameObjects/Components/Chemistry/ReagentDispenserBoundUserInterface.cs b/Content.Client/GameObjects/Components/Chemistry/ReagentDispenserBoundUserInterface.cs new file mode 100644 index 0000000000..a81f7dc79f --- /dev/null +++ b/Content.Client/GameObjects/Components/Chemistry/ReagentDispenserBoundUserInterface.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using static Content.Shared.GameObjects.Components.Chemistry.SharedReagentDispenserComponent; + +namespace Content.Client.GameObjects.Components.Chemistry +{ + /// + /// Initializes a and updates it when new server messages are received. + /// + public class ReagentDispenserBoundUserInterface : BoundUserInterface + { +#pragma warning disable 649 + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + + private ReagentDispenserWindow _window; + private ReagentDispenserBoundUserInterfaceState _lastState; + + public ReagentDispenserBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + + } + + /// + /// Called each time a dispenser UI instance is opened. Generates the dispenser window and fills it with + /// relevant info. Sets the actions for static buttons. + /// Buttons which can change like reagent dispense buttons have their actions set in . + /// + protected override void Open() + { + base.Open(); + + //Setup window layout/elements + _window = new ReagentDispenserWindow + { + Title = _localizationManager.GetString("Reagent dispenser"), + Size = (500, 600) + }; + + _window.OpenCenteredMinSize(); + _window.OnClose += Close; + + //Setup static button actions. + _window.EjectButton.OnPressed += _ => ButtonPressed(UiButton.Eject); + _window.ClearButton.OnPressed += _ => ButtonPressed(UiButton.Clear); + _window.DispenseButton1.OnPressed += _ => ButtonPressed(UiButton.SetDispenseAmount1); + _window.DispenseButton5.OnPressed += _ => ButtonPressed(UiButton.SetDispenseAmount5); + _window.DispenseButton10.OnPressed += _ => ButtonPressed(UiButton.SetDispenseAmount10); + _window.DispenseButton25.OnPressed += _ => ButtonPressed(UiButton.SetDispenseAmount25); + _window.DispenseButton50.OnPressed += _ => ButtonPressed(UiButton.SetDispenseAmount50); + _window.DispenseButton100.OnPressed += _ => ButtonPressed(UiButton.SetDispenseAmount100); + } + + /// + /// 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 = (ReagentDispenserBoundUserInterfaceState)state; + _lastState = castState; + + _window?.UpdateState(castState); //Update window state + UpdateReagentsList(castState.Inventory); //Update reagents list & reagent button actions + + _window.ForceRunLayoutUpdate(); + } + + /// + /// Update the list of reagents that this dispenser can dispense on the UI. + /// + /// A list of the reagents which can be dispensed. + private void UpdateReagentsList(List inventory) + { + _window.UpdateReagentsList(inventory); + for (int i = 0; i < _window.ChemicalList.Children.Count(); i++) + { + var button = (Button)_window.ChemicalList.Children.ElementAt(i); + var i1 = i; + button.OnPressed += _ => ButtonPressed(UiButton.Dispense, i1); + button.OnMouseEntered += _ => + { + if (_lastState != null) + { + _window.UpdateContainerInfo(_lastState, inventory[i1].ID); + } + }; + button.OnMouseExited += _ => + { + if (_lastState != null) + { + _window.UpdateContainerInfo(_lastState); + } + }; + } + } + + public void ButtonPressed(UiButton button, int dispenseIndex = -1) + { + SendMessage(new UiButtonPressedMessage(button, dispenseIndex)); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _window.Dispose(); + } + } + } +} diff --git a/Content.Client/GameObjects/Components/Chemistry/ReagentDispenserWindow.cs b/Content.Client/GameObjects/Components/Chemistry/ReagentDispenserWindow.cs new file mode 100644 index 0000000000..fac745a745 --- /dev/null +++ b/Content.Client/GameObjects/Components/Chemistry/ReagentDispenserWindow.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using Content.Client.UserInterface; +using Content.Shared.Chemistry; +using Robust.Client.Graphics.Drawing; +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.Maths; +using Robust.Shared.Prototypes; +using static Content.Shared.GameObjects.Components.Chemistry.SharedReagentDispenserComponent; + +namespace Content.Client.GameObjects.Components.Chemistry +{ + /// + /// Client-side UI used to control a + /// + public class ReagentDispenserWindow : SS14Window + { + /// Sets the dispense amount to 1 when pressed. + public Button DispenseButton1; + /// Sets the dispense amount to 5 when pressed. + public Button DispenseButton5; + /// Sets the dispense amount to 10 when pressed. + public Button DispenseButton10; + /// Sets the dispense amount to 25 when pressed. + public Button DispenseButton25; + /// Sets the dispense amount to 50 when pressed. + public Button DispenseButton50; + /// Sets the dispense amount to 100 when pressed. + public Button DispenseButton100; + + /// Contains info about the reagent container such as it's contents, if one is loaded into the dispenser. + public VBoxContainer ContainerInfo; + + /// Ejects the reagent container from the dispenser. + public Button ClearButton; + /// Removes all reagents from the reagent container. + public Button EjectButton; + + /// A grid of buttons for each reagent which can be dispensed. + public GridContainer ChemicalList; + +#pragma warning disable 649 + [Dependency] private readonly IPrototypeManager _prototypeManager; + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + + /// + /// Create and initialize the dispenser UI client-side. Creates the basic layout, + /// actual data isn't filled in until the server sends data about the dispenser. + /// + public ReagentDispenserWindow() + { + _prototypeManager = IoCManager.Resolve(); + _localizationManager = IoCManager.Resolve(); + + Contents.AddChild(new VBoxContainer + { + Children = + { + //First, our dispense amount buttons + new HBoxContainer + { + Children = + { + new Label{Text = _localizationManager.GetString("Amount ")}, + (DispenseButton1 = new Button{Text = "1"}), + (DispenseButton5 = new Button{Text = "5"}), + (DispenseButton10 = new Button{Text = "10"}), + (DispenseButton25 = new Button{Text = "25"}), + (DispenseButton50 = new Button{Text = "50"}), + (DispenseButton100 = new Button{Text = "100"}), + } + }, + new Panel{CustomMinimumSize = (0.0f, 10.0f)}, //Padding + (ChemicalList = new GridContainer //Grid of which reagents can be dispensed. + { + CustomMinimumSize = (470.0f, 200.0f), + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsHorizontal = SizeFlags.FillExpand, + Columns = 5 + }), + new Panel{CustomMinimumSize = (0.0f, 10.0f)}, //Padding + new HBoxContainer + { + Children = + { + new Label{Text = _localizationManager.GetString("Container: ")}, + (ClearButton = new Button{Text = _localizationManager.GetString("Clear")}), + (EjectButton = new Button{Text = _localizationManager.GetString("Eject")}) + } + }, + new PanelContainer //Wrap the container info in a PanelContainer so we can color it's background differently. + { + SizeFlagsVertical = SizeFlags.FillExpand, + SizeFlagsStretchRatio = 6, + PanelOverride = new StyleBoxFlat + { + BackgroundColor = new Color(27, 27, 30) + }, + Children = + { + (ContainerInfo = new VBoxContainer //Currently empty, when server sends state data this will have container contents and fill volume. + { + MarginLeft = 5.0f, + SizeFlagsHorizontal = SizeFlags.FillExpand, + Children = + { + new Label{Text = _localizationManager.GetString("No container loaded.")} + } + }), + } + }, + } + }); + } + + /// + /// Update the button grid of reagents which can be dispensed. + /// The actions for these buttons are set in . + /// + /// Reagents which can be dispensed by this dispenser + public void UpdateReagentsList(List inventory) + { + if (ChemicalList == null) return; + if (inventory == null) return; + + ChemicalList.Children.Clear(); + + foreach (var entry in inventory) + { + if (_prototypeManager.TryIndex(entry.ID, out ReagentPrototype proto)) + { + ChemicalList.AddChild(new Button { Text = proto.Name }); + } + else + { + ChemicalList.AddChild(new Button { Text = _localizationManager.GetString("Reagent name not found") }); + } + + } + } + + /// + /// 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 = (ReagentDispenserBoundUserInterfaceState)state; + Title = castState.DispenserName; + UpdateContainerInfo(castState); + } + + /// + /// Update the fill state and list of reagents held by the current reagent container, if applicable. + /// Also highlights a reagent if it's dispense button is being mouse hovered. + /// + /// State data for the dispenser. + /// Prototype id of the reagent whose dispense button is currently being mouse hovered. + public void UpdateContainerInfo(ReagentDispenserBoundUserInterfaceState state, string highlightedReagentId = "InvalidReagent") + { + ContainerInfo.Children.Clear(); + if (state.HasBeaker) //If the dispenser doesn't have a beaker/container don't bother with this. + { + ContainerInfo.Children.Add(new HBoxContainer //Name of the container and it's fill status (Ex: 44/100u) + { + Children = + { + new Label{Text = $"{state.ContainerName}: "}, + new Label{Text = $"{state.BeakerCurrentVolume}/{state.BeakerMaxVolume}", StyleClasses = { NanoStyle.StyleClassLabelSecondaryColor }} + } + }); + //List the reagents in the container if it has any at all. + if (state.ContainerReagents != null) + { + //Loop through the reagents in the container. + foreach (var reagent in state.ContainerReagents) + { + //Try to the prototype for the given reagent. This gives us it's name. + if (_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto)) + { + //Check if the reagent is being moused over. If so, color it green. + if (proto.ID == highlightedReagentId) + { + ContainerInfo.Children.Add(new HBoxContainer + { + Children = + { + new Label {Text = $"{proto.Name}: ", StyleClasses = {NanoStyle.StyleClassPowerStateGood}}, + new Label + { + Text = $"{reagent.Quantity}u", + StyleClasses = {NanoStyle.StyleClassPowerStateGood} + } + } + }); + } + else //Otherwise, color it the normal colors. + { + ContainerInfo.Children.Add(new HBoxContainer + { + Children = + { + new Label {Text = $"{proto.Name}: "}, + new Label + { + Text = $"{reagent.Quantity}u", + StyleClasses = {NanoStyle.StyleClassLabelSecondaryColor} + } + } + }); + } + } + else //If you fail to get the reagents name, just call it "Unknown reagent". + { + ContainerInfo.Children.Add(new HBoxContainer + { + Children = + { + new Label {Text = _localizationManager.GetString("Unknown reagent: ")}, + new Label + { + Text = $"{reagent.Quantity}u", + StyleClasses = {NanoStyle.StyleClassLabelSecondaryColor} + } + } + }); + } + } + } + } + else + { + ContainerInfo.Children.Add(new Label{Text = _localizationManager.GetString("No container loaded.")}); + } + ForceRunLayoutUpdate(); //Force a layout update to avoid text hanging off the window until the user manually resizes it. + } + } +} diff --git a/Content.Client/UserInterface/NanoStyle.cs b/Content.Client/UserInterface/NanoStyle.cs index fd0df7a970..9ca13847cc 100644 --- a/Content.Client/UserInterface/NanoStyle.cs +++ b/Content.Client/UserInterface/NanoStyle.cs @@ -14,6 +14,7 @@ namespace Content.Client.UserInterface { public const string StyleClassLabelHeading = "LabelHeading"; public const string StyleClassLabelSubText = "LabelSubText"; + public const string StyleClassLabelSecondaryColor = "LabelSecondaryColor"; public const string StyleClassButtonBig = "ButtonBig"; private static readonly Color NanoGold = Color.FromHex("#A88B5E"); @@ -435,6 +436,12 @@ namespace Content.Client.UserInterface new StyleProperty(Label.StylePropertyFontColor, Color.DarkGray), } ), + new StyleRule(new SelectorElement(typeof(Label), new []{StyleClassLabelSecondaryColor}, null, null), new [] + { + new StyleProperty(Label.StylePropertyFont, notoSans12), + new StyleProperty(Label.StylePropertyFontColor, Color.DarkGray), + } ), + // Big Button new StyleRule(new SelectorElement(typeof(Button), new []{StyleClassButtonBig}, null, null), new [] { diff --git a/Content.Server/GameObjects/Components/Chemistry/ReagentDispenserComponent.cs b/Content.Server/GameObjects/Components/Chemistry/ReagentDispenserComponent.cs new file mode 100644 index 0000000000..9e30b2f288 --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/ReagentDispenserComponent.cs @@ -0,0 +1,268 @@ +using System; +using System.Linq; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces; +using Content.Server.Interfaces.GameObjects; +using Content.Shared.Chemistry; +using Content.Shared.GameObjects.Components.Chemistry; +using Robust.Server.GameObjects.Components.Container; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +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; + +namespace Content.Server.GameObjects.Components.Chemistry +{ + /// + /// Contains all the server-side logic for reagent dispensers. 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(IAttackBy))] + public class ReagentDispenserComponent : SharedReagentDispenserComponent, IActivate, IAttackBy + { +#pragma warning disable 649 + [Dependency] private readonly IServerNotifyManager _notifyManager; + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + + private BoundUserInterface _userInterface; + private ContainerSlot _beakerContainer; + private string _packPrototypeId; + + public bool HasBeaker => _beakerContainer.ContainedEntity != null; + public int DispenseAmount = 10; + + /// + /// 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(ReagentDispenserUiKey.Key); + _userInterface.OnReceiveMessage += OnUiReceiveMessage; + + _beakerContainer = ContainerManagerComponent.Ensure($"{Name}-reagentContainerContainer", Owner); + + InitializeFromPrototype(); + UpdateUserInterface(); + } + + /// + /// Checks to see if the pack defined in this components yaml prototype + /// exists. If so, it fills the reagent inventory list. + /// + private void InitializeFromPrototype() + { + if (string.IsNullOrEmpty(_packPrototypeId)) return; + + var prototypeManager = IoCManager.Resolve(); + if (!prototypeManager.TryIndex(_packPrototypeId, out ReagentDispenserInventoryPrototype packPrototype)) + { + return; + } + + foreach (var entry in packPrototype.Inventory) + { + Inventory.Add(new ReagentDispenserInventoryEntry(entry)); + } + } + + /// + /// 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) + { + var msg = (UiButtonPressedMessage)obj.Message; + switch (msg.Button) + { + case UiButton.Eject: + TryEject(); + break; + case UiButton.Clear: + TryClear(); + break; + case UiButton.SetDispenseAmount1: + DispenseAmount = 1; + break; + case UiButton.SetDispenseAmount5: + DispenseAmount = 5; + break; + case UiButton.SetDispenseAmount10: + DispenseAmount = 10; + break; + case UiButton.SetDispenseAmount25: + DispenseAmount = 25; + break; + case UiButton.SetDispenseAmount50: + DispenseAmount = 50; + break; + case UiButton.SetDispenseAmount100: + DispenseAmount = 100; + break; + case UiButton.Dispense: + if (HasBeaker) + { + TryDispense(msg.DispenseIndex); + } + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Gets component data to be used to update the user interface client-side. + /// + /// Returns a + private ReagentDispenserBoundUserInterfaceState GetUserInterfaceState() + { + var beaker = _beakerContainer.ContainedEntity; + if (beaker == null) + { + return new ReagentDispenserBoundUserInterfaceState(false, 0,0, + "", Inventory, Owner.Name, null); + } + + var solution = beaker.GetComponent(); + return new ReagentDispenserBoundUserInterfaceState(true, solution.CurrentVolume, solution.MaxVolume, + beaker.Name, Inventory, Owner.Name, solution.ReagentList.ToList()); + } + + /// + /// Gets current component data as a and sends it to the client. + /// + private void UpdateUserInterface() + { + var state = GetUserInterfaceState(); + _userInterface.SetState(state); + } + + /// + /// If this component contains an entity with a , eject it. + /// + private void TryEject() + { + if(!HasBeaker) return; + _beakerContainer.Remove(_beakerContainer.ContainedEntity); + + UpdateUserInterface(); + } + + /// + /// If this component contains an entity with a , remove all of it's reagents / solutions. + /// + private void TryClear() + { + if (!HasBeaker) return; + var solution = _beakerContainer.ContainedEntity.GetComponent(); + solution.RemoveAllSolution(); + + UpdateUserInterface(); + } + + /// + /// If this component contains an entity with a , attempt to dispense the specified reagent to it. + /// + /// The index of the reagent in Inventory. + private void TryDispense(int dispenseIndex) + { + if (!HasBeaker) return; + + var solution = _beakerContainer.ContainedEntity.GetComponent(); + solution.TryAddReagent(Inventory[dispenseIndex].ID, DispenseAmount, out int acceptedQuantity); + + 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. + public void 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; + } + + 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 dispenser doesn't already + /// hold a container, it will be added to the dispenser. + /// + /// Data relevant to the event such as the actor which triggered it. + /// + public bool AttackBy(AttackByEventArgs 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 dispenser already has a container in it.")); + } + else if ((solution.Capabilities & SolutionCaps.FitsInDispenser) == 0) + { + //If it can't fit in the dispenser, 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 dispenser.")); + } + else + { + _beakerContainer.Insert(activeHandEntity); + UpdateUserInterface(); + } + } + else + { + _notifyManager.PopupMessage(Owner.Transform.GridPosition, args.User, + _localizationManager.GetString("You can't put this in the dispenser.")); + } + + return true; + } + } +} diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs index 7398bd83c6..195fda0e20 100644 --- a/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/SolutionComponent.cs @@ -1,8 +1,13 @@ using System; +using Content.Server.GameObjects.EntitySystems; using Content.Shared.Chemistry; using Content.Shared.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; namespace Content.Server.GameObjects.Components.Chemistry { @@ -10,8 +15,13 @@ namespace Content.Server.GameObjects.Components.Chemistry /// Shared ECS component that manages a liquid solution of reagents. /// [RegisterComponent] - internal class SolutionComponent : Shared.GameObjects.Components.Chemistry.SolutionComponent + internal class SolutionComponent : Shared.GameObjects.Components.Chemistry.SolutionComponent, IExamine { +#pragma warning disable 649 + [Dependency] private readonly IPrototypeManager _prototypeManager; + [Dependency] private readonly ILocalizationManager _localizationManager; +#pragma warning restore 649 + /// /// Transfers solution from the held container to the target container. /// @@ -76,6 +86,22 @@ namespace Content.Server.GameObjects.Components.Chemistry } } + void IExamine.Examine(FormattedMessage message) + { + message.AddText(_localizationManager.GetString("Contains:\n")); + foreach (var reagent in ReagentList) + { + if (_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto)) + { + message.AddText($"{proto.Name}: {reagent.Quantity}u\n"); + } + else + { + message.AddText(_localizationManager.GetString("Unknown reagent:") + $"{reagent.Quantity}u\n"); + } + } + } + /// /// Transfers solution from a target container to the held container. /// diff --git a/Content.Shared/Chemistry/Solution.cs b/Content.Shared/Chemistry/Solution.cs index 472503b4df..f7530049f6 100644 --- a/Content.Shared/Chemistry/Solution.cs +++ b/Content.Shared/Chemistry/Solution.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using Robust.Shared.Interfaces.Serialization; using Robust.Shared.Serialization; @@ -17,6 +18,7 @@ namespace Content.Shared.Chemistry // Most objects on the station hold only 1 or 2 reagents [ViewVariables] private List _contents = new List(2); + public IReadOnlyList Contents => _contents; /// /// The calculated total volume of all reagents in the solution (ex. Total volume of liquid in beaker). diff --git a/Content.Shared/Chemistry/SolutionCaps.cs b/Content.Shared/Chemistry/SolutionCaps.cs index c5e4edf193..66fbc71801 100644 --- a/Content.Shared/Chemistry/SolutionCaps.cs +++ b/Content.Shared/Chemistry/SolutionCaps.cs @@ -17,5 +17,13 @@ namespace Content.Shared.Chemistry Injector = 4, Injectable = 8, + + /// + /// Allows the container to be placed in a ReagentDispenserComponent. + /// Otherwise it's considered to be too large or the improper shape to fit. + /// Allows us to have obscenely large containers that are harder to abuse in chem dispensers + /// since they can't be placed directly in them. + /// + FitsInDispenser = 16, } } diff --git a/Content.Shared/GameObjects/Components/Chemistry/ReagentDispenserInventoryPrototype.cs b/Content.Shared/GameObjects/Components/Chemistry/ReagentDispenserInventoryPrototype.cs new file mode 100644 index 0000000000..1c5cb64af0 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Chemistry/ReagentDispenserInventoryPrototype.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using YamlDotNet.RepresentationModel; + +namespace Content.Shared.GameObjects.Components.Chemistry +{ + /// + /// Is simply a list of reagents defined in yaml. This can then be set as a + /// s pack value (also in yaml), + /// to define which reagents it's able to dispense. Based off of how vending + /// machines define their inventory. + /// + [Serializable, NetSerializable, Prototype("reagentDispenserInventory")] + public class ReagentDispenserInventoryPrototype : IPrototype, IIndexedPrototype + { + private string _id; + private List _inventory; + + public string ID => _id; + public List Inventory => _inventory; + + public void LoadFrom(YamlMappingNode mapping) + { + var serializer = YamlObjectSerializer.NewReader(mapping); + + serializer.DataField(ref _id, "id", string.Empty); + serializer.DataField(ref _inventory, "inventory", new List()); + } + } +} diff --git a/Content.Shared/GameObjects/Components/Chemistry/SharedReagentDispenserComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SharedReagentDispenserComponent.cs new file mode 100644 index 0000000000..086ab14c8b --- /dev/null +++ b/Content.Shared/GameObjects/Components/Chemistry/SharedReagentDispenserComponent.cs @@ -0,0 +1,110 @@ +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 ReagentDispenserComponent. Provides a way for entities to dispense and remove reagents from other entities with SolutionComponents via a user interface. + /// This is useful for machines such as the chemical dispensers, booze dispensers, or soda dispensers. + /// The chemicals which may be dispensed are defined by specifying a reagent pack. See for more information on that. + /// + public class SharedReagentDispenserComponent : Component + { + public override string Name => "ReagentDispenser"; + + /// + /// A list of reagents which this may dispense. Defined in yaml prototype, see . + /// + public List Inventory = new List(); + + [Serializable, NetSerializable] + public class ReagentDispenserBoundUserInterfaceState : BoundUserInterfaceState + { + public readonly bool HasBeaker; + public readonly int BeakerCurrentVolume; + public readonly int BeakerMaxVolume; + public readonly string ContainerName; + /// + /// A list of the reagents which this dispenser can dispense. + /// + public readonly List Inventory; + /// + /// A list of the reagents and their amounts within the beaker/reagent container, if applicable. + /// + public readonly List ContainerReagents; + public readonly string DispenserName; + + public ReagentDispenserBoundUserInterfaceState(bool hasBeaker, int beakerCurrentVolume, int beakerMaxVolume, string containerName, + List inventory, string dispenserName, List containerReagents) + { + HasBeaker = hasBeaker; + BeakerCurrentVolume = beakerCurrentVolume; + BeakerMaxVolume = beakerMaxVolume; + ContainerName = containerName; + Inventory = inventory; + DispenserName = dispenserName; + ContainerReagents = containerReagents; + } + } + + /// + /// Message data sent from client to server when a dispenser ui button is pressed. + /// + [Serializable, NetSerializable] + public class UiButtonPressedMessage : BoundUserInterfaceMessage + { + public readonly UiButton Button; + public readonly int DispenseIndex; //Index of dispense button / reagent being pressed. Only used when a dispense button is pressed. + + public UiButtonPressedMessage(UiButton button, int dispenseIndex) + { + Button = button; + DispenseIndex = dispenseIndex; + } + } + + [Serializable, NetSerializable] + public enum ReagentDispenserUiKey + { + Key + } + + /// + /// Used in to specify which button was pressed. + /// + public enum UiButton + { + Eject, + Clear, + SetDispenseAmount1, + SetDispenseAmount5, + SetDispenseAmount10, + SetDispenseAmount25, + SetDispenseAmount50, + SetDispenseAmount100, + /// + /// Used when any dispense button is pressed. Such as "Carbon", or "Oxygen" buttons on the chem dispenser. + /// The index of the reagent attached to that dispense button is sent as . + /// + Dispense + } + + /// + /// Information about a reagent which the dispenser can dispense. + /// + [Serializable, NetSerializable] + public class ReagentDispenserInventoryEntry + { + public string ID; + public ReagentDispenserInventoryEntry(string id) + { + ID = id; + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs index 5453fee8f9..cc1df081b9 100644 --- a/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs +++ b/Content.Shared/GameObjects/Components/Chemistry/SolutionComponent.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Content.Shared.Chemistry; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -52,6 +53,8 @@ namespace Content.Shared.GameObjects.Components.Chemistry set => _capabilities = value; } + public IReadOnlyList ReagentList => _containedSolution.Contents; + /// public override string Name => "Solution"; @@ -88,9 +91,26 @@ namespace Content.Shared.GameObjects.Components.Chemistry _containedSolution = new Solution(); } + public void RemoveAllSolution() + { + _containedSolution.RemoveAllSolution(); + } + public bool TryAddReagent(string reagentId, int quantity, out int acceptedQuantity) { - throw new NotImplementedException(); + if (quantity > _maxVolume - _containedSolution.TotalVolume) + { + acceptedQuantity = _maxVolume - _containedSolution.TotalVolume; + if (acceptedQuantity == 0) return false; + } + else + { + acceptedQuantity = quantity; + } + + _containedSolution.AddReagent(reagentId, acceptedQuantity); + RecalculateColor(); + return true; } public bool TryAddSolution(Solution solution) diff --git a/Resources/Prototypes/Entities/buildings/booze_dispenser.yml b/Resources/Prototypes/Entities/buildings/booze_dispenser.yml new file mode 100644 index 0000000000..d6739ad82b --- /dev/null +++ b/Resources/Prototypes/Entities/buildings/booze_dispenser.yml @@ -0,0 +1,37 @@ +- type: entity + id: booze_dispenser + name: Booze Dispenser + description: A booze dispenser with a single slot for a container to be filled. + components: + - type: Sprite + texture: Buildings/booze_dispenser.png + - type: Icon + texture: Buildings/booze_dispenser.png + - type: Clickable + - type: Collidable + shapes: + - !type:PhysShapeAabb + bounds: "-0.4,-0.25,0.4,0.25" + mask: 19 + layer: 16 + IsScrapingFloor: true + - type: Physics + mass: 25 + Anchored: true + - type: SnapGrid + offset: Center + - type: ReagentDispenser + pack: BoozeDispenserInventory + - type: PowerDevice + - type: UserInterface + interfaces: + - key: enum.ReagentDispenserUiKey.Key + type: ReagentDispenserBoundUserInterface + +- type: reagentDispenserInventory + id: BoozeDispenserInventory + inventory: + - chem.Whiskey + - chem.Ale + - chem.Wine + - chem.Ice \ No newline at end of file diff --git a/Resources/Prototypes/Entities/buildings/chem_dispenser.yml b/Resources/Prototypes/Entities/buildings/chem_dispenser.yml new file mode 100644 index 0000000000..7b4c88c45c --- /dev/null +++ b/Resources/Prototypes/Entities/buildings/chem_dispenser.yml @@ -0,0 +1,44 @@ +- type: entity + id: chem_dispenser + name: Chemical Dispenser + description: An industrial grade chemical dispenser with a sizeable chemical supply. + components: + - type: Sprite + texture: Buildings/industrial_dispenser.png + - type: Icon + texture: Buildings/industrial_dispenser.png + - type: Clickable + - type: Collidable + shapes: + - !type:PhysShapeAabb + bounds: "-0.4,-0.25,0.4,0.25" + mask: 19 + layer: 16 + IsScrapingFloor: true + - type: Physics + mass: 25 + Anchored: true + - type: SnapGrid + offset: Center + - type: ReagentDispenser + pack: ChemDispenserStandardInventory + - type: PowerDevice + - type: UserInterface + interfaces: + - key: enum.ReagentDispenserUiKey.Key + type: ReagentDispenserBoundUserInterface + +- type: reagentDispenserInventory + id: ChemDispenserStandardInventory + inventory: + - chem.H2 + - chem.O2 + - chem.S8 + - chem.C + - chem.Cu + - chem.N2 + - chem.Fe + - chem.F2 + - chem.Al + - chem.H2SO4 + - chem.H2O \ No newline at end of file diff --git a/Resources/Prototypes/Entities/buildings/soda_dispenser.yml b/Resources/Prototypes/Entities/buildings/soda_dispenser.yml new file mode 100644 index 0000000000..67570a84a2 --- /dev/null +++ b/Resources/Prototypes/Entities/buildings/soda_dispenser.yml @@ -0,0 +1,38 @@ +- type: entity + id: soda_dispenser + name: Soda Dispenser + description: A beverage dispenser with a selection of soda and several other common beverages. Has a single fill slot for containers. + components: + - type: Sprite + texture: Buildings/soda_dispenser.png + - type: Icon + texture: Buildings/soda_dispenser.png + - type: Clickable + - type: Collidable + shapes: + - !type:PhysShapeAabb + bounds: "-0.4,-0.25,0.4,0.25" + mask: 19 + layer: 16 + IsScrapingFloor: true + - type: Physics + mass: 25 + Anchored: true + - type: SnapGrid + offset: Center + - type: ReagentDispenser + pack: SodaDispenserInventory + - type: PowerDevice + - type: UserInterface + interfaces: + - key: enum.ReagentDispenserUiKey.Key + type: ReagentDispenserBoundUserInterface + +- type: reagentDispenserInventory + id: SodaDispenserInventory + inventory: + - chem.Cola + - chem.Coffee + - chem.Tea + - chem.Ice + - chem.H2O \ No newline at end of file diff --git a/Resources/Prototypes/Entities/items/bartending.yml b/Resources/Prototypes/Entities/items/bartending.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Resources/Prototypes/Entities/items/chemistry.yml b/Resources/Prototypes/Entities/items/chemistry.yml new file mode 100644 index 0000000000..14ac662899 --- /dev/null +++ b/Resources/Prototypes/Entities/items/chemistry.yml @@ -0,0 +1,41 @@ +- type: entity + name: Beaker + parent: BaseItem + description: Used to contain a moderate amount of chemicals and solutions. + id: Beaker + components: + - type: Sprite + texture: Objects/beaker.png + - type: Icon + texture: Objects/beaker.png + - type: Solution + maxVol: 50 + caps: 19 + +- type: entity + name: Large Beaker + parent: BaseItem + description: Used to contain a large amount of chemicals or solutions. + id: LargeBeaker + components: + - type: Sprite + texture: Objects/beakerlarge.png + - type: Icon + texture: Objects/beakerlarge.png + - type: Solution + maxVol: 100 + caps: 19 + +- type: entity + name: Dropper + parent: BaseItem + description: Used to transfer small amounts of chemical solution between containers. + id: Dropper + components: + - type: Sprite + texture: Objects/dropper.png + - type: Icon + texture: Objects/dropper.png + - type: Solution + maxVol: 5 + caps: 19 \ No newline at end of file diff --git a/Resources/Prototypes/Reagents/chemicals.yml b/Resources/Prototypes/Reagents/chemicals.yml index f698560651..3f67e7eac1 100644 --- a/Resources/Prototypes/Reagents/chemicals.yml +++ b/Resources/Prototypes/Reagents/chemicals.yml @@ -6,4 +6,10 @@ - type: reagent id: chem.H2O name: Water - desc: A tasty colorless liquid. \ No newline at end of file + desc: A tasty colorless liquid. + +- type: reagent + id: chem.Ice + name: Ice + desc: Frozen water. + color: "#bed8e6" \ No newline at end of file diff --git a/Resources/Prototypes/Reagents/drinks.yml b/Resources/Prototypes/Reagents/drinks.yml new file mode 100644 index 0000000000..7f5217b21c --- /dev/null +++ b/Resources/Prototypes/Reagents/drinks.yml @@ -0,0 +1,29 @@ +- type: reagent + id: chem.Whiskey + name: Whiskey + desc: An alcoholic beverage made from fermented grain mash + +- type: reagent + id: chem.Ale + name: Ale + desc: A type of beer brewed using a warm fermentation method, resulting in a sweet, full-bodied and fruity taste. + +- type: reagent + id: chem.Wine + name: Wine + desc: An alcoholic drink made from fermented grapes + +- type: reagent + id: chem.Cola + name: Cola + desc: A sweet, carbonated soft drink. Caffeine free. + +- type: reagent + id: chem.Coffee + name: Coffee + desc: A drink made from brewed coffee beans. Contains a moderate amount of caffeine. + +- type: reagent + id: chem.Tea + name: Tea + desc: A made by boiling leaves of the tea tree, Camellia sinensis. \ No newline at end of file diff --git a/Resources/Prototypes/Reagents/elements.yml b/Resources/Prototypes/Reagents/elements.yml index 461a9c4d0a..7a5fd241f7 100644 --- a/Resources/Prototypes/Reagents/elements.yml +++ b/Resources/Prototypes/Reagents/elements.yml @@ -13,4 +13,37 @@ name: Sulfur desc: A yellow, crystalline solid. color: "#FFFACD" - \ No newline at end of file + +- type: reagent + id: chem.C + name: Carbon + desc: A black, crystalline solid. + color: "#22282b" + +- type: reagent + id: chem.Al + name: Aluminum + desc: A silvery-white, soft, non-magnetic, and ductile metal. + color: "#848789" + +- type: reagent + id: chem.Cu + name: Copper + desc: A soft, malleable, and ductile metal with very high thermal and electrical conductivity. + color: "#b05b3c" + +- type: reagent + id: chem.N2 + name: Nitrogen + desc: A colorless, odorless unreactive gas. Highly stable. + +- type: reagent + id: chem.Fe + name: Iron + desc: A silvery-grey metal which forms iron oxides (rust) with contact with air. Commonly alloyed with other elements to create alloys such as steel. + color: "#434b4d" + +- type: reagent + id: chem.F2 + name: Fluorine + desc: A highly toxic pale yellow gas. Extremely reactive. \ No newline at end of file diff --git a/Resources/Textures/Buildings/booze_dispenser.png b/Resources/Textures/Buildings/booze_dispenser.png new file mode 100644 index 0000000000..fdae72dead Binary files /dev/null and b/Resources/Textures/Buildings/booze_dispenser.png differ diff --git a/Resources/Textures/Buildings/industrial_dispenser.png b/Resources/Textures/Buildings/industrial_dispenser.png new file mode 100644 index 0000000000..faf790d248 Binary files /dev/null and b/Resources/Textures/Buildings/industrial_dispenser.png differ diff --git a/Resources/Textures/Buildings/soda_dispenser.png b/Resources/Textures/Buildings/soda_dispenser.png new file mode 100644 index 0000000000..ca7a568661 Binary files /dev/null and b/Resources/Textures/Buildings/soda_dispenser.png differ diff --git a/Resources/Textures/Objects/beaker.png b/Resources/Textures/Objects/beaker.png new file mode 100644 index 0000000000..f97ff00c3d Binary files /dev/null and b/Resources/Textures/Objects/beaker.png differ diff --git a/Resources/Textures/Objects/beakerlarge.png b/Resources/Textures/Objects/beakerlarge.png new file mode 100644 index 0000000000..4e0fb0fc06 Binary files /dev/null and b/Resources/Textures/Objects/beakerlarge.png differ diff --git a/Resources/Textures/Objects/dropper.png b/Resources/Textures/Objects/dropper.png new file mode 100644 index 0000000000..d10f5b6855 Binary files /dev/null and b/Resources/Textures/Objects/dropper.png differ