StoreMenu.xaml.cs 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. using System.Linq;
  2. using System.Text;
  3. using Content.Client.Actions;
  4. using Content.Client.Message;
  5. using Content.Shared.FixedPoint;
  6. using Content.Shared.Store;
  7. using Robust.Client.AutoGenerated;
  8. using Robust.Client.GameObjects;
  9. using Robust.Client.Graphics;
  10. using Robust.Client.UserInterface.Controls;
  11. using Robust.Client.UserInterface.CustomControls;
  12. using Robust.Client.UserInterface.XAML;
  13. using Robust.Shared.Prototypes;
  14. namespace Content.Client.Store.Ui;
  15. [GenerateTypedNameReferences]
  16. public sealed partial class StoreMenu : DefaultWindow
  17. {
  18. [Dependency] private readonly IEntityManager _entityManager = default!;
  19. [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
  20. private StoreWithdrawWindow? _withdrawWindow;
  21. public event EventHandler<string>? SearchTextUpdated;
  22. public event Action<BaseButton.ButtonEventArgs, ListingDataWithCostModifiers>? OnListingButtonPressed;
  23. public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
  24. public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
  25. public event Action<BaseButton.ButtonEventArgs>? OnRefundAttempt;
  26. public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> Balance = new();
  27. public string CurrentCategory = string.Empty;
  28. private List<ListingDataWithCostModifiers> _cachedListings = new();
  29. public StoreMenu()
  30. {
  31. RobustXamlLoader.Load(this);
  32. IoCManager.InjectDependencies(this);
  33. WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
  34. RefundButton.OnButtonDown += OnRefundButtonDown;
  35. SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text);
  36. }
  37. public void UpdateBalance(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> balance)
  38. {
  39. Balance = balance;
  40. var currency = balance.ToDictionary(type =>
  41. (type.Key, type.Value), type => _prototypeManager.Index(type.Key));
  42. var balanceStr = string.Empty;
  43. foreach (var ((_, amount), proto) in currency)
  44. {
  45. balanceStr += Loc.GetString("store-ui-balance-display", ("amount", amount),
  46. ("currency", Loc.GetString(proto.DisplayName, ("amount", 1)))) + "\n";
  47. }
  48. BalanceInfo.SetMarkup(balanceStr.TrimEnd());
  49. var disabled = true;
  50. foreach (var type in currency)
  51. {
  52. if (type.Value.CanWithdraw && type.Value.Cash != null && type.Key.Item2 > 0)
  53. {
  54. disabled = false;
  55. break;
  56. }
  57. }
  58. WithdrawButton.Disabled = disabled;
  59. }
  60. public void UpdateListing(List<ListingDataWithCostModifiers> listings)
  61. {
  62. _cachedListings = listings;
  63. UpdateListing();
  64. }
  65. public void UpdateListing()
  66. {
  67. var sorted = _cachedListings.OrderBy(l => l.Priority)
  68. .ThenBy(l => l.Cost.Values.Sum());
  69. // should probably chunk these out instead. to-do if this clogs the internet tubes.
  70. // maybe read clients prototypes instead?
  71. ClearListings();
  72. foreach (var item in sorted)
  73. {
  74. AddListingGui(item);
  75. }
  76. }
  77. public void SetFooterVisibility(bool visible)
  78. {
  79. TraitorFooter.Visible = visible;
  80. }
  81. private void OnWithdrawButtonDown(BaseButton.ButtonEventArgs args)
  82. {
  83. // check if window is already open
  84. if (_withdrawWindow != null && _withdrawWindow.IsOpen)
  85. {
  86. _withdrawWindow.MoveToFront();
  87. return;
  88. }
  89. // open a new one
  90. _withdrawWindow = new StoreWithdrawWindow();
  91. _withdrawWindow.OpenCentered();
  92. _withdrawWindow.CreateCurrencyButtons(Balance);
  93. _withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
  94. }
  95. private void OnRefundButtonDown(BaseButton.ButtonEventArgs args)
  96. {
  97. OnRefundAttempt?.Invoke(args);
  98. }
  99. private void AddListingGui(ListingDataWithCostModifiers listing)
  100. {
  101. if (!listing.Categories.Contains(CurrentCategory))
  102. return;
  103. var hasBalance = listing.CanBuyWith(Balance);
  104. var spriteSys = _entityManager.EntitySysManager.GetEntitySystem<SpriteSystem>();
  105. Texture? texture = null;
  106. if (listing.Icon != null)
  107. texture = spriteSys.Frame0(listing.Icon);
  108. if (listing.ProductEntity != null)
  109. {
  110. if (texture == null)
  111. texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default;
  112. }
  113. else if (listing.ProductAction != null)
  114. {
  115. var actionId = _entityManager.Spawn(listing.ProductAction);
  116. if (_entityManager.System<ActionsSystem>().TryGetActionData(actionId, out var action) &&
  117. action.Icon != null)
  118. {
  119. texture = spriteSys.Frame0(action.Icon);
  120. }
  121. }
  122. var listingInStock = GetListingPriceString(listing);
  123. var discount = GetDiscountString(listing);
  124. var newListing = new StoreListingControl(listing, listingInStock, discount, hasBalance, texture);
  125. newListing.StoreItemBuyButton.OnButtonDown += args
  126. => OnListingButtonPressed?.Invoke(args, listing);
  127. StoreListingsContainer.AddChild(newListing);
  128. }
  129. private string GetListingPriceString(ListingDataWithCostModifiers listing)
  130. {
  131. var text = string.Empty;
  132. if (listing.Cost.Count < 1)
  133. text = Loc.GetString("store-currency-free");
  134. else
  135. {
  136. foreach (var (type, amount) in listing.Cost)
  137. {
  138. var currency = _prototypeManager.Index(type);
  139. text += Loc.GetString(
  140. "store-ui-price-display",
  141. ("amount", amount),
  142. ("currency", Loc.GetString(currency.DisplayName, ("amount", amount)))
  143. );
  144. }
  145. }
  146. return text.TrimEnd();
  147. }
  148. private string GetDiscountString(ListingDataWithCostModifiers listingDataWithCostModifiers)
  149. {
  150. string discountMessage;
  151. if (!listingDataWithCostModifiers.IsCostModified)
  152. {
  153. return string.Empty;
  154. }
  155. var relativeModifiersSummary = listingDataWithCostModifiers.GetModifiersSummaryRelative();
  156. if (relativeModifiersSummary.Count > 1)
  157. {
  158. var sb = new StringBuilder();
  159. sb.Append('(');
  160. foreach (var (currency, amount) in relativeModifiersSummary)
  161. {
  162. var currencyPrototype = _prototypeManager.Index(currency);
  163. if (sb.Length != 0)
  164. {
  165. sb.Append(", ");
  166. }
  167. var currentDiscountMessage = Loc.GetString(
  168. "store-ui-discount-display-with-currency",
  169. ("amount", amount.ToString("P0")),
  170. ("currency", Loc.GetString(currencyPrototype.DisplayName))
  171. );
  172. sb.Append(currentDiscountMessage);
  173. }
  174. sb.Append(')');
  175. discountMessage = sb.ToString();
  176. }
  177. else
  178. {
  179. // if cost was modified - it should have diff relatively to original cost in 1 or more currency
  180. // ReSharper disable once GenericEnumeratorNotDisposed Dictionary enumerator doesn't require dispose
  181. var enumerator = relativeModifiersSummary.GetEnumerator();
  182. enumerator.MoveNext();
  183. var amount = enumerator.Current.Value;
  184. discountMessage = Loc.GetString(
  185. "store-ui-discount-display",
  186. ("amount", (amount.ToString("P0")))
  187. );
  188. }
  189. return discountMessage;
  190. }
  191. private void ClearListings()
  192. {
  193. StoreListingsContainer.Children.Clear();
  194. }
  195. public void PopulateStoreCategoryButtons(HashSet<ListingDataWithCostModifiers> listings)
  196. {
  197. var allCategories = new List<StoreCategoryPrototype>();
  198. foreach (var listing in listings)
  199. {
  200. foreach (var cat in listing.Categories)
  201. {
  202. var proto = _prototypeManager.Index(cat);
  203. if (!allCategories.Contains(proto))
  204. allCategories.Add(proto);
  205. }
  206. }
  207. allCategories = allCategories.OrderBy(c => c.Priority).ToList();
  208. // This will reset the Current Category selection if nothing matches the search.
  209. if (allCategories.All(category => category.ID != CurrentCategory))
  210. CurrentCategory = string.Empty;
  211. if (CurrentCategory == string.Empty && allCategories.Count > 0)
  212. CurrentCategory = allCategories.First().ID;
  213. CategoryListContainer.Children.Clear();
  214. if (allCategories.Count < 1)
  215. return;
  216. var group = new ButtonGroup();
  217. foreach (var proto in allCategories)
  218. {
  219. var catButton = new StoreCategoryButton
  220. {
  221. Text = Loc.GetString(proto.Name),
  222. Id = proto.ID,
  223. Pressed = proto.ID == CurrentCategory,
  224. Group = group,
  225. ToggleMode = true,
  226. StyleClasses = { "OpenBoth" }
  227. };
  228. catButton.OnPressed += args => OnCategoryButtonPressed?.Invoke(args, catButton.Id);
  229. CategoryListContainer.AddChild(catButton);
  230. }
  231. }
  232. public override void Close()
  233. {
  234. base.Close();
  235. _withdrawWindow?.Close();
  236. }
  237. public void UpdateRefund(bool allowRefund)
  238. {
  239. RefundButton.Visible = allowRefund;
  240. }
  241. private sealed class StoreCategoryButton : Button
  242. {
  243. public string? Id;
  244. }
  245. }