Generalized Store System (#10201)

This commit is contained in:
Nemanja
2022-08-17 00:34:25 -04:00
committed by GitHub
parent 1b50928d50
commit 2152914acc
68 changed files with 2493 additions and 1568 deletions

View File

@@ -0,0 +1,79 @@
using Content.Shared.Store;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using System.Linq;
namespace Content.Client.Store.Ui;
[UsedImplicitly]
public sealed class StoreBoundUserInterface : BoundUserInterface
{
private StoreMenu? _menu;
private string _windowName = Loc.GetString("store-ui-default-title");
public StoreBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
_menu = new StoreMenu(_windowName);
_menu.OpenCentered();
_menu.OnClose += Close;
_menu.OnListingButtonPressed += (_, listing) =>
{
if (_menu.CurrentBuyer != null)
SendMessage(new StoreBuyListingMessage(_menu.CurrentBuyer.Value, listing));
};
_menu.OnCategoryButtonPressed += (_, category) =>
{
_menu.CurrentCategory = category;
if (_menu.CurrentBuyer != null)
SendMessage(new StoreRequestUpdateInterfaceMessage(_menu.CurrentBuyer.Value));
};
_menu.OnWithdrawAttempt += (_, type, amount) =>
{
if (_menu.CurrentBuyer != null)
SendMessage(new StoreRequestWithdrawMessage(_menu.CurrentBuyer.Value, type, amount));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_menu == null)
return;
switch (state)
{
case StoreUpdateState msg:
if (msg.Buyer != null)
_menu.CurrentBuyer = msg.Buyer;
_menu.UpdateBalance(msg.Balance);
_menu.PopulateStoreCategoryButtons(msg.Listings);
_menu.UpdateListing(msg.Listings.ToList());
break;
case StoreInitializeState msg:
_windowName = msg.Name;
if (_menu != null && _menu.Window != null)
_menu.Window.Title = msg.Name;
break;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Close();
_menu?.Dispose();
}
}

View File

@@ -0,0 +1,21 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Margin="8,8,8,8" Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Name="StoreItemName" HorizontalExpand="True" />
<Button
Name="StoreItemBuyButton"
MinWidth="64"
HorizontalAlignment="Right"
Access="Public" />
</BoxContainer>
<PanelContainer StyleClasses="HighDivider" />
<BoxContainer HorizontalExpand="True" Orientation="Horizontal">
<TextureRect
Name="StoreItemTexture"
Margin="0,0,4,0"
MinSize="48 48"
Stretch="KeepAspectCentered" />
<RichTextLabel Name="StoreItemDescription" />
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,24 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Store.Ui;
[GenerateTypedNameReferences]
public sealed partial class StoreListingControl : Control
{
public StoreListingControl(string itemName, string itemDescription,
string price, bool canBuy, Texture? texture = null)
{
RobustXamlLoader.Load(this);
StoreItemName.Text = itemName;
StoreItemDescription.SetMessage(itemDescription);
StoreItemBuyButton.Text = price;
StoreItemBuyButton.Disabled = !canBuy;
StoreItemTexture.Texture = texture;
}
}

View File

@@ -0,0 +1,54 @@
<DefaultWindow
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'store-ui-default-title'}"
MinSize="512 512"
SetSize="512 512">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer Margin="4,4,4,4" Orientation="Horizontal">
<RichTextLabel
Name="BalanceInfo"
HorizontalAlignment="Left"
Access="Public"
HorizontalExpand="True" />
<Button
Name="WithdrawButton"
MinWidth="64"
HorizontalAlignment="Right"
Text="{Loc 'store-ui-default-withdraw-text'}" />
</BoxContainer>
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#80808005" />
</PanelContainer.PanelOverride>
<BoxContainer Name="CategoryListContainer" Orientation="Vertical">
<!-- Category buttons are added here by code -->
</BoxContainer>
</PanelContainer>
<ScrollContainer
Name="StoreListingsScroll"
HScrollEnabled="False"
HorizontalExpand="True"
MinSize="100 256"
SizeFlagsStretchRatio="2"
VerticalExpand="True">
<BoxContainer
Name="StoreListingsContainer"
MinSize="100 256"
Orientation="Vertical"
SizeFlagsStretchRatio="2"
VerticalExpand="True">
<!-- Listings are added here by code -->
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,222 @@
using Content.Client.Message;
using Content.Shared.Store;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Client.Graphics;
using Content.Shared.Actions.ActionTypes;
using System.Linq;
using Content.Shared.FixedPoint;
namespace Content.Client.Store.Ui;
[GenerateTypedNameReferences]
public sealed partial class StoreMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private StoreWithdrawWindow? _withdrawWindow;
public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
public EntityUid? CurrentBuyer;
public Dictionary<string, FixedPoint2> Balance = new();
public string CurrentCategory = string.Empty;
public StoreMenu(string name)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
if (Window != null)
Window.Title = name;
}
public void UpdateBalance(Dictionary<string, FixedPoint2> balance)
{
Balance = balance;
var currency = new Dictionary<(string, FixedPoint2), CurrencyPrototype>();
foreach (var type in balance)
{
currency.Add((type.Key, type.Value), _prototypeManager.Index<CurrencyPrototype>(type.Key));
}
var balanceStr = string.Empty;
foreach (var type in currency)
{
balanceStr += $"{Loc.GetString(type.Value.BalanceDisplay, ("amount", type.Key.Item2))}\n";
}
BalanceInfo.SetMarkup(balanceStr.TrimEnd());
var disabled = true;
foreach (var type in currency)
{
if (type.Value.CanWithdraw && type.Value.EntityId != null && type.Key.Item2 > 0)
disabled = false;
}
WithdrawButton.Disabled = disabled;
}
public void UpdateListing(List<ListingData> listings)
{
var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
// should probably chunk these out instead. to-do if this clogs the internet tubes.
// maybe read clients prototypes instead?
ClearListings();
foreach (var item in sorted)
{
AddListingGui(item);
}
}
private void OnWithdrawButtonDown(BaseButton.ButtonEventArgs args)
{
// check if window is already open
if (_withdrawWindow != null && _withdrawWindow.IsOpen)
{
_withdrawWindow.MoveToFront();
return;
}
// open a new one
_withdrawWindow = new StoreWithdrawWindow();
_withdrawWindow.OpenCentered();
_withdrawWindow.CreateCurrencyButtons(Balance);
_withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
}
private void AddListingGui(ListingData listing)
{
if (!listing.Categories.Contains(CurrentCategory))
return;
string listingName = new (listing.Name);
string listingDesc = new (listing.Description);
var listingPrice = listing.Cost;
var canBuy = CanBuyListing(Balance, listingPrice);
var spriteSys = _entityManager.EntitySysManager.GetEntitySystem<SpriteSystem>();
Texture? texture = null;
if (listing.Icon != null)
texture = spriteSys.Frame0(listing.Icon);
if (listing.ProductEntity != null)
{
if (texture == null)
texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default;
var proto = _prototypeManager.Index<EntityPrototype>(listing.ProductEntity);
if (listingName == string.Empty)
listingName = proto.Name;
if (listingDesc == string.Empty)
listingDesc = proto.Description;
}
else if (listing.ProductAction != null)
{
var action = _prototypeManager.Index<InstantActionPrototype>(listing.ProductAction);
if (action.Icon != null)
texture = spriteSys.Frame0(action.Icon);
}
var newListing = new StoreListingControl(listingName, listingDesc, GetListingPriceString(listing), canBuy, texture);
newListing.StoreItemBuyButton.OnButtonDown += args
=> OnListingButtonPressed?.Invoke(args, listing);
StoreListingsContainer.AddChild(newListing);
}
public bool CanBuyListing(Dictionary<string, FixedPoint2> currency, Dictionary<string, FixedPoint2> price)
{
foreach (var type in price)
{
if (!currency.ContainsKey(type.Key))
return false;
if (currency[type.Key] < type.Value)
return false;
}
return true;
}
public string GetListingPriceString(ListingData listing)
{
var text = string.Empty;
foreach (var type in listing.Cost)
{
var currency = _prototypeManager.Index<CurrencyPrototype>(type.Key);
text += $"{Loc.GetString(currency.PriceDisplay, ("amount", type.Value))}\n";
}
if (listing.Cost.Count < 1)
text = Loc.GetString("store-currency-free");
return text.TrimEnd();
}
private void ClearListings()
{
StoreListingsContainer.Children.Clear();
}
public void PopulateStoreCategoryButtons(HashSet<ListingData> listings)
{
var allCategories = new List<StoreCategoryPrototype>();
foreach (var listing in listings)
{
foreach (var cat in listing.Categories)
{
var proto = _prototypeManager.Index<StoreCategoryPrototype>(cat);
if (!allCategories.Contains(proto))
allCategories.Add(proto);
}
}
allCategories = allCategories.OrderBy(c => c.Priority).ToList();
if (CurrentCategory == string.Empty && allCategories.Count > 0)
CurrentCategory = allCategories.First().ID;
if (allCategories.Count <= 1)
return;
CategoryListContainer.Children.Clear();
foreach (var proto in allCategories)
{
var catButton = new StoreCategoryButton
{
Text = Loc.GetString(proto.Name),
Id = proto.ID
};
catButton.OnPressed += args => OnCategoryButtonPressed?.Invoke(args, catButton.Id);
CategoryListContainer.AddChild(catButton);
}
}
public override void Close()
{
base.Close();
CurrentBuyer = null;
_withdrawWindow?.Close();
}
private sealed class StoreCategoryButton : Button
{
public string? Id;
}
}

View File

@@ -0,0 +1,16 @@
<DefaultWindow
xmlns="https://spacestation14.io"
Title="{Loc 'store-ui-default-withdraw-text'}"
MinSize="256 128">
<BoxContainer
HorizontalExpand="True"
Orientation="Vertical"
VerticalExpand="True">
<SliderIntInput Name="WithdrawSlider" HorizontalExpand="True" />
<BoxContainer
Name="ButtonContainer"
VerticalAlignment="Bottom"
Orientation="Vertical"
VerticalExpand="True" />
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,102 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
using Robust.Client.UserInterface;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Client.Graphics;
using Content.Shared.Actions.ActionTypes;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
namespace Content.Client.Store.Ui;
/// <summary>
/// Window to select amount TC to withdraw from Uplink account
/// Used as sub-window in Uplink UI
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class StoreWithdrawWindow : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private Dictionary<FixedPoint2, CurrencyPrototype> _validCurrencies = new();
private HashSet<CurrencyWithdrawButton> _buttons = new();
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
public StoreWithdrawWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void CreateCurrencyButtons(Dictionary<string, FixedPoint2> balance)
{
_validCurrencies.Clear();
foreach (var currency in balance)
{
if (!_prototypeManager.TryIndex<CurrencyPrototype>(currency.Key, out var proto))
continue;
_validCurrencies.Add(currency.Value, proto);
}
//this shouldn't ever happen but w/e
if (_validCurrencies.Count < 1)
return;
ButtonContainer.Children.Clear();
_buttons.Clear();
foreach (var currency in _validCurrencies)
{
Logger.Debug((currency.Value.PriceDisplay));
var button = new CurrencyWithdrawButton()
{
Id = currency.Value.ID,
Amount = currency.Key,
MinHeight = 20,
Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Value.PriceDisplay))),
};
button.Disabled = false;
button.OnPressed += args =>
{
OnWithdrawAttempt?.Invoke(args, button.Id, WithdrawSlider.Value);
Close();
};
_buttons.Add(button);
ButtonContainer.AddChild(button);
}
var maxWithdrawAmount = _validCurrencies.Keys.Max().Int();
// setup withdraw slider
WithdrawSlider.MinValue = 1;
WithdrawSlider.MaxValue = maxWithdrawAmount;
WithdrawSlider.OnValueChanged += OnValueChanged;
OnValueChanged(WithdrawSlider.Value);
}
public void OnValueChanged(int i)
{
foreach (var button in _buttons)
{
button.Disabled = button.Amount < WithdrawSlider.Value;
}
}
private sealed class CurrencyWithdrawButton : Button
{
public string? Id;
public FixedPoint2 Amount = FixedPoint2.Zero;
}
}