From 0809f86ca9495613aa9cb2f9eeb66bb3e323c6ae Mon Sep 17 00:00:00 2001 From: Ritchie Frodomar Date: Sun, 4 Aug 2024 22:59:35 -0400 Subject: [PATCH] Implement skeletal crafting UI Signed-off-by: Ritchie Frodomar --- .../VisualStyles/FallbackVisualStyle.cs | 9 + src/AcidicGUI/VisualStyles/IVisualStyle.cs | 3 + src/AcidicGUI/Widgets/ProgressBar.cs | 29 ++ src/AcidicGUI/Widgets/TablePanel.cs | 128 +++++++ .../Core/ISkillTree.cs | 13 + src/SociallyDistant.Framework/Core/IWorld.cs | 1 + .../Core/WorldData/Data/WorldInventoryItem.cs | 43 +++ .../Core/WorldRevision.cs | 1 + .../OS/Tasks/CommandAttribute.cs | 2 + .../UI/Recycling/LabelWidget.cs | 40 ++- .../UI/Recycling/ListItemWidgetController.cs | 11 + .../SociallyDistantVisualStyle.cs | 9 + src/SociallyDistant/CommandAsset.cs | 2 + .../Commands/Cheats/CodingInTheNameOf.cs | 27 ++ .../Commands/Cheats/KnowledgeIsPower.cs | 27 ++ .../Core/WorldData/LedgerEntry.cs | 1 + src/SociallyDistant/Core/WorldData/World.cs | 8 +- src/SociallyDistant/Recipes/DataScraper.cs | 28 ++ .../Crafting/CraftingProgramController.cs | 325 +++++++++++++++++- .../UI/Tools/Crafting/CraftingToolProvider.cs | 1 + 20 files changed, 700 insertions(+), 8 deletions(-) create mode 100644 src/AcidicGUI/Widgets/ProgressBar.cs create mode 100644 src/AcidicGUI/Widgets/TablePanel.cs create mode 100644 src/SociallyDistant.Framework/Core/WorldData/Data/WorldInventoryItem.cs create mode 100644 src/SociallyDistant/Commands/Cheats/CodingInTheNameOf.cs create mode 100644 src/SociallyDistant/Commands/Cheats/KnowledgeIsPower.cs create mode 100644 src/SociallyDistant/Recipes/DataScraper.cs diff --git a/src/AcidicGUI/VisualStyles/FallbackVisualStyle.cs b/src/AcidicGUI/VisualStyles/FallbackVisualStyle.cs index ba1805b0..e7417ce0 100644 --- a/src/AcidicGUI/VisualStyles/FallbackVisualStyle.cs +++ b/src/AcidicGUI/VisualStyles/FallbackVisualStyle.cs @@ -8,6 +8,7 @@ namespace AcidicGUI.VisualStyles; internal sealed class FallbackVisualStyle : IVisualStyle { + public int ProgressBarHeight => 6; public int SliderThickness => 12; public Point ToggleSize => new Point(18, 18); public Point SwitchSize => ToggleSize; @@ -54,6 +55,14 @@ internal sealed class FallbackVisualStyle : IVisualStyle geometry.AddQuad(new LayoutRect(scrollBarArea.Left, scrollBarArea.Top + barOffset, scrollBarArea.Width, barHeight), Color.White); } + public void DrawProgressBar(Widget widget, GeometryHelper geometry, float fillPercentage) + { + geometry.AddQuad(widget.ContentArea, Color.DimGray); + + var fillWidth = (int)MathHelper.Lerp(0, widget.ContentArea.Width, fillPercentage); + geometry.AddQuad(new LayoutRect(widget.ContentArea.Left, widget.ContentArea.Top, fillWidth, widget.ContentArea.Height), Color.Red); + } + public void DrawToggle( Toggle toggle, GeometryHelper geometry, diff --git a/src/AcidicGUI/VisualStyles/IVisualStyle.cs b/src/AcidicGUI/VisualStyles/IVisualStyle.cs index b3875999..6b0736a4 100644 --- a/src/AcidicGUI/VisualStyles/IVisualStyle.cs +++ b/src/AcidicGUI/VisualStyles/IVisualStyle.cs @@ -8,6 +8,7 @@ namespace AcidicGUI.VisualStyles; public interface IVisualStyle : IFontFamilyProvider { + int ProgressBarHeight { get; } int SliderThickness { get; } Point ToggleSize { get; } Point SwitchSize { get; } @@ -41,6 +42,8 @@ public interface IVisualStyle : IFontFamilyProvider bool isChecked ); + void DrawProgressBar(Widget widget, GeometryHelper geometry, float fillPercentage); + void DrawToggleSwitch( Toggle toggle, GeometryHelper geometry, diff --git a/src/AcidicGUI/Widgets/ProgressBar.cs b/src/AcidicGUI/Widgets/ProgressBar.cs new file mode 100644 index 00000000..7f4f899a --- /dev/null +++ b/src/AcidicGUI/Widgets/ProgressBar.cs @@ -0,0 +1,29 @@ +using AcidicGUI.Rendering; +using Microsoft.Xna.Framework; + +namespace AcidicGUI.Widgets; + +public sealed class ProgressBar : Widget +{ + private float fillPercentage = 0; + + public float Value + { + get => fillPercentage; + set + { + fillPercentage = MathHelper.Clamp(value, 0, 1); + InvalidateGeometry(); + } + } + + protected override Point GetContentSize(Point availableSize) + { + return new Point(availableSize.X, GetVisualStyle().ProgressBarHeight); + } + + protected override void RebuildGeometry(GeometryHelper geometry) + { + GetVisualStyle().DrawProgressBar(this, geometry, fillPercentage); + } +} \ No newline at end of file diff --git a/src/AcidicGUI/Widgets/TablePanel.cs b/src/AcidicGUI/Widgets/TablePanel.cs new file mode 100644 index 00000000..2cd3a230 --- /dev/null +++ b/src/AcidicGUI/Widgets/TablePanel.cs @@ -0,0 +1,128 @@ +using AcidicGUI.Layout; +using Microsoft.Xna.Framework; + +namespace AcidicGUI.Widgets; + +public sealed class TablePanel : ContainerWidget +{ + private int columnCount = 3; + private int rowSpacing = 0; + private int columnSpacing = 0; + private int[] columnSizesCache = Array.Empty(); + + public int ColumnCount + { + get => columnCount; + set + { + if (value < 1) + throw new InvalidOperationException("A table must have at least one column."); + + columnCount = value; + InvalidateLayout(); + } + } + + public int ColumnSpacing + { + get => columnSpacing; + set + { + columnSpacing = value; + InvalidateLayout(); + } + } + + public int RowSpacing + { + get => rowSpacing; + set + { + rowSpacing = value; + InvalidateLayout(); + } + } + + protected override unsafe Point GetContentSize(Point availableSize) + { + if (Children.Count == 0) + return Point.Zero; + + Span columnSizes = stackalloc int[columnCount]; + int maxColumnSize = availableSize.X - (columnSpacing * columnCount - 1) / columnCount; + int height = 0; + int rowHeight = 0; + int lastRow = 0; + + for (int i = 0; i < Children.Count; i++) + { + var columnIndex = i % columnCount; + var row = i / columnCount; + var child = Children[i]; + + if (row != lastRow) + { + lastRow = row; + height += rowHeight; + rowHeight = 0; + } + + int availableHeight = availableSize.Y - height; + + var widgetSize = child.GetCachedContentSize(new Point(maxColumnSize, availableHeight)); + + columnSizes[columnIndex] = Math.Max(columnSizes[columnIndex], widgetSize.X); + rowHeight = Math.Max(rowHeight, widgetSize.Y); + } + + if (rowHeight > 0) + { + height += rowHeight; + rowHeight = 0; + } + + var columnsWidth = columnSpacing * (columnCount - 1); + for (var i = 0; i < columnCount; i++) + { + columnsWidth += columnSizes[i]; + } + + columnSizesCache = columnSizes.ToArray(); + return new Point(columnsWidth, height + rowSpacing * lastRow); + } + + protected override void ArrangeChildren(IGuiContext context, LayoutRect availableSpace) + { + if (Children.Count == 0) + return; + + var rowCount = Math.Max(1, Children.Count / columnCount); + var rowSpace = (availableSpace.Height - rowSpacing * (rowCount - 1)) / rowCount; + int yStart = availableSpace.Top; + int xStart = availableSpace.Left; + int lastRow = 0; + int rowHeight = 0; + + for (var i = 0; i < Children.Count; i++) + { + var columnIndex = i % columnCount; + var rowIndex = i / columnCount; + var child = Children[i]; + + if (rowIndex != lastRow) + { + lastRow = rowIndex; + yStart += rowHeight + rowSpacing; + xStart = availableSpace.Left; + rowHeight = 0; + } + + var childSize = child.GetCachedContentSize(new Point(columnSizesCache[columnIndex], rowSpace)); + + child.UpdateLayout(context, new LayoutRect(xStart, yStart, childSize.X, rowSpace)); + + rowHeight = Math.Max(rowHeight, childSize.Y); + xStart += columnSizesCache[columnIndex] + columnSpacing; + } + } +} \ No newline at end of file diff --git a/src/SociallyDistant.Framework/Core/ISkillTree.cs b/src/SociallyDistant.Framework/Core/ISkillTree.cs index a2e35ce4..2f58294f 100644 --- a/src/SociallyDistant.Framework/Core/ISkillTree.cs +++ b/src/SociallyDistant.Framework/Core/ISkillTree.cs @@ -8,9 +8,22 @@ namespace SociallyDistant.Core.Core } + public enum RecipeCategory + { + Components, + Exploits, + Payloads, + Attacks, + Daemons, + ShellExtensions + } + public abstract class CraftingRecipe : IGameContent { + public abstract RecipeCategory Category { get; } public abstract string Id { get; } + public abstract string Description { get; } + public abstract string Title { get; } public abstract IEnumerable RequiredIngredients { get; } diff --git a/src/SociallyDistant.Framework/Core/IWorld.cs b/src/SociallyDistant.Framework/Core/IWorld.cs index db1c24ec..f2b9cd9a 100644 --- a/src/SociallyDistant.Framework/Core/IWorld.cs +++ b/src/SociallyDistant.Framework/Core/IWorld.cs @@ -16,6 +16,7 @@ namespace SociallyDistant.Core.Core INarrativeObjectTable Computers { get; } INarrativeObjectTable InternetProviders { get; } INarrativeObjectTable LocalAreaNetworks { get; } + IWorldTable Inventory { get; } IWorldTable NetworkConnections { get; } IWorldTable PortForwardingRules { get; } IWorldTable CraftedExploits { get; } diff --git a/src/SociallyDistant.Framework/Core/WorldData/Data/WorldInventoryItem.cs b/src/SociallyDistant.Framework/Core/WorldData/Data/WorldInventoryItem.cs new file mode 100644 index 00000000..57b1386e --- /dev/null +++ b/src/SociallyDistant.Framework/Core/WorldData/Data/WorldInventoryItem.cs @@ -0,0 +1,43 @@ +using SociallyDistant.Core.Core.Serialization; + +namespace SociallyDistant.Core.Core.WorldData.Data; + +public struct WorldInventoryItem : IWorldData, IDataWithId +{ + private ObjectId id; + private ObjectId userId; + private string recipeId; + private int quantity; + + public ObjectId InstanceId + { + get => id; + set => id = value; + } + + public ObjectId Owner + { + get => userId; + set => userId = value; + } + + public string RecipeId + { + get => recipeId; + set => recipeId = value; + } + + public int Quantity + { + get => quantity; + set => quantity = value; + } + + public void Serialize(IWorldSerializer serializer) + { + SerializationUtility.SerializeAtRevision(ref id, serializer, WorldRevision.Inventory, default); + SerializationUtility.SerializeAtRevision(ref userId, serializer, WorldRevision.Inventory, default); + SerializationUtility.SerializeAtRevision(ref recipeId, serializer, WorldRevision.Inventory, string.Empty); + SerializationUtility.SerializeAtRevision(ref quantity, serializer, WorldRevision.Inventory, default); + } +} \ No newline at end of file diff --git a/src/SociallyDistant.Framework/Core/WorldRevision.cs b/src/SociallyDistant.Framework/Core/WorldRevision.cs index 7a582b09..b8d0b32e 100644 --- a/src/SociallyDistant.Framework/Core/WorldRevision.cs +++ b/src/SociallyDistant.Framework/Core/WorldRevision.cs @@ -33,6 +33,7 @@ SubDocuments = 28, Checkpoints = 29, IngredientLedger = 30, + Inventory=31, Latest } diff --git a/src/SociallyDistant.Framework/OS/Tasks/CommandAttribute.cs b/src/SociallyDistant.Framework/OS/Tasks/CommandAttribute.cs index cf5365e6..4dd0cd16 100644 --- a/src/SociallyDistant.Framework/OS/Tasks/CommandAttribute.cs +++ b/src/SociallyDistant.Framework/OS/Tasks/CommandAttribute.cs @@ -7,6 +7,8 @@ public sealed class CommandAttribute : Attribute public string Name => name; + public bool Cheat { get; set; } + public CommandAttribute(string name) { this.name = name; diff --git a/src/SociallyDistant.Framework/UI/Recycling/LabelWidget.cs b/src/SociallyDistant.Framework/UI/Recycling/LabelWidget.cs index 2ee23d57..c419fb6a 100644 --- a/src/SociallyDistant.Framework/UI/Recycling/LabelWidget.cs +++ b/src/SociallyDistant.Framework/UI/Recycling/LabelWidget.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations.Schema; +using AcidicGUI.CustomProperties; using AcidicGUI.Layout; using AcidicGUI.ListAdapters; using AcidicGUI.Widgets; @@ -7,12 +9,13 @@ namespace SociallyDistant.Core.UI.Recycling; public sealed class TwoLineListItemWithIcon : Widget { - private readonly ListItem root = new(); - private readonly StackPanel stack = new(); - private readonly Box icon = new(); - private readonly StackPanel lines = new(); - private readonly TextWidget line1 = new(); - private readonly TextWidget line2 = new(); + private readonly ListItem root = new(); + private readonly FlexPanel stack = new(); + private readonly Box icon = new(); + private readonly StackPanel lines = new(); + private readonly TextWidget line1 = new(); + private readonly TextWidget line2 = new(); + private readonly Emblem emblem = new(); public string Line1 { @@ -40,8 +43,32 @@ public sealed class TwoLineListItemWithIcon : Widget set => root.ClickCallback = value; } + public bool ShowEmblem + { + get => emblem.Visibility == Visibility.Visible; + set + { + if (value) + emblem.Visibility = Visibility.Visible; + else + emblem.Visibility = Visibility.Collapsed; + } + } + + public string EmblemText + { + get => emblem.Text; + set => emblem.Text = value; + } + public TwoLineListItemWithIcon() { + emblem.Visibility = Visibility.Collapsed; + + lines.GetCustomProperties().Mode = FlexMode.Proportional; + + emblem.VerticalAlignment = VerticalAlignment.Middle; + line1.WordWrapping = true; line2.WordWrapping = true; line1.UseMarkup = true; @@ -57,6 +84,7 @@ public sealed class TwoLineListItemWithIcon : Widget stack.ChildWidgets.Add(lines); lines.ChildWidgets.Add(line1); lines.ChildWidgets.Add(line2); + stack.ChildWidgets.Add(emblem); } } diff --git a/src/SociallyDistant.Framework/UI/Recycling/ListItemWidgetController.cs b/src/SociallyDistant.Framework/UI/Recycling/ListItemWidgetController.cs index 61dc2f5c..4388e670 100644 --- a/src/SociallyDistant.Framework/UI/Recycling/ListItemWidgetController.cs +++ b/src/SociallyDistant.Framework/UI/Recycling/ListItemWidgetController.cs @@ -18,6 +18,17 @@ public sealed class ListItemWidgetController : RecyclableWidgetController { listItem = GetWidget(); + if (typeof(T) == typeof(int)) + { + listItem.ShowEmblem = true; + listItem.EmblemText = Data!.ToString() ?? "0"; + } + else + { + listItem.ShowEmblem = false; + listItem.EmblemText = string.Empty; + } + if (Image != null) { Image.Build(listItem.Icon); diff --git a/src/SociallyDistant.Framework/UI/VisualStyles/SociallyDistantVisualStyle.cs b/src/SociallyDistant.Framework/UI/VisualStyles/SociallyDistantVisualStyle.cs index 261585ac..6d5b087a 100644 --- a/src/SociallyDistant.Framework/UI/VisualStyles/SociallyDistantVisualStyle.cs +++ b/src/SociallyDistant.Framework/UI/VisualStyles/SociallyDistantVisualStyle.cs @@ -53,6 +53,7 @@ public class SociallyDistantVisualStyle : IVisualStyle private IFontFamily monospace = null!; private Texture2D? checkboxEmblem; + public int ProgressBarHeight => 6; public int SliderThickness => 18; public Point ToggleSize => new Point(20, 20); public Point SwitchSize => new Point(40, 22); @@ -485,6 +486,14 @@ public class SociallyDistantVisualStyle : IVisualStyle } } + public void DrawProgressBar(Widget widget, GeometryHelper geometry, float fillPercentage) + { + geometry.AddRoundedRectangleOutline(widget.ContentArea, 1, 6, accentCyberspace); + + var fillWidth = (int)MathHelper.Lerp(0, widget.ContentArea.Width, fillPercentage); + geometry.AddRoundedRectangle(new LayoutRect(widget.ContentArea.Left, widget.ContentArea.Top, fillWidth, widget.ContentArea.Height), 6, accentCyberspace * 0.5f); + } + public void DrawToggleSwitch( Toggle toggle, GeometryHelper geometry, diff --git a/src/SociallyDistant/CommandAsset.cs b/src/SociallyDistant/CommandAsset.cs index a3875722..ef9ae720 100644 --- a/src/SociallyDistant/CommandAsset.cs +++ b/src/SociallyDistant/CommandAsset.cs @@ -13,6 +13,8 @@ internal sealed class CommandAsset : private readonly CommandAttribute attribute; private readonly Func constructor; + public bool IsCheat => attribute.Cheat; + public string Name => attribute.Name; public CommandAsset(IGameContext context, CommandAttribute attribute, Func constructor) diff --git a/src/SociallyDistant/Commands/Cheats/CodingInTheNameOf.cs b/src/SociallyDistant/Commands/Cheats/CodingInTheNameOf.cs new file mode 100644 index 00000000..64d5269f --- /dev/null +++ b/src/SociallyDistant/Commands/Cheats/CodingInTheNameOf.cs @@ -0,0 +1,27 @@ +using SociallyDistant.Architecture; +using SociallyDistant.Core; +using SociallyDistant.Core.Core; +using SociallyDistant.Core.Modules; +using SociallyDistant.Core.OS.Tasks; +using SociallyDistant.Core.WorldData; + +namespace SociallyDistant.Commands.Cheats; + +[Command("codinginthenameof", Cheat = true)] +public class CodingInTheNameOf : ScriptableCommand +{ + public CodingInTheNameOf(IGameContext gameContext) : base(gameContext) + { + } + + protected override Task OnExecute() + { + var ledger = WorldManager.Instance.World.Ledger; + if (ledger is not IngredientLedger ledgerInternal) + return Task.CompletedTask; + + ledgerInternal.Deposit(CraftingIngredient.CodeFragments, 50); + Console.WriteLine("Fuck you, I won't code what you tell me!"); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/SociallyDistant/Commands/Cheats/KnowledgeIsPower.cs b/src/SociallyDistant/Commands/Cheats/KnowledgeIsPower.cs new file mode 100644 index 00000000..d5f6540a --- /dev/null +++ b/src/SociallyDistant/Commands/Cheats/KnowledgeIsPower.cs @@ -0,0 +1,27 @@ +using SociallyDistant.Architecture; +using SociallyDistant.Core; +using SociallyDistant.Core.Core; +using SociallyDistant.Core.Modules; +using SociallyDistant.Core.OS.Tasks; +using SociallyDistant.Core.WorldData; + +namespace SociallyDistant.Commands.Cheats; + +[Command("knowledgeispower", Cheat = true)] +public class KnowledgeIsPower : ScriptableCommand +{ + public KnowledgeIsPower(IGameContext gameContext) : base(gameContext) + { + } + + protected override Task OnExecute() + { + var ledger = WorldManager.Instance.World.Ledger; + if (ledger is not IngredientLedger ledgerInternal) + return Task.CompletedTask; + + ledgerInternal.Deposit(CraftingIngredient.DataFragments, 50); + Console.WriteLine("Knowledge is power, Noah..."); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/SociallyDistant/Core/WorldData/LedgerEntry.cs b/src/SociallyDistant/Core/WorldData/LedgerEntry.cs index 10636e93..729249ba 100644 --- a/src/SociallyDistant/Core/WorldData/LedgerEntry.cs +++ b/src/SociallyDistant/Core/WorldData/LedgerEntry.cs @@ -1,5 +1,6 @@ using SociallyDistant.Core.Core; using SociallyDistant.Core.Core.Serialization; +using YamlDotNet.Core.Tokens; namespace SociallyDistant.Core.WorldData; diff --git a/src/SociallyDistant/Core/WorldData/World.cs b/src/SociallyDistant/Core/WorldData/World.cs index 613bcd19..ac768334 100644 --- a/src/SociallyDistant/Core/WorldData/World.cs +++ b/src/SociallyDistant/Core/WorldData/World.cs @@ -35,6 +35,7 @@ namespace SociallyDistant.Core.WorldData private readonly WorldDataTable witnessedObjects; private readonly WorldDataTable notifications; private readonly NarrativeObjectTable newsArticles; + private readonly WorldDataTable inventory; internal IWorldDataObject ProtectedWorldData => protectedWorldState; @@ -44,6 +45,9 @@ namespace SociallyDistant.Core.WorldData public ILedger Ledger => ledger; + /// + public IWorldTable Inventory => inventory; + /// public IWorldDataObject GlobalWorldState => globalWorldState; @@ -160,7 +164,7 @@ namespace SociallyDistant.Core.WorldData witnessedObjects = new WorldDataTable(instanceIdGenerator, eventDispatcher); notifications = new WorldDataTable(instanceIdGenerator, eventDispatcher); newsArticles = new NarrativeObjectTable(instanceIdGenerator, eventDispatcher); - + inventory = new WorldDataTable(instanceIdGenerator, eventDispatcher); } public void Serialize(IWorldSerializer serializer) @@ -196,6 +200,7 @@ namespace SociallyDistant.Core.WorldData witnessedObjects.Serialize(serializer, WorldRevision.MissionFailures); notifications.Serialize(serializer, WorldRevision.Notifications); newsArticles.Serialize(serializer, WorldRevision.Articles); + inventory.Serialize(serializer, WorldRevision.Inventory); eventDispatcher.PauseEvents = false; } @@ -204,6 +209,7 @@ namespace SociallyDistant.Core.WorldData { // You must wipe the world in reverse order of how you would create or serialize it. // This ensures proper handling of deleting objects that depend on other objects. + inventory.Clear(); newsArticles.Clear(); notifications.Clear(); witnessedObjects.Clear(); diff --git a/src/SociallyDistant/Recipes/DataScraper.cs b/src/SociallyDistant/Recipes/DataScraper.cs new file mode 100644 index 00000000..c6ee6744 --- /dev/null +++ b/src/SociallyDistant/Recipes/DataScraper.cs @@ -0,0 +1,28 @@ +using SociallyDistant.Core.Core; +using SociallyDistant.Core.Modules; + +namespace SociallyDistant.Recipes; + +public sealed class DataScraper : CraftingRecipe +{ + public override RecipeCategory Category => RecipeCategory.ShellExtensions; + + public override string Description => + """ + Scrapes Data Fragments from the remote system, adding them to the Inventory. Has a small chance of locating useful information in home directories but does not download it. + """; + public override string Id => "data_scraper"; + public override string Title => "Data Scraper"; + + public override IEnumerable RequiredIngredients + { + get + { + yield return new IngredientRequirement(CraftingIngredient.DataFragments, 250); + yield return new IngredientRequirement(CraftingIngredient.CodeFragments, 100); + } + } + protected override void OnCraft(IGameContext game) + { + } +} \ No newline at end of file diff --git a/src/SociallyDistant/UI/Tools/Crafting/CraftingProgramController.cs b/src/SociallyDistant/UI/Tools/Crafting/CraftingProgramController.cs index 6b81245f..2ffeed35 100644 --- a/src/SociallyDistant/UI/Tools/Crafting/CraftingProgramController.cs +++ b/src/SociallyDistant/UI/Tools/Crafting/CraftingProgramController.cs @@ -1,15 +1,338 @@ +using AcidicGUI.CustomProperties; +using AcidicGUI.Layout; +using AcidicGUI.ListAdapters; +using AcidicGUI.TextRendering; +using AcidicGUI.Widgets; +using Microsoft.Xna.Framework; +using SociallyDistant.Core; +using SociallyDistant.Core.Core; using SociallyDistant.Core.Programs; +using SociallyDistant.Core.Shell; +using SociallyDistant.Core.UI.Common; +using SociallyDistant.Core.UI.Recycling; +using SociallyDistant.Core.UI.VisualStyles; +using SociallyDistant.UI.FloatingTools.FileManager; namespace SociallyDistant.UI.Tools.Crafting; public sealed class CraftingProgramController : ProgramController { + private readonly string ghost = """ + ______ __ __ _____ __ _ __ + / ____// /_ ____ _____ / /_/ ___/ ____ / /____ (_)/ /_ + / / __ / __ \ / __ \ / ___// __/\__ \ / __ \ / // __ \ / // __/ + / /_/ // / / // /_/ /(__ )/ /_ ___/ // /_/ // // /_/ // // /_ + \____//_/ /_/ \____//____/ \__//____// .___//_/ \____//_/ \__/ + /_/ + """; + + private readonly FlexPanel root = new(); + private readonly FlexPanel header = new(); + private readonly StackPanel headerInfo = new(); + private readonly TablePanel resourcesTable = new(); + private readonly TextWidget headerTitle = new(); + private readonly TextWidget logo = new(); + private readonly FlexPanel bodyArea = new(); + private readonly RecyclableWidgetList componentsAndCategories = new(); + private readonly FlexPanel inventoryRoot = new(); + private readonly FlexPanel specsRoot = new(); + private readonly FlexPanel currentItemRoot = new(); + private readonly TextWidget storageLabel = new(); + private readonly ProgressBar storageProgress = new(); + private readonly TextWidget storageValue = new(); + private readonly TextWidget skillLabel = new(); + private readonly ProgressBar skillProgress = new(); + private readonly TextWidget skillValue = new(); + private readonly List recipes = new(); + private readonly TextWidget inventoryTitle = new(); + private readonly ScrollView inventoryView = new(); + private readonly FileGrid inventoryGrid = new(); + private readonly TextWidget recipesTitle = new(); + private readonly ScrollView recipesView = new(); + private readonly FileGrid recipesGrid = new(); + private readonly List inventoryItems = new(); + private readonly List filteredRecipeMap = new(); + private readonly TextWidget currentItemHeader = new(); + private readonly ScrollView itemInfoScroller = new(); + private readonly FlexPanel itemBanner = new(); + private readonly CompositeIconWidget itemBannerIcon = new(); + private readonly TextWidget itemBannerText = new(); + private readonly TextWidget itemDescription = new(); + private readonly FlexPanel interactionPanel = new(); + private readonly TextWidget interactionLabel = new(); + private readonly ProgressBar interactionProgress = new(); + private readonly TextWidget interactionPercentage = new(); + private readonly OverlayWidget interactionOverlay = new(); + private readonly TextButton interactionButton = new(); + private readonly TextWidget interactionError = new(); + private readonly StackPanel interactionsArea = new(); + private InventoryItemModel? currentItem; + private RecipeCategory inventoryFilter; + private CraftingRecipe? currentRecipe; + protected CraftingProgramController(ProgramContext context) : base(context) { + context.Window.Content = root; + + skillLabel.Text = "Skill:"; + storageLabel.Text = "Storage:"; + storageValue.Text = "0 / 0 MiB"; + skillValue.Text = "0 / 0 XP"; + + headerTitle.Text = "CRAFTING"; + + resourcesTable.ColumnCount = 3; + resourcesTable.ColumnSpacing = 3; + resourcesTable.RowSpacing = 3; + root.Padding = 12; + root.Spacing = 12; + bodyArea.Spacing = 12; + logo.Text = ghost; + headerTitle.FontWeight = FontWeight.Bold; + headerInfo.Spacing = 3; + headerInfo.VerticalAlignment = VerticalAlignment.Middle; + logo.VerticalAlignment = VerticalAlignment.Middle; + logo.HorizontalAlignment = HorizontalAlignment.Right; + inventoryTitle.Text = "Inventory"; + inventoryTitle.FontWeight = FontWeight.Bold; + recipesTitle.FontWeight = FontWeight.Bold; + itemBannerText.UseMarkup = true; + + headerTitle.FontSize = 22; + logo.FontSize = 8; + logo.Font = PresetFontFamily.Monospace; + skillProgress.VerticalAlignment = VerticalAlignment.Middle; + storageProgress.VerticalAlignment = VerticalAlignment.Middle; + currentItemHeader.FontWeight = FontWeight.Bold; + + bodyArea.GetCustomProperties().Mode = FlexMode.Proportional; + logo.GetCustomProperties().Mode = FlexMode.Proportional; + headerInfo.GetCustomProperties().Mode = FlexMode.Proportional; + inventoryRoot.GetCustomProperties().Mode = FlexMode.Proportional; + specsRoot.GetCustomProperties().Mode = FlexMode.Proportional; + itemInfoScroller.GetCustomProperties().Mode = FlexMode.Proportional; + inventoryView.GetCustomProperties().Mode = FlexMode.Proportional; + recipesView.GetCustomProperties().Mode = FlexMode.Proportional; + itemBannerText.GetCustomProperties().Mode = FlexMode.Proportional; + interactionProgress.GetCustomProperties().Mode = FlexMode.Proportional; + + interactionPanel.Direction = Direction.Horizontal; + interactionsArea.Padding = new Padding(48, 6); + interactionsArea.Spacing = 3; + interactionPanel.Spacing = 3; + interactionProgress.VerticalAlignment = VerticalAlignment.Middle; + interactionLabel.VerticalAlignment = VerticalAlignment.Middle; + interactionPercentage.VerticalAlignment = VerticalAlignment.Middle; + interactionButton.VerticalAlignment = VerticalAlignment.Middle; + interactionError.VerticalAlignment = VerticalAlignment.Middle; + interactionError.HorizontalAlignment = HorizontalAlignment.Center; + interactionButton.HorizontalAlignment = HorizontalAlignment.Center; + + itemBannerIcon.IconSize = 56; + + itemBannerIcon.VerticalAlignment = VerticalAlignment.Middle; + itemBannerText.VerticalAlignment = VerticalAlignment.Middle; + itemBanner.Spacing = 6; + + itemDescription.UseMarkup = true; + itemDescription.WordWrapping = true; + + header.Direction = Direction.Horizontal; + bodyArea.Direction = Direction.Horizontal; + + currentItemRoot.MinimumSize = new Point(391, 0); + currentItemRoot.MaximumSize = new Point(391, 0); + + itemInfoScroller.Spacing = 12; + + itemBanner.Direction = Direction.Horizontal; + + itemInfoScroller.Padding = 24; + + interactionError.FontWeight = FontWeight.SemiBold; + interactionError.SetCustomProperty(WidgetForegrounds.Common); + interactionError.SetCustomProperty(CommonColor.Red); + interactionPanel.Visibility = Visibility.Hidden; + + root.ChildWidgets.Add(header); + root.ChildWidgets.Add(bodyArea); + header.ChildWidgets.Add(headerInfo); + header.ChildWidgets.Add(logo); + headerInfo.ChildWidgets.Add(headerTitle); + headerInfo.ChildWidgets.Add(resourcesTable); + bodyArea.ChildWidgets.Add(componentsAndCategories); + bodyArea.ChildWidgets.Add(inventoryRoot); + bodyArea.ChildWidgets.Add(specsRoot); + bodyArea.ChildWidgets.Add(currentItemRoot); + resourcesTable.ChildWidgets.Add(storageLabel); + resourcesTable.ChildWidgets.Add(storageProgress); + resourcesTable.ChildWidgets.Add(storageValue); + resourcesTable.ChildWidgets.Add(skillLabel); + resourcesTable.ChildWidgets.Add(skillProgress); + resourcesTable.ChildWidgets.Add(skillValue); + inventoryRoot.ChildWidgets.Add(inventoryTitle); + inventoryRoot.ChildWidgets.Add(inventoryView); + inventoryView.ChildWidgets.Add(inventoryGrid); + specsRoot.ChildWidgets.Add(recipesTitle); + specsRoot.ChildWidgets.Add(recipesView); + recipesView.ChildWidgets.Add(recipesGrid); + currentItemRoot.ChildWidgets.Add(currentItemHeader); + currentItemRoot.ChildWidgets.Add(itemInfoScroller); + itemInfoScroller.ChildWidgets.Add(itemBanner); + itemBanner.ChildWidgets.Add(itemBannerIcon); + itemBanner.ChildWidgets.Add(itemBannerText); + itemInfoScroller.ChildWidgets.Add(itemDescription); + currentItemRoot.ChildWidgets.Add(interactionsArea); + interactionsArea.ChildWidgets.Add(interactionPanel); + interactionsArea.ChildWidgets.Add(interactionOverlay); + interactionOverlay.ChildWidgets.Add(interactionError); + interactionOverlay.ChildWidgets.Add(interactionButton); + interactionPanel.ChildWidgets.Add(interactionLabel); + interactionPanel.ChildWidgets.Add(interactionProgress); + interactionPanel.ChildWidgets.Add(interactionPercentage); } protected override void Main() { - + recipes.Clear(); + recipes.AddRange(SociallyDistantGame.Instance.ContentManager.GetContentOfType()); + + RefreshInterface(); } + + private void RefreshInterface() + { + filteredRecipeMap.Clear(); + for (var i = 0; i < recipes.Count; i++) + { + var recipe = recipes[i]; + if (recipe.Category != inventoryFilter) + continue; + + filteredRecipeMap.Add(i); + } + + this.inventoryItems.Clear(); + this.inventoryItems.AddRange(WorldManager.Instance.World.Inventory.Where(x => x.Owner == 0).Select(item => new InventoryItemModel { WorldItem = item.InstanceId, Recipe = filteredRecipeMap.Select(i=>recipes[i]).FirstOrDefault(y => y.Id == item.RecipeId)! }).Where(x => x.Recipe != null!).ToArray()); + + var codeFragmentCount = WorldManager.Instance.World.Ledger.GetItemCount(CraftingIngredient.CodeFragments); + var dataFragmentCount = WorldManager.Instance.World.Ledger.GetItemCount(CraftingIngredient.DataFragments); + + RefreshSidebar(codeFragmentCount, dataFragmentCount); + RefreshInventory(); + RefreshRecipes(); + + if (currentItem != null) + { + currentItemRoot.Visibility = Visibility.Visible; + currentItemHeader.Text = "Inventory Item"; + + SetItemInfo(currentItem.Value.Recipe); + + interactionError.Visibility = Visibility.Collapsed; + interactionButton.Visibility = Visibility.Visible; + interactionButton.Text = "Disassemble"; + interactionLabel.Text = "Disassembling:"; + } + else if (currentRecipe != null) + { + currentItemRoot.Visibility = Visibility.Visible; + currentItemHeader.Text = "Crafting Spec"; + + SetItemInfo(currentRecipe); + + interactionError.Visibility = Visibility.Collapsed; + interactionButton.Visibility = Visibility.Visible; + interactionButton.Text = "Craft"; + interactionLabel.Text = "Crafting:"; + } + else + { + currentItemRoot.Visibility = Visibility.Hidden; + } + } + + private void SetItemInfo(CraftingRecipe data) + { + itemBannerIcon.Icon = MaterialIcons.Star; + itemBannerText.Text = $"{data.Title.ToUpper()}{Environment.NewLine}{data.Category}"; + itemDescription.Text = data.Description; + } + + private void RefreshRecipes() + { + recipesTitle.Text = inventoryFilter.ToString(); + var models = new List(); + + for (int i = 0; i < filteredRecipeMap.Count; i++) + { + var recipe = recipes[filteredRecipeMap[i]]; + + models.Add(new FileIconModel + { + Title = recipe.Title, + Id = i, + OpenHandler = SelectRecipe + }); + } + + recipesGrid.SetFiles(models); + } + + private void RefreshInventory() + { + inventoryGrid.SetFiles(inventoryItems.Select(x => new FileIconModel + { + Title = x.Recipe.Title, + Id = x.WorldItem.Id, + Selected = currentItem?.WorldItem == x.WorldItem, + OpenHandler = SelectItem + })); + } + + private void SelectRecipe(int id) + { + currentItem = null; + currentRecipe = recipes[filteredRecipeMap[id]]; + RefreshInterface(); + } + + private void SelectItem(int id) + { + currentRecipe = null; + currentItem = inventoryItems.First(x => x.WorldItem == id); + RefreshInterface(); + } + + private void RefreshSidebar(int codeFragments, int dataFragments) + { + var builder = new WidgetBuilder(); + builder.Begin(); + + builder.AddSection("Crafting Components", out SectionWidget components); + + builder.AddWidget(new ListItemWidget { Title = "Data Fragments", Data = dataFragments }, components); + builder.AddWidget(new ListItemWidget { Title = "Code Fragments", Data = codeFragments }, components); + + builder.AddSection("Craftables", out SectionWidget craftables); + + foreach (RecipeCategory category in Enum.GetValues()) + { + builder.AddWidget(new ListItemWidget() { Title = category.ToString(), Data = category, Callback = SetFilter, Selected = category == inventoryFilter }, craftables); + } + + componentsAndCategories.SetWidgets(builder.Build()); + } + + private void SetFilter(RecipeCategory category) + { + inventoryFilter = category; + RefreshInterface(); + } +} + +public struct InventoryItemModel +{ + public CraftingRecipe Recipe; + public ObjectId WorldItem; } \ No newline at end of file diff --git a/src/SociallyDistant/UI/Tools/Crafting/CraftingToolProvider.cs b/src/SociallyDistant/UI/Tools/Crafting/CraftingToolProvider.cs index f7666ce6..0b15182a 100644 --- a/src/SociallyDistant/UI/Tools/Crafting/CraftingToolProvider.cs +++ b/src/SociallyDistant/UI/Tools/Crafting/CraftingToolProvider.cs @@ -1,3 +1,4 @@ +using AcidicGUI.Widgets; using SociallyDistant.Core.Shell; namespace SociallyDistant.UI.Tools.Crafting;