diff --git a/Content.Client/Construction/ConstructionMenu.xaml.cs b/Content.Client/Construction/ConstructionMenu.xaml.cs index c899923859..f5831b0fce 100644 --- a/Content.Client/Construction/ConstructionMenu.xaml.cs +++ b/Content.Client/Construction/ConstructionMenu.xaml.cs @@ -1,314 +1,134 @@ -#nullable enable using System; -using System.Collections.Generic; -using System.Linq; -using Content.Client.GameObjects.EntitySystems; -using Content.Client.Utility; -using Content.Shared.Construction; -using Content.Shared.GameObjects.Components; -using Content.Shared.GameObjects.Components.Interactable; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; -using Robust.Client.Placement; -using Robust.Client.ResourceManagement; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -using Robust.Client.Utility; -using Robust.Shared.Enums; -using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Maths; -using Robust.Shared.Prototypes; + +#nullable enable namespace Content.Client.Construction { - [GenerateTypedNameReferences] - public partial class ConstructionMenu : SS14Window + /// + /// This is the interface for a UI View of the construction window. The point of it is to abstract away the actual + /// UI controls and just provide higher level operations on the entire window. This View is completely passive and + /// just raises events to the outside world. This class is controlled by the . + /// + public interface IConstructionMenuView : IDisposable { - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IResourceCache _resourceCache = default!; - [Dependency] private readonly IEntitySystemManager _systemManager = default!; - [Dependency] private readonly IPlacementManager _placementManager = default!; + // It isn't optimal to expose UI controls like this, but the UI control design is + // questionable so it can't be helped. + string[] Categories { get; set; } + OptionButton CategoryButton { get; } + bool EraseButtonPressed { get; set; } + bool BuildButtonPressed { get; set; } + + ItemList Recipes { get; } + ItemList RecipeStepList { get; } + + event EventHandler<(string search, string catagory)> PopulateRecipes; + event EventHandler RecipeSelected; + event EventHandler BuildButtonToggled; + event EventHandler EraseButtonToggled; + event EventHandler ClearAllGhosts; + + void ClearRecipeInfo(); + void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem); + void ResetPlacement(); + + #region Window Control + + event Action? OnClose; + + bool IsOpen { get; } + + void OpenCentered(); + void MoveToFront(); + bool IsAtFront(); + void Close(); + + #endregion + } + + [GenerateTypedNameReferences] + public partial class ConstructionMenu : SS14Window, IConstructionMenuView + { protected override Vector2? CustomSize => (720, 320); - private ConstructionPrototype? _selected; - private string[] _categories = Array.Empty(); + public bool BuildButtonPressed + { + get => BuildButton.Pressed; + set => BuildButton.Pressed = value; + } + + public string[] Categories { get; set; } = Array.Empty(); + + public OptionButton CategoryButton => Category; + + public bool EraseButtonPressed + { + get => EraseButton.Pressed; + set => EraseButton.Pressed = value; + } + + /// + public ItemList Recipes => RecipesList; + + public ItemList RecipeStepList => StepList; public ConstructionMenu() { IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - _placementManager.PlacementChanged += PlacementChanged; - Title = Loc.GetString("Construction"); + + BuildButton.Text = Loc.GetString("Place construction ghost"); + RecipesList.OnItemSelected += obj => RecipeSelected?.Invoke(this, obj.ItemList[obj.ItemIndex]); + RecipesList.OnItemDeselected += _ => RecipeSelected?.Invoke(this, null); + + SearchBar.OnTextChanged += _ => PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[Category.SelectedId])); + Category.OnItemSelected += obj => + { + Category.SelectId(obj.Id); + PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[obj.Id])); + }; BuildButton.Text = Loc.GetString("Place construction ghost"); - RecipesList.OnItemSelected += RecipeSelected; - RecipesList.OnItemDeselected += RecipeDeselected; - - SearchBar.OnTextChanged += SearchTextChanged; - Category.OnItemSelected += CategorySelected; - - BuildButton.Text = Loc.GetString("Place construction ghost"); - BuildButton.OnToggled += BuildButtonToggled; + BuildButton.OnToggled += args => BuildButtonToggled?.Invoke(this, args.Pressed); ClearButton.Text = Loc.GetString("Clear All"); - ClearButton.OnPressed += ClearAllButtonPressed; + ClearButton.OnPressed += _ => ClearAllGhosts?.Invoke(this, EventArgs.Empty); EraseButton.Text = Loc.GetString("Eraser Mode"); - EraseButton.OnToggled += EraseButtonToggled; - - PopulateCategories(); - PopulateAll(); + EraseButton.OnToggled += args => EraseButtonToggled?.Invoke(this, args.Pressed); } - private void PlacementChanged(object? sender, EventArgs e) + public event EventHandler? ClearAllGhosts; + + public event EventHandler<(string search, string catagory)>? PopulateRecipes; + public event EventHandler? RecipeSelected; + public event EventHandler? BuildButtonToggled; + public event EventHandler? EraseButtonToggled; + + public void ResetPlacement() { BuildButton.Pressed = false; EraseButton.Pressed = false; } - private void PopulateAll() + public void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem) { - foreach (var recipe in _prototypeManager.EnumeratePrototypes()) - { - RecipesList.Add(GetItem(recipe, RecipesList)); - } - } - - private static ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList) - { - return new(itemList) - { - Metadata = recipe, - Text = recipe.Name, - Icon = recipe.Icon.Frame0(), - TooltipEnabled = true, - TooltipText = recipe.Description, - }; - } - - private void PopulateBy(string search, string category) - { - RecipesList.Clear(); - - foreach (var recipe in _prototypeManager.EnumeratePrototypes()) - { - if (!string.IsNullOrEmpty(search)) - { - if (!recipe.Name.ToLowerInvariant().Contains(search.Trim().ToLowerInvariant())) - continue; - } - - if (!string.IsNullOrEmpty(category) && category != Loc.GetString("All")) - { - if (recipe.Category != category) - continue; - } - - RecipesList.Add(GetItem(recipe, RecipesList)); - } - } - - private void PopulateCategories() - { - var uniqueCategories = new HashSet(); - - // hard-coded to show all recipes - uniqueCategories.Add(Loc.GetString("All")); - - foreach (var prototype in _prototypeManager.EnumeratePrototypes()) - { - var category = Loc.GetString(prototype.Category); - - if (!string.IsNullOrEmpty(category)) - uniqueCategories.Add(category); - } - - Category.Clear(); - - var array = uniqueCategories.ToArray(); - Array.Sort(array); - - for (var i = 0; i < array.Length; i++) - { - var category = array[i]; - Category.AddItem(category, i); - } - - _categories = array; - } - - private void PopulateInfo(ConstructionPrototype prototype) - { - ClearInfo(); - - var isItem = prototype.Type == ConstructionType.Item; - BuildButton.Disabled = false; - BuildButton.Text = Loc.GetString(!isItem ? "Place construction ghost" : "Craft"); - TargetName.SetMessage(prototype.Name); - TargetDesc.SetMessage(prototype.Description); - TargetTexture.Texture = prototype.Icon.Frame0(); - - if (!_prototypeManager.TryIndex(prototype.Graph, out ConstructionGraphPrototype graph)) - return; - - var startNode = graph.Nodes[prototype.StartNode]; - var targetNode = graph.Nodes[prototype.TargetNode]; - - var path = graph.Path(startNode.Name, targetNode.Name); - - var current = startNode; - - var stepNumber = 1; - - Texture? GetTextureForStep(ConstructionGraphStep step) - { - switch (step) - { - case MaterialConstructionGraphStep materialStep: - switch (materialStep.Material) - { - case StackType.Metal: - return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/metal.png"); - - case StackType.Glass: - return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/glass.png"); - - case StackType.Plasteel: - return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/plasteel.png"); - - case StackType.Plasma: - return _resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/plasma.png"); - - case StackType.Cable: - return _resourceCache.GetTexture("/Textures/Objects/Tools/cables.rsi/coil-30.png"); - - case StackType.MetalRod: - return _resourceCache.GetTexture("/Textures/Objects/Materials/materials.rsi/rods.png"); - - } - break; - - case ToolConstructionGraphStep toolStep: - switch (toolStep.Tool) - { - case ToolQuality.Anchoring: - return _resourceCache.GetTexture("/Textures/Objects/Tools/wrench.rsi/icon.png"); - case ToolQuality.Prying: - return _resourceCache.GetTexture("/Textures/Objects/Tools/crowbar.rsi/icon.png"); - case ToolQuality.Screwing: - return _resourceCache.GetTexture("/Textures/Objects/Tools/screwdriver.rsi/screwdriver-map.png"); - case ToolQuality.Cutting: - return _resourceCache.GetTexture("/Textures/Objects/Tools/wirecutters.rsi/cutters-map.png"); - case ToolQuality.Welding: - return _resourceCache.GetTexture("/Textures/Objects/Tools/welder.rsi/welder.png"); - case ToolQuality.Multitool: - return _resourceCache.GetTexture("/Textures/Objects/Tools/multitool.rsi/multitool.png"); - } - - break; - - case ComponentConstructionGraphStep componentStep: - return componentStep.Icon?.Frame0(); - - case PrototypeConstructionGraphStep prototypeStep: - return prototypeStep.Icon?.Frame0(); - - case NestedConstructionGraphStep _: - return null; - } - - return null; - } - - foreach (var node in path) - { - var edge = current.GetEdge(node.Name); - var firstNode = current == startNode; - - if (firstNode) - { - StepList.AddItem(isItem - ? Loc.GetString($"{stepNumber++}. To craft this item, you need:") - : Loc.GetString($"{stepNumber++}. To build this, first you need:")); - } - - foreach (var step in edge.Steps) - { - var icon = GetTextureForStep(step); - - switch (step) - { - case MaterialConstructionGraphStep materialStep: - StepList.AddItem( - !firstNode - ? Loc.GetString( - "{0}. Add {1}x {2}.", stepNumber++, materialStep.Amount, materialStep.Material) - : Loc.GetString(" {0}x {1}", materialStep.Amount, materialStep.Material), icon); - - break; - - case ToolConstructionGraphStep toolStep: - StepList.AddItem(Loc.GetString("{0}. Use a {1}.", stepNumber++, toolStep.Tool.GetToolName()), icon); - break; - - case PrototypeConstructionGraphStep prototypeStep: - StepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, prototypeStep.Name), icon); - break; - - case ComponentConstructionGraphStep componentStep: - StepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, componentStep.Name), icon); - break; - - case NestedConstructionGraphStep nestedStep: - var parallelNumber = 1; - StepList.AddItem(Loc.GetString("{0}. In parallel...", stepNumber++)); - - foreach (var steps in nestedStep.Steps) - { - var subStepNumber = 1; - - foreach (var subStep in steps) - { - icon = GetTextureForStep(subStep); - - switch (subStep) - { - case MaterialConstructionGraphStep materialStep: - if (!isItem) - StepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}x {4}.", stepNumber, parallelNumber, subStepNumber++, materialStep.Amount, materialStep.Material), icon); - break; - - case ToolConstructionGraphStep toolStep: - StepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Use a {3}.", stepNumber, parallelNumber, subStepNumber++, toolStep.Tool.GetToolName()), icon); - break; - - case PrototypeConstructionGraphStep prototypeStep: - StepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, prototypeStep.Name), icon); - break; - - case ComponentConstructionGraphStep componentStep: - StepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, componentStep.Name), icon); - break; - } - } - - parallelNumber++; - } - - break; - } - } - - current = node; - } + BuildButton.Text = Loc.GetString(isItem ? "Place construction ghost" : "Craft"); + TargetName.SetMessage(name); + TargetDesc.SetMessage(description); + TargetTexture.Texture = iconTexture; } - private void ClearInfo() + public void ClearRecipeInfo() { BuildButton.Disabled = true; TargetName.SetMessage(string.Empty); @@ -317,92 +137,12 @@ namespace Content.Client.Construction StepList.Clear(); } - private void RecipeSelected(ItemList.ItemListSelectedEventArgs obj) - { - _selected = (ConstructionPrototype) obj.ItemList[obj.ItemIndex].Metadata!; - if(_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement(); - PopulateInfo(_selected); - } - - private void RecipeDeselected(ItemList.ItemListDeselectedEventArgs obj) - { - _selected = null; - ClearInfo(); - } - - private void CategorySelected(OptionButton.ItemSelectedEventArgs obj) - { - Category.SelectId(obj.Id); - PopulateBy(SearchBar.Text, _categories[obj.Id]); - } - - private void SearchTextChanged(LineEdit.LineEditEventArgs obj) - { - PopulateBy(SearchBar.Text, _categories[Category.SelectedId]); - } - - private void BuildButtonToggled(BaseButton.ButtonToggledEventArgs args) - { - if (args.Pressed) - { - if(_selected == null) return; - - var constructSystem = EntitySystem.Get(); - - if (_selected.Type == ConstructionType.Item) - { - constructSystem.TryStartItemConstruction(_selected.ID); - BuildButton.Pressed = false; - return; - } - - UpdateGhostPlacement(); - } - else - { - _placementManager.Clear(); - } - - BuildButton.Pressed = args.Pressed; - } - - private void UpdateGhostPlacement() - { - if (_selected == null || _selected.Type != ConstructionType.Structure) return; - - var constructSystem = EntitySystem.Get(); - - _placementManager.BeginPlacing(new PlacementInformation() - { - IsTile = false, - PlacementOption = _selected.PlacementMode, - }, new ConstructionPlacementHijack(constructSystem, _selected)); - - BuildButton.Pressed = true; - } - - private void EraseButtonToggled(BaseButton.ButtonToggledEventArgs args) - { - if (args.Pressed) _placementManager.Clear(); - _placementManager.ToggleEraserHijacked(new ConstructionPlacementHijack(_systemManager.GetEntitySystem(), null)); - EraseButton.Pressed = args.Pressed; - } - - private void ClearAllButtonPressed(BaseButton.ButtonEventArgs obj) - { - var constructionSystem = EntitySystem.Get(); - - constructionSystem.ClearAllGhosts(); - } - + /// protected override void Dispose(bool disposing) { base.Dispose(disposing); - if (disposing) - { - _placementManager.PlacementChanged -= PlacementChanged; - } + if (disposing) { } } } } diff --git a/Content.Client/Construction/ConstructionMenuPresenter.cs b/Content.Client/Construction/ConstructionMenuPresenter.cs new file mode 100644 index 0000000000..efff7f3256 --- /dev/null +++ b/Content.Client/Construction/ConstructionMenuPresenter.cs @@ -0,0 +1,505 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Content.Client.GameObjects.EntitySystems; +using Content.Client.UserInterface; +using Content.Client.Utility; +using Content.Shared.Construction; +using Content.Shared.GameObjects.Components; +using Content.Shared.GameObjects.Components.Interactable; +using Robust.Client.Graphics; +using Robust.Client.Placement; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using Robust.Client.Utility; +using Robust.Shared.Enums; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Prototypes; + +namespace Content.Client.Construction +{ + /// + /// This class presents the Construction/Crafting UI to the client, linking the with the + /// model. This is where the bulk of UI work is done, either calling functions in the model to change state, or collecting + /// data out of the model to *present* to the screen though the UI framework. + /// + internal class ConstructionMenuPresenter : IDisposable + { + [Dependency] private readonly IEntitySystemManager _systemManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly IPlacementManager _placementManager = default!; + + private readonly IGameHud _gameHud; + private readonly IConstructionMenuView _constructionView; + + private ConstructionSystem? _constructionSystem; + private ConstructionPrototype? _selected; + + private bool CraftingAvailable + { + get => _gameHud.CraftingButtonVisible; + set + { + _gameHud.CraftingButtonVisible = value; + if (!value) + _constructionView.Close(); + } + } + + /// + /// Does the window have focus? If the window is closed, this will always return false. + /// + private bool IsAtFront => _constructionView.IsOpen && _constructionView.IsAtFront(); + + private bool WindowOpen + { + get => _constructionView.IsOpen; + set + { + if (value && CraftingAvailable) + { + if (_constructionView.IsOpen) + _constructionView.MoveToFront(); + else + _constructionView.OpenCentered(); + } + else + _constructionView.Close(); + } + } + + /// + /// Constructs a new instance of . + /// + /// GUI that is being presented to. + public ConstructionMenuPresenter(IGameHud gameHud) + { + // This is a lot easier than a factory + IoCManager.InjectDependencies(this); + + _gameHud = gameHud; + _constructionView = new ConstructionMenu(); + + // This is required so that if we load after the system is initialized, we can bind to it immediately + if (_systemManager.TryGetEntitySystem(out var constructionSystem)) + SystemBindingChanged(constructionSystem); + + _systemManager.SystemLoaded += OnSystemLoaded; + _systemManager.SystemUnloaded += OnSystemUnloaded; + + _placementManager.PlacementChanged += OnPlacementChanged; + + _constructionView.OnClose += () => _gameHud.CraftingButtonDown = false; + _constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts(); + _constructionView.PopulateRecipes += OnViewPopulateRecipes; + _constructionView.RecipeSelected += OnViewRecipeSelected; + _constructionView.BuildButtonToggled += (_, b) => BuildButtonToggled(b); + _constructionView.EraseButtonToggled += (_, b) => + { + if (_constructionSystem is null) return; + if (b) _placementManager.Clear(); + _placementManager.ToggleEraserHijacked(new ConstructionPlacementHijack(_constructionSystem, null)); + _constructionView.EraseButtonPressed = b; + }; + + PopulateCategories(); + OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty)); + + _gameHud.CraftingButtonToggled += b => WindowOpen = b; + } + + /// + public void Dispose() + { + _constructionView.Dispose(); + + _systemManager.SystemLoaded -= OnSystemLoaded; + _systemManager.SystemUnloaded -= OnSystemUnloaded; + + _placementManager.PlacementChanged -= OnPlacementChanged; + } + + private void OnPlacementChanged(object? sender, EventArgs e) + { + _constructionView.ResetPlacement(); + } + + private void OnViewRecipeSelected(object? sender, ItemList.Item? item) + { + if (item is null) + { + _selected = null; + _constructionView.ClearRecipeInfo(); + return; + } + + _selected = (ConstructionPrototype) item.Metadata!; + if (_placementManager.IsActive && !_placementManager.Eraser) UpdateGhostPlacement(); + PopulateInfo(_selected); + } + + private void OnViewPopulateRecipes(object? sender, (string search, string catagory) args) + { + var (search, category) = args; + var recipesList = _constructionView.Recipes; + + recipesList.Clear(); + + foreach (var recipe in _prototypeManager.EnumeratePrototypes()) + { + if (!string.IsNullOrEmpty(search)) + { + if (!recipe.Name.ToLowerInvariant().Contains(search.Trim().ToLowerInvariant())) + continue; + } + + if (!string.IsNullOrEmpty(category) && category != Loc.GetString("All")) + { + if (recipe.Category != category) + continue; + } + + recipesList.Add(GetItem(recipe, recipesList)); + } + + // There is apparently no way to set which + } + + private void PopulateCategories() + { + var uniqueCategories = new HashSet(); + + // hard-coded to show all recipes + uniqueCategories.Add(Loc.GetString("All")); + + foreach (var prototype in _prototypeManager.EnumeratePrototypes()) + { + var category = Loc.GetString(prototype.Category); + + if (!string.IsNullOrEmpty(category)) + uniqueCategories.Add(category); + } + + _constructionView.CategoryButton.Clear(); + + var array = uniqueCategories.ToArray(); + Array.Sort(array); + + for (var i = 0; i < array.Length; i++) + { + var category = array[i]; + _constructionView.CategoryButton.AddItem(category, i); + } + + _constructionView.Categories = array; + } + + private void PopulateInfo(ConstructionPrototype prototype) + { + _constructionView.ClearRecipeInfo(); + _constructionView.SetRecipeInfo(prototype.Name, prototype.Description, prototype.Icon.Frame0(), prototype.Type != ConstructionType.Item); + + var stepList = _constructionView.RecipeStepList; + GenerateStepList(prototype, stepList); + } + + private void GenerateStepList(ConstructionPrototype prototype, ItemList stepList) + { + if (!_prototypeManager.TryIndex(prototype.Graph, out ConstructionGraphPrototype graph)) + return; + + var startNode = graph.Nodes[prototype.StartNode]; + var targetNode = graph.Nodes[prototype.TargetNode]; + + var path = graph.Path(startNode.Name, targetNode.Name); + + var current = startNode; + + var stepNumber = 1; + + foreach (var node in path) + { + var edge = current.GetEdge(node.Name); + var firstNode = current == startNode; + + if (firstNode) + { + stepList.AddItem(prototype.Type == ConstructionType.Item + ? Loc.GetString($"{stepNumber++}. To craft this item, you need:") + : Loc.GetString($"{stepNumber++}. To build this, first you need:")); + } + + foreach (var step in edge.Steps) + { + var icon = GetTextureForStep(_resourceCache, step); + + switch (step) + { + case MaterialConstructionGraphStep materialStep: + stepList.AddItem( + !firstNode + ? Loc.GetString( + "{0}. Add {1}x {2}.", stepNumber++, materialStep.Amount, materialStep.Material) + : Loc.GetString(" {0}x {1}", materialStep.Amount, materialStep.Material), icon); + + break; + + case ToolConstructionGraphStep toolStep: + stepList.AddItem(Loc.GetString("{0}. Use a {1}.", stepNumber++, toolStep.Tool.GetToolName()), icon); + break; + + case PrototypeConstructionGraphStep prototypeStep: + stepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, prototypeStep.Name), icon); + break; + + case ComponentConstructionGraphStep componentStep: + stepList.AddItem(Loc.GetString("{0}. Add {1}.", stepNumber++, componentStep.Name), icon); + break; + + case NestedConstructionGraphStep nestedStep: + var parallelNumber = 1; + stepList.AddItem(Loc.GetString("{0}. In parallel...", stepNumber++)); + + foreach (var steps in nestedStep.Steps) + { + var subStepNumber = 1; + + foreach (var subStep in steps) + { + icon = GetTextureForStep(_resourceCache, subStep); + + switch (subStep) + { + case MaterialConstructionGraphStep materialStep: + if (!(prototype.Type == ConstructionType.Item)) stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}x {4}.", stepNumber, parallelNumber, subStepNumber++, materialStep.Amount, materialStep.Material), icon); + break; + + case ToolConstructionGraphStep toolStep: + stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Use a {3}.", stepNumber, parallelNumber, subStepNumber++, toolStep.Tool.GetToolName()), icon); + break; + + case PrototypeConstructionGraphStep prototypeStep: + stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, prototypeStep.Name), icon); + break; + + case ComponentConstructionGraphStep componentStep: + stepList.AddItem(Loc.GetString(" {0}.{1}.{2}. Add {3}.", stepNumber, parallelNumber, subStepNumber++, componentStep.Name), icon); + break; + } + } + + parallelNumber++; + } + + break; + } + } + + current = node; + } + } + + private static Texture? GetTextureForStep(IResourceCache resourceCache, ConstructionGraphStep step) + { + switch (step) + { + case MaterialConstructionGraphStep materialStep: + switch (materialStep.Material) + { + case StackType.Metal: + return resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/metal.png"); + + case StackType.Glass: + return resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/glass.png"); + + case StackType.Plasteel: + return resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/plasteel.png"); + + case StackType.Plasma: + return resourceCache.GetTexture("/Textures/Objects/Materials/sheets.rsi/phoron.png"); + + case StackType.Cable: + return resourceCache.GetTexture("/Textures/Objects/Tools/cables.rsi/coil-30.png"); + + case StackType.MetalRod: + return resourceCache.GetTexture("/Textures/Objects/Materials/materials.rsi/rods.png"); + } + + break; + + case ToolConstructionGraphStep toolStep: + switch (toolStep.Tool) + { + case ToolQuality.Anchoring: + return resourceCache.GetTexture("/Textures/Objects/Tools/wrench.rsi/icon.png"); + case ToolQuality.Prying: + return resourceCache.GetTexture("/Textures/Objects/Tools/crowbar.rsi/icon.png"); + case ToolQuality.Screwing: + return resourceCache.GetTexture("/Textures/Objects/Tools/screwdriver.rsi/screwdriver-map.png"); + case ToolQuality.Cutting: + return resourceCache.GetTexture("/Textures/Objects/Tools/wirecutters.rsi/cutters-map.png"); + case ToolQuality.Welding: + return resourceCache.GetTexture("/Textures/Objects/Tools/welder.rsi/welder.png"); + case ToolQuality.Multitool: + return resourceCache.GetTexture("/Textures/Objects/Tools/multitool.rsi/multitool.png"); + } + + break; + + case ComponentConstructionGraphStep componentStep: + return componentStep.Icon?.Frame0(); + + case PrototypeConstructionGraphStep prototypeStep: + return prototypeStep.Icon?.Frame0(); + + case NestedConstructionGraphStep: + return null; + } + + return null; + } + + private static ItemList.Item GetItem(ConstructionPrototype recipe, ItemList itemList) + { + return new(itemList) + { + Metadata = recipe, + Text = recipe.Name, + Icon = recipe.Icon.Frame0(), + TooltipEnabled = true, + TooltipText = recipe.Description + }; + } + + private void BuildButtonToggled(bool pressed) + { + if (pressed) + { + if (_selected == null) return; + + // not bound to a construction system + if (_constructionSystem is null) + { + _constructionView.BuildButtonPressed = false; + return; + } + + if (_selected.Type == ConstructionType.Item) + { + _constructionSystem.TryStartItemConstruction(_selected.ID); + _constructionView.BuildButtonPressed = false; + return; + } + + _placementManager.BeginPlacing(new PlacementInformation + { + IsTile = false, + PlacementOption = _selected.PlacementMode + }, new ConstructionPlacementHijack(_constructionSystem, _selected)); + + UpdateGhostPlacement(); + } + else + _placementManager.Clear(); + + _constructionView.BuildButtonPressed = pressed; + } + + private void UpdateGhostPlacement() + { + if (_selected == null || _selected.Type != ConstructionType.Structure) return; + + var constructSystem = EntitySystem.Get(); + + _placementManager.BeginPlacing(new PlacementInformation() + { + IsTile = false, + PlacementOption = _selected.PlacementMode, + }, new ConstructionPlacementHijack(constructSystem, _selected)); + + _constructionView.BuildButtonPressed = true; + } + + private void OnSystemLoaded(object? sender, SystemChangedArgs args) + { + if (args.System is ConstructionSystem system) SystemBindingChanged(system); + } + + private void OnSystemUnloaded(object? sender, SystemChangedArgs args) + { + if (args.System is ConstructionSystem) SystemBindingChanged(null); + } + + private void SystemBindingChanged(ConstructionSystem? newSystem) + { + if (newSystem is null) + { + if (_constructionSystem is null) + return; + + UnbindFromSystem(); + } + else + { + if (_constructionSystem is null) + { + BindToSystem(newSystem); + return; + } + + UnbindFromSystem(); + BindToSystem(newSystem); + } + } + + private void BindToSystem(ConstructionSystem system) + { + _constructionSystem = system; + system.ToggleCraftingWindow += SystemOnToggleMenu; + system.CraftingAvailabilityChanged += SystemCraftingAvailabilityChanged; + } + + private void UnbindFromSystem() + { + var system = _constructionSystem; + + if (system is null) + throw new InvalidOperationException(); + + system.ToggleCraftingWindow -= SystemOnToggleMenu; + system.CraftingAvailabilityChanged -= SystemCraftingAvailabilityChanged; + _constructionSystem = null; + } + + private void SystemCraftingAvailabilityChanged(object? sender, CraftingAvailabilityChangedArgs e) + { + CraftingAvailable = e.Available; + } + + private void SystemOnToggleMenu(object? sender, EventArgs eventArgs) + { + if (!CraftingAvailable) + return; + + if (WindowOpen) + { + if (IsAtFront) + { + WindowOpen = false; + _gameHud.CraftingButtonDown = false; // This does not call CraftingButtonToggled + } + else + _constructionView.MoveToFront(); + } + else + { + WindowOpen = true; + _gameHud.CraftingButtonDown = true; // This does not call CraftingButtonToggled + } + } + } +} diff --git a/Content.Client/GameObjects/EntitySystems/ConstructionSystem.cs b/Content.Client/GameObjects/EntitySystems/ConstructionSystem.cs index b4b688a3f4..2e429d9927 100644 --- a/Content.Client/GameObjects/EntitySystems/ConstructionSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/ConstructionSystem.cs @@ -1,7 +1,6 @@ -using System.Collections.Generic; -using Content.Client.Construction; +using System; +using System.Collections.Generic; using Content.Client.GameObjects.Components.Construction; -using Content.Client.UserInterface; using Content.Shared.Construction; using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Input; @@ -9,12 +8,15 @@ using Content.Shared.Utility; using JetBrains.Annotations; using Robust.Client.GameObjects; using Robust.Client.Player; +using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.Input.Binding; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; +#nullable enable + namespace Content.Client.GameObjects.EntitySystems { /// @@ -23,12 +25,12 @@ namespace Content.Client.GameObjects.EntitySystems [UsedImplicitly] public class ConstructionSystem : SharedConstructionSystem { - [Dependency] private readonly IGameHud _gameHud = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + private readonly Dictionary _ghosts = new(); private int _nextId; - private readonly Dictionary _ghosts = new(); - private ConstructionMenu _constructionMenu; + + private bool CraftingEnabled { get; set; } /// public override void Initialize() @@ -46,6 +48,16 @@ namespace Content.Client.GameObjects.EntitySystems .Register(); } + /// + public override void Shutdown() + { + CommandBinds.Unregister(); + base.Shutdown(); + } + + public event EventHandler? CraftingAvailabilityChanged; + public event EventHandler? ToggleCraftingWindow; + private void HandleAckStructure(AckStructureConstructionMessage msg) { ClearGhost(msg.GhostId); @@ -53,66 +65,32 @@ namespace Content.Client.GameObjects.EntitySystems private void HandlePlayerAttached(PlayerAttachSysMessage msg) { - if (msg.AttachedEntity == null) - { - _gameHud.CraftingButtonVisible = false; - return; - } - - if (_constructionMenu == null) - { - _constructionMenu = new ConstructionMenu(); - _constructionMenu.OnClose += () => _gameHud.CraftingButtonDown = false; - } - - _gameHud.CraftingButtonVisible = true; - _gameHud.CraftingButtonToggled = b => - { - if (b) - { - _constructionMenu.Open(); - } - else - { - _constructionMenu.Close(); - } - }; - } - - /// - public override void Shutdown() - { - _constructionMenu?.Dispose(); - - CommandBinds.Unregister(); - base.Shutdown(); + var available = IsCrafingAvailable(msg.AttachedEntity); + UpdateCraftingAvailability(available); } private bool HandleOpenCraftingMenu(in PointerInputCmdHandler.PointerInputCmdArgs args) { - if (_playerManager.LocalPlayer.ControlledEntity == null) - { + if (args.State == BoundKeyState.Down) + ToggleCraftingWindow?.Invoke(this, EventArgs.Empty); + return true; + } + + private void UpdateCraftingAvailability(bool available) + { + if (CraftingEnabled == available) + return; + + CraftingAvailabilityChanged?.Invoke(this, new CraftingAvailabilityChangedArgs(available)); + CraftingEnabled = available; + } + + private static bool IsCrafingAvailable(IEntity? entity) + { + if (entity == null) return false; - } - - var menu = _constructionMenu; - - if (menu.IsOpen) - { - if (menu.IsAtFront()) - { - SetOpenValue(menu, false); - } - else - { - menu.MoveToFront(); - } - } - else - { - SetOpenValue(menu, true); - } + // TODO: Decide if entity can craft, using capabilities or something return true; } @@ -123,26 +101,11 @@ namespace Content.Client.GameObjects.EntitySystems var entity = EntityManager.GetEntity(args.EntityUid); - if (!entity.TryGetComponent(out ConstructionGhostComponent ghostComp)) + if (!entity.TryGetComponent(out var ghostComp)) return false; TryStartConstruction(ghostComp.GhostID); return true; - - } - - private void SetOpenValue(ConstructionMenu menu, bool value) - { - if (value) - { - _gameHud.CraftingButtonDown = true; - menu.OpenCentered(); - } - else - { - _gameHud.CraftingButtonDown = false; - menu.Close(); - } } /// @@ -153,10 +116,7 @@ namespace Content.Client.GameObjects.EntitySystems var user = _playerManager.LocalPlayer?.ControlledEntity; // This InRangeUnobstructed should probably be replaced with "is there something blocking us in that tile?" - if (user == null || GhostPresent(loc) || !user.InRangeUnobstructed(loc, 20f, ignoreInsideBlocker:prototype.CanBuildInImpassable)) - { - return; - } + if (user == null || GhostPresent(loc) || !user.InRangeUnobstructed(loc, 20f, ignoreInsideBlocker: prototype.CanBuildInImpassable)) return; foreach (var condition in prototype.Conditions) { @@ -185,10 +145,7 @@ namespace Content.Client.GameObjects.EntitySystems { foreach (var ghost in _ghosts) { - if (ghost.Value.Owner.Transform.Coordinates.Equals(loc)) - { - return true; - } + if (ghost.Value.Owner.Transform.Coordinates.Equals(loc)) return true; } return false; @@ -211,7 +168,7 @@ namespace Content.Client.GameObjects.EntitySystems } /// - /// Removes a construction ghost entity with the given ID. + /// Removes a construction ghost entity with the given ID. /// public void ClearGhost(int ghostId) { @@ -223,7 +180,7 @@ namespace Content.Client.GameObjects.EntitySystems } /// - /// Removes all construction ghosts. + /// Removes all construction ghosts. /// public void ClearAllGhosts() { @@ -235,4 +192,14 @@ namespace Content.Client.GameObjects.EntitySystems _ghosts.Clear(); } } + + public class CraftingAvailabilityChangedArgs : EventArgs + { + public bool Available { get; } + + public CraftingAvailabilityChangedArgs(bool available) + { + Available = available; + } + } } diff --git a/Content.Client/State/GameScreen.cs b/Content.Client/State/GameScreen.cs index dbbc4a2ab3..fcccc3693b 100644 --- a/Content.Client/State/GameScreen.cs +++ b/Content.Client/State/GameScreen.cs @@ -1,5 +1,7 @@ +#nullable enable using Content.Client.Administration; using Content.Client.Chat; +using Content.Client.Construction; using Content.Client.Interfaces.Chat; using Content.Client.UserInterface; using Content.Client.Voting; @@ -26,7 +28,8 @@ namespace Content.Client.State [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IClientAdminManager _adminManager = default!; - [ViewVariables] private ChatBox _gameChat; + [ViewVariables] private ChatBox? _gameChat; + private ConstructionMenuPresenter? _constructionMenu; private bool _oocEnabled; private bool _adminOocEnabled; @@ -61,16 +64,38 @@ namespace Content.Client.State _configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true); _configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true); _adminManager.AdminStatusUpdated += OnAdminStatusUpdated; + + SetupPresenters(); } public override void Shutdown() { + DisposePresenters(); + base.Shutdown(); - _gameChat.Dispose(); + _gameChat?.Dispose(); _gameHud.RootControl.Orphan(); + } + /// + /// All UI Presenters should be constructed in here. + /// + private void SetupPresenters() + { + _constructionMenu = new ConstructionMenuPresenter(_gameHud); + } + + /// + /// All UI Presenters should be disposed in here. + /// + private void DisposePresenters() + { + _constructionMenu?.Dispose(); + } + + private void OnOocEnabledChanged(bool val) { _oocEnabled = val; @@ -80,6 +105,9 @@ namespace Content.Client.State return; } + if(_gameChat is null) + return; + _gameChat.Input.PlaceHolder = Loc.GetString(_oocEnabled ? "Say something! [ for OOC" : "Say something!"); } @@ -92,11 +120,17 @@ namespace Content.Client.State return; } + if (_gameChat is null) + return; + _gameChat.Input.PlaceHolder = Loc.GetString(_adminOocEnabled ? "Say something! [ for OOC" : "Say something!"); } private void OnAdminStatusUpdated() { + if (_gameChat is null) + return; + _gameChat.Input.PlaceHolder = _adminManager.IsActive() ? Loc.GetString(_adminOocEnabled ? "Say something! [ for OOC" : "Say something!") : Loc.GetString(_oocEnabled ? "Say something! [ for OOC" : "Say something!");