Client LuaCsForBarotrauma
UpgradeStore.cs
1 #nullable enable
2 
3 using System;
4 using System.Collections.Generic;
5 using System.Collections.Immutable;
6 using System.Diagnostics;
7 using System.Linq;
10 using FarseerPhysics;
11 using Microsoft.Xna.Framework;
12 using Microsoft.Xna.Framework.Graphics;
13 using Microsoft.Xna.Framework.Input;
14 using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement;
15 
16 // ReSharper disable UnusedVariable
17 
18 namespace Barotrauma
19 {
20  internal sealed class UpgradeStore
21  {
22  public readonly struct CategoryData
23  {
24  public readonly UpgradeCategory Category;
25  public readonly List<UpgradePrefab>? Prefabs;
26  public readonly UpgradePrefab? SinglePrefab;
27 
28  public CategoryData(UpgradeCategory category, List<UpgradePrefab> prefabs)
29  {
30  Category = category;
31  Prefabs = prefabs;
32  SinglePrefab = null;
33  }
34 
35  public CategoryData(UpgradeCategory category, UpgradePrefab prefab)
36  {
37  Category = category;
38  SinglePrefab = prefab;
39  Prefabs = null;
40  }
41  }
42 
43  private readonly CampaignUI campaignUI;
44  private CampaignMode? Campaign => campaignUI.Campaign;
45  private int PlayerBalance => Campaign?.GetBalance() ?? 0;
46  private UpgradeTab selectedUpgradeTab = UpgradeTab.Upgrade;
47 
48  private GUIMessageBox? currectConfirmation;
49 
50  public readonly GUIFrame ItemInfoFrame;
51  private GUIComponent? selectedUpgradeCategoryLayout;
52  private GUILayoutGroup? topHeaderLayout;
53  private GUILayoutGroup? mainStoreLayout;
54  private GUILayoutGroup? storeLayout;
55  private GUILayoutGroup? categoryButtonLayout;
56  private GUILayoutGroup? submarineInfoFrame;
57  private GUIListBox? currentStoreLayout;
58  private GUICustomComponent? submarinePreviewComponent;
59  private GUIFrame? subPreviewFrame;
60  private Submarine? drawnSubmarine;
61  private readonly List<UpgradeCategory> applicableCategories = new List<UpgradeCategory>();
62  private Vector2[][] subHullVertices = new Vector2[0][];
63  private List<Structure> submarineWalls = new List<Structure>();
64 
65  public MapEntity? HoveredEntity;
66  private bool highlightWalls;
67 
68  private UpgradeCategory? currentUpgradeCategory;
69  private GUIButton? activeItemSwapSlideDown;
70 
71  private readonly Dictionary<Item, GUIComponent> itemPreviews = new Dictionary<Item, GUIComponent>();
72 
73  private static readonly Color previewWhite = Color.White * 0.5f;
74 
75  private Point screenResolution;
76 
77  private bool needsRefresh = true;
78 
79  private PlayerBalanceElement? playerBalanceElement;
80 
81  private static ImmutableHashSet<Character> characterList = ImmutableHashSet<Character>.Empty;
82 
87  public static bool WaitForServerUpdate;
88 
89  private enum UpgradeTab
90  {
91  Upgrade,
92  Repairs
93  }
94 
95  private enum UpgradeStoreUserData
96  {
97  BuyButton,
98  BuyButtonLayout,
99  ProgressBarLayout,
100  IncreaseLabel,
101  PriceLabel,
102  MaterialCostList
103  }
104 
105  public UpgradeStore(CampaignUI campaignUI, GUIComponent parent)
106  {
107  WaitForServerUpdate = false;
108  characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both);
109  this.campaignUI = campaignUI;
110  GUIFrame upgradeFrame = new GUIFrame(rectT(1, 1, parent, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f)
111  {
112  CanBeFocused = false, UserData = "outerglow"
113  };
114 
115  ItemInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.13f), GUI.Canvas, minSize: new Point(250, 150)), style: "GUIToolTip")
116  {
117  CanBeFocused = false
118  };
119 
120  CreateUI(upgradeFrame);
121 
122  if (Campaign == null) { return; }
123  Identifier eventId = new Identifier(nameof(UpgradeStore));
124  Campaign.UpgradeManager.OnUpgradesChanged?.RegisterOverwriteExisting(eventId, _ => RequestRefresh());
125  Campaign.CargoManager.OnPurchasedItemsChanged.RegisterOverwriteExisting(eventId, _ => RequestRefresh());
126  Campaign.CargoManager.OnSoldItemsChanged.RegisterOverwriteExisting(eventId, _ => RequestRefresh());
127  Campaign.OnMoneyChanged.RegisterOverwriteExisting(eventId, _ => RequestRefresh());
128  }
129 
130  public void RequestRefresh()
131  {
132  needsRefresh = true;
133  }
134 
135  private void RefreshAll()
136  {
137  characterList = GameSession.GetSessionCrewCharacters(CharacterType.Both);
138  switch (selectedUpgradeTab)
139  {
140  case UpgradeTab.Repairs:
141  SelectTab(UpgradeTab.Repairs);
142  break;
143  case UpgradeTab.Upgrade:
144  RefreshUpgradeList();
145  foreach (var itemPreview in itemPreviews)
146  {
147  if (!(itemPreview.Value is GUIImage image) || itemPreview.Key == null) { continue; }
148  if (itemPreview.Key.PendingItemSwap == null)
149  {
150  image.Sprite = itemPreview.Key.Prefab.UpgradePreviewSprite;
151  }
152  else if (itemPreview.Key.PendingItemSwap.UpgradePreviewSprite != null)
153  {
154  image.Sprite = itemPreview.Key.PendingItemSwap.UpgradePreviewSprite;
155  }
156  }
157  break;
158  }
159  needsRefresh = false;
160  }
161 
162  private void RefreshUpgradeList()
163  {
164  if (Campaign == null) { return; }
165  // Updates the progress bar / text and disables the buy button if we reached max level
166  if (selectedUpgradeCategoryLayout?.Parent != null && selectedUpgradeCategoryLayout.FindChild("prefablist", true) is GUIListBox listBox)
167  {
168  foreach (var component in listBox.Content.Children)
169  {
170  if (component.UserData is CategoryData { SinglePrefab: { } prefab} data)
171  {
172  UpdateUpgradeEntry(component, prefab, data.Category, Campaign);
173  }
174  }
175  if (customizeTabOpen && selectedUpgradeCategoryLayout != null && Submarine.MainSub != null && currentUpgradeCategory != null)
176  {
177  CreateSwappableItemList(listBox, currentUpgradeCategory, Submarine.MainSub);
178  if (activeItemSwapSlideDown?.UserData is Item prevOpenedItem)
179  {
180  var currentButton = listBox.FindChild(c => c.UserData as Item == prevOpenedItem, recursive: true) as GUIButton;
181  currentButton?.OnClicked(currentButton, prevOpenedItem);
182  }
183  }
184  }
185 
186  // update the small indicator icons on the list
187  if (currentStoreLayout?.Parent != null)
188  {
189  UpdateCategoryList(currentStoreLayout, Campaign, drawnSubmarine, applicableCategories);
190  }
191 
192  }
193 
194  //TODO: move this somewhere else
195  public static void UpdateCategoryList(GUIListBox categoryList, CampaignMode campaign, Submarine? drawnSubmarine, IEnumerable<UpgradeCategory> applicableCategories)
196  {
197  var subItems = GetSubItems();
198  foreach (GUIComponent component in categoryList.Content.Children)
199  {
200  if (!(component.UserData is CategoryData data)) { continue; }
201  if (component.FindChild("indicators", true) is { } indicators && data.Prefabs != null)
202  {
203  // ReSharper disable once PossibleMultipleEnumeration
204  UpdateCategoryIndicators(indicators, component, data.Prefabs, data.Category, campaign, drawnSubmarine, applicableCategories);
205  }
206  var customizeButton = component.FindChild("customizebutton", true);
207  if (customizeButton != null)
208  {
209  customizeButton.Visible = HasSwappableItems(data.Category, subItems);
210  }
211  }
212 
213  // reset the order first
214  foreach (UpgradeCategory category in UpgradeCategory.Categories.OrderBy(c => c.Name))
215  {
216  GUIComponent component = categoryList.Content.FindChild(c => c.UserData is CategoryData categoryData && categoryData.Category == category);
217  component?.SetAsLastChild();
218  }
219 
220  // send the disabled components to the bottom
221  List<GUIComponent> lastChilds = categoryList.Content.Children.Where(component => !component.Enabled).ToList();
222 
223  foreach (var lastChild in lastChilds)
224  {
225  lastChild.SetAsLastChild();
226  }
227  }
228 
229  /* Rough layout of the upgrade store 0.9 padding
230  * _____________________________________________________________________________________________________________________
231  * | i | Shipyard | balance |
232  * |---------------------------------------------------------| xxxx mk |
233  * | upg. | maint. | |_________________________________________________________|
234  * |---------------------------------------------------------|---------------------------------------------------------| <- header separator
235  * | upgrade list | | selected category | | sub name |
236  * | | | | empty space | submarine description |
237  * | | | | |______________________________|
238  * | | | __________________|_______________________________________________________________________|
239  * | | | | | |
240  * |____________________| |__|_________________| |
241  * | store layout | | category layout | |
242  * | | | | | |
243  * |____________________| | | | |
244  * | | | | |
245  * | | | submarine preview layout |
246  * | | | | |
247  * | | | | |
248  * | empty space | | | |
249  * | | | | |
250  * | | | | |
251  * | | | | |
252  * |______________________|__|_________________|_______________________________________________________________________|
253  */
254  private void CreateUI(GUIComponent parent)
255  {
256  selectedUpgradeTab = UpgradeTab.Upgrade;
257  parent.ClearChildren();
258 
259  ItemInfoFrame.ClearChildren();
260 
261  /* TOOLTIP
262  * |----------------------------|
263  * | item name |
264  * |----------------------------|
265  * | upgrades: |
266  * |----------------------------|
267  * | upgrade list |
268  * | |
269  * | |
270  * |----------------------------|
271  * | X more... |
272  * |----------------------------|
273  */
274  GUILayoutGroup tooltipLayout = new GUILayoutGroup(rectT(0.95f,0.95f, ItemInfoFrame, Anchor.Center)) { Stretch = true };
275  new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty, font: GUIStyle.SubHeadingFont) { UserData = "itemname" };
276  new GUITextBlock(rectT(1, 0, tooltipLayout), TextManager.Get("UpgradeUITooltip.UpgradeListHeader"));
277  new GUIListBox(rectT(1, 0.5f, tooltipLayout), style: null) { ScrollBarVisible = false, AutoHideScrollBar = false, SmoothScroll = true, UserData = "upgradelist"};
278  new GUITextBlock(rectT(1, 0, tooltipLayout), string.Empty) { UserData = "moreindicator" };
279  ItemInfoFrame.Children.ForEach(c => { c.CanBeFocused = false; c.Children.ForEach(c2 => c2.CanBeFocused = false); });
280 
281  GUIFrame paddedLayout = new GUIFrame(rectT(0.95f, 0.95f, parent, Anchor.Center), style: null);
282  mainStoreLayout = new GUILayoutGroup(rectT(1, 0.9f, paddedLayout, Anchor.BottomLeft), isHorizontal: true) { RelativeSpacing = 0.01f };
283  topHeaderLayout = new GUILayoutGroup(rectT(1, 0.1f, paddedLayout, Anchor.TopLeft), isHorizontal: true);
284 
285  storeLayout = new GUILayoutGroup(rectT(0.2f, 0.4f, mainStoreLayout), isHorizontal: true) { RelativeSpacing = 0.02f };
286 
287 
288  /* LEFT HEADER LAYOUT
289  * |---------------------------------------------------------------------------------------------------|
290  * | icon | Shipyard |
291  * |---------------------------------------------------------------------------------------------------|
292  * | upgrades | maintenance | <- 1/3rd empty space |
293  * |---------------------------------------------------------------------------------------------------|
294  */
295  GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.4f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f };
296  GUILayoutGroup locationLayout = new GUILayoutGroup(rectT(1, 0.5f, leftLayout), isHorizontal: true);
297  GUIImage submarineIcon = new GUIImage(rectT(new Point(locationLayout.Rect.Height, locationLayout.Rect.Height), locationLayout), style: "SubmarineIcon", scaleToFit: true);
298  var header = new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont);
299  header.RectTransform.MaxSize = new Point((int)(header.TextSize.X + header.Padding.X + header.Padding.Z), int.MaxValue);
300  new GUITextBlock(rectT(1.0f, 1, locationLayout), TextManager.Get("UpgradeUI.AllSubmarinesInfo"), font: GUIStyle.SmallFont, wrap: true);
301 
302  categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true };
303  GUIButton upgradeButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade };
304  GUIButton repairButton = new GUIButton(rectT(0.5f, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs };
305 
306  /* RIGHT HEADER LAYOUT
307  * |---------------------------------------------------------------------------------------------------|
308  * | empty space |
309  * |---------------------------------------------------------------------------------------------------|
310  * | Balance |
311  * | XXXX Mk |
312  * |---------------------------------------------------------------------------------------------------|
313  * | empty space | horizontal line |
314  * |---------------------------------------------------------------------------------------------------|
315  */
316  GUILayoutGroup rightLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout), childAnchor: Anchor.TopRight);
317  playerBalanceElement = CampaignUI.AddBalanceElement(rightLayout, new Vector2(1.0f, 0.8f));
318  if (playerBalanceElement is { } balanceElement)
319  {
320  balanceElement.TotalBalanceContainer.OnAddedToGUIUpdateList += (_) =>
321  {
322  playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement);
323  };
324  }
325  new GUIFrame(rectT(0.5f, 0.1f, rightLayout, Anchor.BottomRight), style: "HorizontalLine") { IgnoreLayoutGroups = true };
326 
327  repairButton.OnClicked = upgradeButton.OnClicked = (button, o) =>
328  {
329  if (o is UpgradeTab upgradeTab)
330  {
331  if (upgradeTab != selectedUpgradeTab || currentStoreLayout == null || currentStoreLayout.Parent != storeLayout)
332  {
333  selectedUpgradeTab = upgradeTab;
334  SelectTab(selectedUpgradeTab);
335  storeLayout?.Recalculate();
336  }
337 
338  repairButton.Selected = (UpgradeTab) repairButton.UserData == selectedUpgradeTab;
339  upgradeButton.Selected = (UpgradeTab) upgradeButton.UserData == selectedUpgradeTab;
340 
341  return true;
342  }
343 
344  return false;
345  };
346 
347  // submarine preview
348  submarinePreviewComponent = new GUICustomComponent(rectT(0.75f, 0.75f, mainStoreLayout, Anchor.BottomRight), onUpdate: UpdateSubmarinePreview, onDraw: DrawSubmarine)
349  {
350  IgnoreLayoutGroups = true
351  };
352 
353  SelectTab(UpgradeTab.Upgrade);
354 
355  var itemSwapPreview = new GUICustomComponent(new RectTransform(new Vector2(0.25f, 0.4f), mainStoreLayout.RectTransform, Anchor.TopLeft)
356  { RelativeOffset = new Vector2(0.52f * GUI.AspectRatioAdjustment, 0.0f) }, DrawItemSwapPreview)
357  {
358  IgnoreLayoutGroups = true,
359  CanBeFocused = true
360  };
361 
362  GUITextBlock.AutoScaleAndNormalize(upgradeButton.TextBlock, repairButton.TextBlock);
363 
364 #if DEBUG
365  // creates a button that re-creates the UI
366  CreateRefreshButton();
367  void CreateRefreshButton()
368  {
369  new GUIButton(rectT(0.2f, 0.1f, parent, Anchor.TopCenter), "Recreate UI - NOT PRESENT IN RELEASE!")
370  {
371  OnClicked = (button, o) =>
372  {
373  CreateUI(parent);
374  return true;
375  }
376  };
377  }
378 #endif
379  }
380 
381  private void DrawItemSwapPreview(SpriteBatch spriteBatch, GUICustomComponent component)
382  {
383  var selectedItem = customizeTabOpen ?
384  activeItemSwapSlideDown?.UserData as Item ?? HoveredEntity as Item :
385  HoveredEntity as Item;
386  if (selectedItem?.Prefab.SwappableItem == null) { return; }
387 
388  Sprite schematicsSprite = selectedItem.Prefab.SwappableItem.SchematicSprite;
389  if (schematicsSprite == null) { return; }
390  float schematicsScale = Math.Min(component.Rect.Width / 2 / schematicsSprite.size.X, component.Rect.Height / schematicsSprite.size.Y);
391  Vector2 center = new Vector2(component.Rect.Center.X, component.Rect.Center.Y);
392  schematicsSprite.Draw(spriteBatch, new Vector2(component.Rect.X, center.Y), GUIStyle.Green, new Vector2(0, schematicsSprite.size.Y / 2),
393  scale: schematicsScale);
394 
395  var swappableItemList = selectedUpgradeCategoryLayout?.FindChild("prefablist", true) as GUIListBox;
396  var highlightedElement = swappableItemList?.Content.FindChild(c => c.UserData is ItemPrefab && c.IsParentOf(GUI.MouseOn)) ?? GUI.MouseOn;
397  ItemPrefab swapTo = highlightedElement?.UserData as ItemPrefab ?? selectedItem.PendingItemSwap;
398  if (swapTo?.SwappableItem == null) { return; }
399  Sprite? schematicsSprite2 = swapTo.SwappableItem?.SchematicSprite;
400  schematicsSprite2?.Draw(spriteBatch, new Vector2(component.Rect.Right, center.Y), GUIStyle.Orange, new Vector2(schematicsSprite2.size.X, schematicsSprite2.size.Y / 2),
401  scale: Math.Min(component.Rect.Width / 2 / schematicsSprite2.size.X, component.Rect.Height / schematicsSprite2.size.Y));
402 
403  var arrowSprite = GUIStyle.GetComponentStyle("GUIButtonToggleRight")?.GetDefaultSprite();
404  if (arrowSprite != null)
405  {
406  arrowSprite.Draw(spriteBatch, center, scale: GUI.Scale);
407  }
408  }
409 
410  private void SelectTab(UpgradeTab tab)
411  {
412  if (currentStoreLayout != null)
413  {
414  storeLayout?.RemoveChild(currentStoreLayout);
415  }
416 
417  if (selectedUpgradeCategoryLayout != null)
418  {
419  mainStoreLayout?.RemoveChild(selectedUpgradeCategoryLayout);
420  }
421 
422  switch (tab)
423  {
424  case UpgradeTab.Upgrade:
425  {
426  CreateUpgradeTab();
427  break;
428  }
429  case UpgradeTab.Repairs:
430  {
431  CreateRepairsTab();
432  break;
433  }
434  }
435  }
436 
437  private void CreateRepairsTab()
438  {
439  if (Campaign == null || storeLayout == null) { return; }
440 
441  highlightWalls = false;
442  foreach (GUIComponent itemFrame in itemPreviews.Values)
443  {
444  itemFrame.OutlineColor = previewWhite;
445  }
446 
447  currentStoreLayout = new GUIListBox(new RectTransform(new Vector2(1.2f, 1.5f), storeLayout.RectTransform) { MinSize = new Point(256, 0) }, style: null)
448  {
449  AutoHideScrollBar = false,
450  ScrollBarVisible = false,
451  Spacing = 8
452  };
453 
454  Location location = Campaign.Map.CurrentLocation;
455 
456  int hullRepairCost = CampaignMode.GetHullRepairCost();
457  int itemRepairCost = CampaignMode.GetItemRepairCost();
458  int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost;
459  if (location != null)
460  {
461  hullRepairCost = location.GetAdjustedMechanicalCost(hullRepairCost);
462  itemRepairCost = location.GetAdjustedMechanicalCost(itemRepairCost);
463  shuttleRetrieveCost = location.GetAdjustedMechanicalCost(shuttleRetrieveCost);
464  }
465 
466  CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallwalls"), "RepairHullButton", hullRepairCost, (button, o) =>
467  {
468  //cost is zero = nothing to repair
469  if (Campaign.PurchasedHullRepairs || hullRepairCost <= 0)
470  {
471  button.Enabled = false;
472  return false;
473  }
474 
475  if (PlayerBalance >= hullRepairCost)
476  {
477  LocalizedString body = TextManager.GetWithVariable("WallRepairs.PurchasePromptBody", "[amount]", hullRepairCost.ToString());
478  currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () =>
479  {
480  if (PlayerBalance >= hullRepairCost)
481  {
482  Campaign.TryPurchase(null, hullRepairCost);
483  GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "hullrepairs");
484  Campaign.PurchasedHullRepairs = true;
485  button.Enabled = false;
486  SelectTab(UpgradeTab.Repairs);
487  GameMain.Client?.SendCampaignState();
488  }
489  else
490  {
491  button.Enabled = false;
492  }
493  return true;
494  }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction);
495  }
496  else
497  {
498  button.Enabled = false;
499  return false;
500  }
501  return true;
502  }, Campaign.PurchasedHullRepairs || !HasPermission || hullRepairCost <= 0, isHovered =>
503  {
504  highlightWalls = isHovered;
505  return true;
506  });
507 
508  CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("repairallitems"), "RepairItemsButton", itemRepairCost, (button, o) =>
509  {
510  //cost is zero = nothing to repair
511  if (PlayerBalance >= itemRepairCost && !Campaign.PurchasedItemRepairs && itemRepairCost > 0)
512  {
513  LocalizedString body = TextManager.GetWithVariable("ItemRepairs.PurchasePromptBody", "[amount]", itemRepairCost.ToString());
514  currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () =>
515  {
516  if (PlayerBalance >= itemRepairCost && !Campaign.PurchasedItemRepairs)
517  {
518  Campaign.TryPurchase(null, itemRepairCost);
519  GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "devicerepairs");
520  Campaign.PurchasedItemRepairs = true;
521  button.Enabled = false;
522  SelectTab(UpgradeTab.Repairs);
523  GameMain.Client?.SendCampaignState();
524  }
525  else
526  {
527  button.Enabled = false;
528  }
529  return true;
530  }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction);
531  }
532  else
533  {
534  button.Enabled = false;
535  return false;
536  }
537  return true;
538  }, Campaign.PurchasedItemRepairs || !HasPermission || itemRepairCost <= 0, isHovered =>
539  {
540  foreach (var (item, itemFrame) in itemPreviews)
541  {
542  itemFrame.OutlineColor = itemFrame.Color = isHovered && item.GetComponent<DockingPort>() == null ? GUIStyle.Orange : previewWhite;
543  }
544  return true;
545  });
546 
547  CreateRepairEntry(currentStoreLayout.Content, TextManager.Get("replacelostshuttles"), "ReplaceShuttlesButton", shuttleRetrieveCost, (button, o) =>
548  {
549  if (GameMain.GameSession?.SubmarineInfo != null &&
550  GameMain.GameSession.SubmarineInfo.LeftBehindSubDockingPortOccupied)
551  {
552  new GUIMessageBox("", TextManager.Get("ReplaceShuttleDockingPortOccupied"));
553  return false;
554  }
555 
556  if (PlayerBalance >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles)
557  {
558  LocalizedString body = TextManager.GetWithVariable("ReplaceLostShuttles.PurchasePromptBody", "[amount]", shuttleRetrieveCost.ToString());
559  currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), body, () =>
560  {
561  if (PlayerBalance >= shuttleRetrieveCost && !Campaign.PurchasedLostShuttles)
562  {
563  Campaign.TryPurchase(null, shuttleRetrieveCost);
564  GameAnalyticsManager.AddMoneySpentEvent(hullRepairCost, GameAnalyticsManager.MoneySink.Service, "retrieveshuttle");
565  Campaign.PurchasedLostShuttles = true;
566  button.Enabled = false;
567  SelectTab(UpgradeTab.Repairs);
568  GameMain.Client?.SendCampaignState();
569  }
570  return true;
571  }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction);
572  }
573  else
574  {
575  button.Enabled = false;
576  return false;
577  }
578 
579  return true;
580  }, Campaign.PurchasedLostShuttles || !HasPermission || GameMain.GameSession?.SubmarineInfo == null || !GameMain.GameSession.SubmarineInfo.SubsLeftBehind, isHovered =>
581  {
582  if (!isHovered) { return false; }
583  if (!(GameMain.GameSession?.SubmarineInfo is { } subInfo)) { return false; }
584 
585  foreach (var (item, itemFrame) in itemPreviews)
586  {
587  if (subInfo.LeftBehindDockingPortIDs.Contains(item.ID))
588  {
589  itemFrame.OutlineColor = itemFrame.Color = subInfo.BlockedDockingPortIDs.Contains(item.ID) ? GUIStyle.Red : GUIStyle.Green;
590  }
591  else
592  {
593  itemFrame.OutlineColor = itemFrame.Color = previewWhite;
594  }
595  }
596  return true;
597  }, disableElement: true);
598  }
599 
600  private void CreateRepairEntry(GUIComponent parent, LocalizedString title, string imageStyle, int price, GUIButton.OnClickedHandler onPressed, bool isDisabled, Func<bool, bool>? onHover = null, bool disableElement = false)
601  {
602  GUIFrame frameChild = new GUIFrame(rectT(new Point(parent.Rect.Width, (int) (96 * GUI.Scale)), parent), style: "UpgradeUIFrame");
603  frameChild.SelectedColor = frameChild.Color;
604 
605  // Kinda hacky? idk, I don't see any other way to bring an Update() function to the campaign store.
606  new GUICustomComponent(rectT(1, 1, frameChild), onUpdate: UpdateHover) { CanBeFocused = false };
607 
608  /* REPAIR ENTRY
609  * |-------------------------------------------------|
610  * | | repair title | |
611  * | icon |---------------------------| buy btn. |
612  * | | xxx mk | |
613  * |-------------------------------------------------|
614  */
615  GUILayoutGroup contentLayout = new GUILayoutGroup(rectT(0.9f, 0.85f, frameChild, Anchor.Center), isHorizontal: true);
616  var repairIcon = new GUIFrame(rectT(new Point(contentLayout.Rect.Height, contentLayout.Rect.Height), contentLayout), style: imageStyle);
617  GUILayoutGroup textLayout = new GUILayoutGroup(rectT(0.8f - repairIcon.RectTransform.RelativeSize.X, 1, contentLayout)) { Stretch = true };
618  new GUITextBlock(rectT(1, 0, textLayout), title, font: GUIStyle.SubHeadingFont) { CanBeFocused = false, AutoScaleHorizontal = true };
619  new GUITextBlock(rectT(1, 0, textLayout), TextManager.FormatCurrency(price));
620  GUILayoutGroup buyButtonLayout = new GUILayoutGroup(rectT(0.2f, 1, contentLayout), childAnchor: Anchor.Center) { UserData = UpgradeStoreUserData.BuyButtonLayout };
621  new GUIButton(rectT(0.7f, 0.5f, buyButtonLayout), string.Empty, style: "RepairBuyButton") { Enabled = PlayerBalance >= price && !isDisabled, OnClicked = onPressed };
622  contentLayout.Recalculate();
623  buyButtonLayout.Recalculate();
624 
625  if (disableElement)
626  {
627  frameChild.Enabled = PlayerBalance >= price && !isDisabled;
628  }
629 
630  if (!HasPermission)
631  {
632  frameChild.Enabled = false;
633  }
634 
635  void UpdateHover(float deltaTime, GUICustomComponent component)
636  {
637  onHover?.Invoke(GUI.MouseOn != null && frameChild.IsParentOf(GUI.MouseOn) || GUI.MouseOn == frameChild);
638  }
639  }
640 
641  //TODO: put this somewhere else
642  public static GUIListBox CreateUpgradeCategoryList(RectTransform rectTransform)
643  {
644  var upgradeCategoryList = new GUIListBox(rectTransform, style: null)
645  {
646  AutoHideScrollBar = false,
647  ScrollBarVisible = false,
648  HideChildrenOutsideFrame = false,
649  SmoothScroll = true,
650  FadeElements = true,
651  PadBottom = true,
652  SelectTop = true,
653  ClampScrollToElements = true,
654  Spacing = 8,
655  PlaySoundOnSelect = true
656  };
657 
658  Dictionary<UpgradeCategory, List<UpgradePrefab>> upgrades = new Dictionary<UpgradeCategory, List<UpgradePrefab>>();
659 
660  foreach (UpgradeCategory category in UpgradeCategory.Categories.OrderBy(c => c.Name))
661  {
662  foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs.OrderBy(p => p.Name))
663  {
664  if (prefab.UpgradeCategories.Contains(category))
665  {
666  if (upgrades.ContainsKey(category))
667  {
668  upgrades[category].Add(prefab);
669  }
670  else
671  {
672  upgrades.Add(category, new List<UpgradePrefab> { prefab });
673  }
674  }
675  }
676  }
677 
678  foreach (var (category, prefabs) in upgrades)
679  {
680  var frameChild = new GUIFrame(rectT(1, 0.15f, upgradeCategoryList.Content), style: "UpgradeUIFrame")
681  {
682  UserData = new CategoryData(category, prefabs),
683  GlowOnSelect = true
684  };
685 
686  frameChild.DefaultColor = frameChild.Color;
687  frameChild.Color = Color.Transparent;
688 
689  var weaponSwitchBg = new GUIButton(new RectTransform(new Vector2(0.65f), frameChild.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.Smallest)
690  { RelativeOffset = new Vector2(0.04f, 0.0f) }, style: "WeaponSwitchTab")
691  {
692  Visible = false,
693  CanBeSelected = false,
694  UserData = "customizebutton"
695  };
696  weaponSwitchBg.DefaultColor = weaponSwitchBg.Frame.DefaultColor = weaponSwitchBg.Color;
697  var weaponSwitchImg = new GUIImage(new RectTransform(new Vector2(0.7f), weaponSwitchBg.RectTransform, Anchor.Center), "WeaponSwitchIcon", scaleToFit: true)
698  {
699  CanBeFocused = false
700  };
701  weaponSwitchImg.DefaultColor = weaponSwitchImg.Color;
702 
703  /* UPGRADE CATEGORY
704  * |--------------------------------------------------------|
705  * | |
706  * | category title |--------------------------|
707  * | | indicators |
708  * |-----------------------------|--------------------------|
709  */
710  GUILayoutGroup contentLayout = new GUILayoutGroup(rectT(0.9f, 0.85f, frameChild, Anchor.Center));
711  var itemCategoryLabel = new GUITextBlock(rectT(1, 1, contentLayout), category.Name, font: GUIStyle.SubHeadingFont) { CanBeFocused = false };
712  GUILayoutGroup indicatorLayout = new GUILayoutGroup(rectT(0.5f, 0.25f, contentLayout, Anchor.BottomRight), isHorizontal: true, childAnchor: Anchor.TopRight) { UserData = "indicators", IgnoreLayoutGroups = true, RelativeSpacing = 0.01f };
713 
714  foreach (var prefab in prefabs)
715  {
716  GUIImage upgradeIndicator = new GUIImage(rectT(0.1f, 1f, indicatorLayout), style: "UpgradeIndicator", scaleToFit: true) { UserData = prefab, CanBeFocused = false };
717  upgradeIndicator.DefaultColor = upgradeIndicator.Color;
718  upgradeIndicator.Color = Color.Transparent;
719  }
720 
721  itemCategoryLabel.DefaultColor = itemCategoryLabel.TextColor;
722  itemCategoryLabel.TextColor = Color.Transparent;
723 
724  contentLayout.Recalculate();
725  indicatorLayout.Recalculate();
726  }
727 
728  return upgradeCategoryList;
729  }
730 
731  private void CreateUpgradeTab()
732  {
733  if (storeLayout == null || mainStoreLayout == null) { return; }
734  currentStoreLayout = CreateUpgradeCategoryList(rectT(1.0f, 1.5f, storeLayout));
735 
736  selectedUpgradeCategoryLayout = new GUIFrame(rectT(0.3f * GUI.AspectRatioAdjustment, 1, mainStoreLayout), style: null) { CanBeFocused = false };
737 
738  RefreshUpgradeList();
739 
740  currentStoreLayout.OnSelected += (component, userData) =>
741  {
742  if (!component.Enabled)
743  {
744  selectedUpgradeCategoryLayout?.ClearChildren();
745  foreach (GUIComponent itemFrame in itemPreviews.Values)
746  {
747  itemFrame.OutlineColor = itemFrame.Color = previewWhite;
748  itemFrame.Children.ForEach(c => c.Color = itemFrame.Color);
749  }
750  return true;
751  }
752 
753  if (userData is CategoryData categoryData && Submarine.MainSub is { } sub && categoryData.Prefabs is { } prefabs)
754  {
755  TrySelectCategory(prefabs, categoryData.Category, sub);
756  }
757 
758  var customizeCategoryButton = selectedUpgradeCategoryLayout?.FindChild("customizebutton", recursive: true) as GUIButton;
759  customizeCategoryButton?.OnClicked(customizeCategoryButton, customizeCategoryButton.UserData);
760 
761  return true;
762  };
763  }
764 
765  // This was supposed to have some logic for fancy animations to slide the previous tab out but maybe another time
766  private void TrySelectCategory(List<UpgradePrefab> prefabs, UpgradeCategory category, Submarine submarine) => SelectUpgradeCategory(prefabs, category, submarine);
767 
768  private bool customizeTabOpen;
769 
770  private static bool HasSwappableItems(UpgradeCategory category, List<Item>? subItems = null)
771  {
772  if (Submarine.MainSub == null) { return false; }
773  subItems ??= GetSubItems();
774  return subItems.Any(i =>
775  i.Prefab.SwappableItem != null &&
776  !i.IsHidden && i.AllowSwapping &&
777  (i.Prefab.SwappableItem.CanBeBought || ItemPrefab.Prefabs.Any(ip => ip.SwappableItem?.ReplacementOnUninstall == i.Prefab.Identifier)) &&
778  Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && category.ItemTags.Any(t => i.HasTag(t)));
779  }
780 
781  private static List<Item> GetSubItems() => Submarine.MainSub?.GetItems(true) ?? new List<Item>();
782 
783  private void SelectUpgradeCategory(List<UpgradePrefab> prefabs, UpgradeCategory category, Submarine submarine)
784  {
785  if (selectedUpgradeCategoryLayout == null) { return; }
786 
787  customizeTabOpen = false;
788 
789  GUIComponent[] categoryFrames = GetFrames(category);
790  foreach (GUIComponent itemFrame in itemPreviews.Values)
791  {
792  itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUIStyle.Orange : previewWhite;
793  itemFrame.Children.ForEach(c => c.Color = itemFrame.Color);
794  }
795 
796  highlightWalls = category.IsWallUpgrade;
797 
798  selectedUpgradeCategoryLayout.ClearChildren();
799  GUIFrame frame = new GUIFrame(rectT(1.0f, 0.4f, selectedUpgradeCategoryLayout));
800  GUIFrame paddedFrame = new GUIFrame(rectT(0.93f, 0.9f, frame, Anchor.Center), style: null);
801 
802  bool hasSwappableItems = HasSwappableItems(category);
803 
804  float listHeight = hasSwappableItems ? 0.9f : 1.0f;
805 
806  GUIListBox prefabList = new GUIListBox(rectT(1.0f, listHeight, paddedFrame, Anchor.BottomLeft))
807  {
808  UserData = "prefablist",
809  AutoHideScrollBar = false,
810  ScrollBarVisible = true
811  };
812 
813  if (hasSwappableItems)
814  {
815  GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1.0f, 0.1f, paddedFrame, anchor: Anchor.TopLeft), isHorizontal: true);
816 
817  GUIButton customizeButton = new GUIButton(rectT(0.5f, 1f, buttonLayout), text: TextManager.Get("uicategory.customize"), style: "GUITabButton")
818  {
819  UserData = "customizebutton"
820  };
821  new GUIImage(new RectTransform(new Vector2(1.0f, 0.75f), customizeButton.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest) { RelativeOffset = new Vector2(0.015f, 0.0f) }, "WeaponSwitchIcon", scaleToFit: true);
822  customizeButton.TextBlock.RectTransform.RelativeSize = new Vector2(0.7f, 1.0f);
823 
824  GUIButton upgradeButton = new GUIButton(rectT(0.5f, 1f, buttonLayout), text: TextManager.Get("uicategory.upgrades"), style: "GUITabButton")
825  {
826  Selected = true
827  };
828 
829  GUITextBlock.AutoScaleAndNormalize(upgradeButton.TextBlock, customizeButton.TextBlock);
830 
831  upgradeButton.OnClicked = delegate
832  {
833  customizeTabOpen = false;
834  customizeButton.Selected = false;
835  upgradeButton.Selected = true;
836  CreateUpgradePrefabList(prefabList, category, prefabs, submarine);
837  GUIComponent[] categoryFrames = GetFrames(category);
838  foreach (GUIComponent itemFrame in itemPreviews.Values)
839  {
840  itemFrame.OutlineColor = itemFrame.Color = categoryFrames.Contains(itemFrame) ? GUIStyle.Orange : previewWhite;
841  itemFrame.Children.ForEach(c => c.Color = itemFrame.Color);
842  }
843  return true;
844  };
845 
846  customizeButton.OnClicked = delegate
847  {
848  customizeTabOpen = true;
849  customizeButton.Selected = true;
850  upgradeButton.Selected = false;
851  CreateSwappableItemList(prefabList, category, submarine);
852  return true;
853  };
854  }
855 
856  CreateUpgradePrefabList(prefabList, category, prefabs, submarine);
857  }
858 
859  private void CreateUpgradePrefabList(GUIListBox parent, UpgradeCategory category, List<UpgradePrefab> prefabs, Submarine submarine)
860  {
861  parent.Content.ClearChildren();
862  List<Item>? entitiesOnSub = null;
863  if (!category.IsWallUpgrade)
864  {
865  entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true)).ToList();
866  }
867 
868  foreach (UpgradePrefab prefab in prefabs)
869  {
870  if (prefab.GetMaxLevelForCurrentSub() == 0) { continue; }
871  CreateUpgradeEntry(prefab, category, parent.Content, submarine, entitiesOnSub);
872  }
873  }
874 
875  private void CreateSwappableItemList(GUIListBox parent, UpgradeCategory category, Submarine submarine)
876  {
877  parent.Content.ClearChildren();
878  currentUpgradeCategory = category;
879  var entitiesOnSub = submarine.GetItems(true).Where(i => submarine.IsEntityFoundOnThisSub(i, true) && !i.IsHidden && i.AllowSwapping && i.Prefab.SwappableItem != null && category.ItemTags.Any(t => i.HasTag(t))).ToList();
880 
881  foreach (Item item in entitiesOnSub)
882  {
883  CreateSwappableItemSlideDown(parent, item, entitiesOnSub, submarine);
884  }
885  }
886 
887  private void CreateSwappableItemSlideDown(GUIListBox parent, Item item, List<Item> swappableEntities, Submarine submarine)
888  {
889  if (Campaign == null || submarine == null) { return; }
890 
891  IEnumerable<ItemPrefab> availableReplacements = MapEntityPrefab.List.Where(p =>
892  p is ItemPrefab itemPrefab &&
893  itemPrefab.SwappableItem != null &&
894  itemPrefab.SwappableItem.CanBeBought &&
895  itemPrefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase)).Cast<ItemPrefab>();
896 
897  var linkedItems = UpgradeManager.GetLinkedItemsToSwap(item) ?? new List<Item>() { item };
898  //create the swap entry only for one of the items (the one with the smallest ID)
899  if (linkedItems.Min(it => it.ID) < item.ID) { return; }
900 
901  var currentOrPending = item.PendingItemSwap ?? item.Prefab;
902  LocalizedString name = currentOrPending.Name;
903  LocalizedString nameWithQuantity = "";
904  if (linkedItems.Count > 1)
905  {
906  foreach (ItemPrefab distinctItem in linkedItems.Select(it => it.Prefab).Distinct())
907  {
908  if (nameWithQuantity != string.Empty)
909  {
910  nameWithQuantity += ", ";
911  }
912  int count = linkedItems.Count(it => it.Prefab == distinctItem);
913  nameWithQuantity += distinctItem.Name;
914  if (count > 1)
915  {
916  nameWithQuantity += " " + TextManager.GetWithVariable("campaignstore.quantity", "[amount]", count.ToString());
917  }
918  }
919  }
920  else
921  {
922  nameWithQuantity = name;
923  }
924 
925  bool isOpen = false;
926  GUIButton toggleButton = new GUIButton(rectT(1f, 0.1f, parent.Content), text: string.Empty, style: "SlideDown")
927  {
928  UserData = item
929  };
930  GUILayoutGroup buttonLayout = new GUILayoutGroup(rectT(1f, 1f, toggleButton.Frame), isHorizontal: true);
931 
932  LocalizedString slotText = "";
933  if (linkedItems.Count() > 1)
934  {
935  slotText = TextManager.GetWithVariable("weaponslot", "[number]", string.Join(", ", linkedItems.Select(it => (swappableEntities.IndexOf(it) + 1).ToString())));
936  }
937  else
938  {
939  slotText = TextManager.GetWithVariable("weaponslot", "[number]", (swappableEntities.IndexOf(item) + 1).ToString());
940  }
941 
942  new GUITextBlock(rectT(0.3f, 1f, buttonLayout), text: slotText, font: GUIStyle.SubHeadingFont);
943  GUILayoutGroup group = new GUILayoutGroup(rectT(0.7f, 1f, buttonLayout), isHorizontal: true) { Stretch = true };
944 
945  var title = item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : nameWithQuantity;
946  GUITextBlock text = new GUITextBlock(rectT(0.7f, 1f, group), text: RichString.Rich(title), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right)
947  {
948  TextColor = GUIStyle.Orange
949  };
950  GUIImage arrowImage = new GUIImage(rectT(0.5f, 1f, group, scaleBasis: ScaleBasis.BothHeight), style: "SlideDownArrow", scaleToFit: true);
951 
952  group.Recalculate();
953  if (text.TextSize.X > text.Rect.Width)
954  {
955  text.ToolTip = text.Text;
956  text.Text = ToolBox.LimitString(text.Text, text.Font, text.Rect.Width);
957  }
958 
959  List<GUIFrame> frames = new List<GUIFrame>();
960  if (currentOrPending != null)
961  {
962  bool canUninstall = item.PendingItemSwap != null || !(currentOrPending.SwappableItem?.ReplacementOnUninstall.IsEmpty ?? true);
963 
964  bool isUninstallPending = item.Prefab.SwappableItem != null && item.PendingItemSwap?.Identifier == item.Prefab.SwappableItem.ReplacementOnUninstall;
965  if (isUninstallPending) { canUninstall = false; }
966 
967  frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), currentOrPending.UpgradePreviewSprite,
968  item.PendingItemSwap != null ? TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", name) : TextManager.GetWithVariable("upgrades.installeditem", "[itemname]", nameWithQuantity),
969  currentOrPending.Description,
970  0, null, addBuyButton: canUninstall, addProgressBar: false, buttonStyle: "WeaponUninstallButton").Frame);
971 
972  if (canUninstall && frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton refundButton)
973  {
974  refundButton.Enabled = true;
975  refundButton.OnClicked += (button, o) =>
976  {
977  string textTag = item.PendingItemSwap != null ? "upgrades.cancelitemswappromptbody" : "upgrades.itemuninstallpromptbody";
978  if (isUninstallPending) { textTag = "upgrades.cancelitemuninstallpromptbody"; }
979  LocalizedString promptBody = TextManager.GetWithVariable(textTag, "[itemtouninstall]", isUninstallPending ? item.Name : currentOrPending.Name);
980  currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("upgrades.refundprompttitle"), promptBody, () =>
981  {
982  if (GameMain.NetworkMember != null)
983  {
984  WaitForServerUpdate = true;
985  }
986  Campaign?.UpgradeManager.CancelItemSwap(item);
987  GameMain.Client?.SendCampaignState();
988  return true;
989  });
990  return true;
991  };
992  }
993 
994  var dividerContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), parent.Content.RectTransform), style: null);
995  new GUIFrame(new RectTransform(new Vector2(0.8f, 0.5f), dividerContainer.RectTransform, Anchor.Center), style: "HorizontalLine");
996  frames.Add(dividerContainer);
997  }
998 
999  foreach (ItemPrefab replacement in availableReplacements)
1000  {
1001  if (replacement == currentOrPending) { continue; }
1002 
1003  bool isPurchased = item.AvailableSwaps.Contains(replacement);
1004 
1005  int price = isPurchased || replacement == item.Prefab ? 0 : replacement.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count();
1006 
1007  frames.Add(CreateUpgradeEntry(rectT(1f, 0.35f, parent.Content), replacement.UpgradePreviewSprite, replacement.Name, replacement.Description,
1008  price, replacement,
1009  addBuyButton: true,
1010  addProgressBar: false,
1011  buttonStyle: isPurchased ? "WeaponInstallButton" : "StoreAddToCrateButton").Frame);
1012 
1013  if (!(frames.Last().FindChild(c => c is GUIButton, recursive: true) is GUIButton buyButton)) { continue; }
1014  if (PlayerBalance >= price)
1015  {
1016  buyButton.Enabled = true;
1017  buyButton.OnClicked += (button, o) =>
1018  {
1019  LocalizedString promptBody = TextManager.GetWithVariables(isPurchased ? "upgrades.itemswappromptbody" : "upgrades.purchaseitemswappromptbody",
1020  ("[itemtoinstall]", replacement.Name),
1021  ("[amount]", (replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation) * linkedItems.Count()).ToString()));
1022  currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () =>
1023  {
1024  if (GameMain.NetworkMember != null)
1025  {
1026  WaitForServerUpdate = true;
1027  }
1028  if (item.Prefab == replacement && item.PendingItemSwap != null)
1029  {
1030  Campaign?.UpgradeManager.CancelItemSwap(item);
1031  }
1032  else
1033  {
1034  Campaign?.UpgradeManager.PurchaseItemSwap(item, replacement);
1035  }
1036  GameMain.Client?.SendCampaignState();
1037  return true;
1038  });
1039 
1040  return true;
1041  };
1042  }
1043  else
1044  {
1045  buyButton.Enabled = false;
1046  }
1047  }
1048 
1049  foreach (GUIFrame frame in frames)
1050  {
1051  frame.Visible = false;
1052  }
1053 
1054  toggleButton.OnClicked = delegate
1055  {
1056  if (Campaign == null) { return false; }
1057  isOpen = !isOpen;
1058  toggleButton.Selected = !toggleButton.Selected;
1059  foreach (GUIFrame frame in frames)
1060  {
1061  frame.Visible = toggleButton.Selected;
1062  }
1063  if (toggleButton.Selected)
1064  {
1065  var linkedItems = UpgradeManager.GetLinkedItemsToSwap(item);
1066  foreach (var itemPreview in itemPreviews)
1067  {
1068  itemPreview.Value.OutlineColor = itemPreview.Value.Color = linkedItems.Contains(itemPreview.Key) ? GUIStyle.Orange : previewWhite;
1069  }
1070  foreach (GUIComponent otherComponent in toggleButton.Parent.Children)
1071  {
1072  if (otherComponent == toggleButton || frames.Contains(otherComponent)) { continue; }
1073  if (otherComponent is GUIButton otherButton)
1074  {
1075  var otherArrowImage = otherComponent.FindChild(c => c is GUIImage, recursive: true);
1076  otherArrowImage.SpriteEffects = SpriteEffects.None;
1077  otherButton.Selected = false;
1078  }
1079  else
1080  {
1081  otherComponent.Visible = false;
1082  }
1083  }
1084  }
1085  else
1086  {
1087  foreach (var itemPreview in itemPreviews)
1088  {
1089  if (currentStoreLayout?.SelectedData is CategoryData categoryData && !categoryData.Category.ItemTags.Any(t => itemPreview.Key.HasTag(t))) { continue; }
1090  itemPreview.Value.OutlineColor = itemPreview.Value.Color = GUIStyle.Orange;
1091  }
1092  }
1093  activeItemSwapSlideDown = toggleButton.Selected ? toggleButton : null;
1094  arrowImage.SpriteEffects = toggleButton.Selected ? SpriteEffects.FlipVertically : SpriteEffects.None;
1095  parent.RecalculateChildren();
1096  parent.UpdateScrollBarSize();
1097  return true;
1098  };
1099  }
1100 
1101  public readonly record struct BuyButtonFrame(GUILayoutGroup Layout, GUIListBox MaterialCostList, GUIButton BuyButton, GUITextBlock PriceText);
1102  public readonly record struct ProgressBarFrame(GUITextBlock ProgressText, GUIProgressBar ProgressBar);
1103 
1104  public readonly record struct UpgradeFrame(GUIFrame Frame,
1105  GUIImage Icon,
1106  GUITextBlock Name,
1107  GUITextBlock Description,
1108  Option<BuyButtonFrame> BuyButton,
1109  Option<ProgressBarFrame> ProgressBar);
1110 
1111  public static UpgradeFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true)
1112  {
1113  int price = prefab.Price.GetBuyPrice(prefab, campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList);
1114  return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category));
1115  }
1116 
1117  public static UpgradeFrame CreateUpgradeEntry(RectTransform parent, Sprite sprite, LocalizedString title, LocalizedString body, int price, object? userData, bool addBuyButton = true, bool addProgressBar = true, string buttonStyle = "UpgradeBuyButton", UpgradePrefab? upgradePrefab = null, int currentLevel = 0)
1118  {
1119  float progressBarHeight = 0.25f;
1120 
1121  if (!addProgressBar)
1122  {
1123  progressBarHeight = 0f;
1124  }
1125  /* UPGRADE PREFAB ENTRY
1126  * |------------------------------------------------------------------|
1127  * | | title | price |
1128  * | |----------------------------------|_______________|
1129  * | icon | description | |
1130  * | |----------------------------------| buy btn. |
1131  * | | progress bar | x / y | |
1132  * |------------------------------------------------------------------|
1133  */
1134  GUIFrame prefabFrame = new GUIFrame(parent, style: "ListBoxElement") { SelectedColor = Color.Transparent, UserData = userData };
1135  GUILayoutGroup mainLayout = new GUILayoutGroup(rectT(0.98f, 0.95f, prefabFrame, Anchor.Center), isHorizontal: false);
1136  GUILayoutGroup prefabLayout = new GUILayoutGroup(rectT(1f, addBuyButton ? 0.65f : 1f, mainLayout, Anchor.Center), isHorizontal: true) { Stretch = true };
1137  GUILayoutGroup imageLayout = new GUILayoutGroup(rectT(new Point(prefabLayout.Rect.Height, prefabLayout.Rect.Height), prefabLayout), childAnchor: Anchor.Center);
1138  var icon = new GUIImage(rectT(0.9f, 0.9f, imageLayout, scaleBasis: ScaleBasis.BothHeight), sprite, scaleToFit: true) { CanBeFocused = false };
1139  GUILayoutGroup textLayout = new GUILayoutGroup(rectT(1f - imageLayout.RectTransform.RelativeSize.X, 1, prefabLayout));
1140  var name = new GUITextBlock(rectT(1, 0.35f, textLayout), RichString.Rich(title), font: GUIStyle.SubHeadingFont) { AutoScaleHorizontal = true, AutoScaleVertical = true, Padding = Vector4.Zero };
1141  //name.RectTransform.MinSize = new Point(0, (int)name.TextSize.Y);
1142  GUILayoutGroup descriptionLayout = new GUILayoutGroup(rectT(1, 0.75f - progressBarHeight, textLayout));
1143  var description = new GUITextBlock(rectT(1, 1, descriptionLayout), body, font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.TopLeft) { Padding = Vector4.Zero };
1144  GUILayoutGroup? progressLayout = null;
1145  GUILayoutGroup? buyButtonLayout = null;
1146 
1147  Option<BuyButtonFrame> buyButtonOption = Option<BuyButtonFrame>.None();
1148  Option<ProgressBarFrame> progressBarOption = Option<ProgressBarFrame>.None();
1149 
1150  if (addProgressBar)
1151  {
1152  progressLayout = new GUILayoutGroup(rectT(1, 0.25f, textLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = UpgradeStoreUserData.ProgressBarLayout };
1153  GUITextBlock progressText = new GUITextBlock(rectT(0.15f, 1, progressLayout), string.Empty, font: GUIStyle.SmallFont, textAlignment: Alignment.Center) { Padding = Vector4.Zero };
1154  GUIProgressBar progressBar = new GUIProgressBar(rectT(0.85f, 0.75f, progressLayout), 0.0f, GUIStyle.Orange);
1155  progressBarOption = Option.Some(new ProgressBarFrame(progressText, progressBar));
1156  }
1157 
1158  if (addBuyButton)
1159  {
1160  var formattedPrice = TextManager.FormatCurrency(Math.Abs(price));
1161  //negative price = refund
1162  if (price < 0) { formattedPrice = "+" + formattedPrice; }
1163  buyButtonLayout = new GUILayoutGroup(rectT(1f, 0.35f, mainLayout), isHorizontal: true) { UserData = UpgradeStoreUserData.BuyButtonLayout };;
1164 
1165  GUIListBox materialCostList;
1166  if (upgradePrefab is not null)
1167  {
1168  var increaseText = new GUITextBlock(rectT(imageLayout.RectTransform.RelativeSize.X, 1f, buyButtonLayout), "", textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont)
1169  {
1170  UserData = UpgradeStoreUserData.IncreaseLabel
1171  };
1172  UpdateUpgradePercentageText(increaseText, upgradePrefab, currentLevel);
1173  materialCostList = new GUIListBox(rectT(0.65f - imageLayout.RectTransform.RelativeSize.X, 1f, buyButtonLayout), isHorizontal: true, style: null);
1174  }
1175  else
1176  {
1177  materialCostList = new GUIListBox(rectT(0.65f, 1f, buyButtonLayout), isHorizontal: true, style: null);
1178  }
1179 
1180  materialCostList.Visible = false;
1181  materialCostList.UserData = UpgradeStoreUserData.MaterialCostList;
1182 
1183  var priceText = new GUITextBlock(rectT(0.2f, 1f, buyButtonLayout), formattedPrice, textAlignment: Alignment.CenterRight)
1184  {
1185  UserData = UpgradeStoreUserData.PriceLabel,
1186  //prices on swappable items are always visible, upgrade prices are enabled in UpdateUpgradeEntry for purchasable upgrades
1187  Visible = userData is ItemPrefab
1188  };
1189 
1190  if (price < 0)
1191  {
1192  priceText.TextColor = GUIStyle.Green;
1193  }
1194  else if (price == 0)
1195  {
1196  priceText.Text = string.Empty;
1197  }
1198  GUIButton buyButton = new GUIButton(rectT(0.15f, 1f, buyButtonLayout), string.Empty, style: buttonStyle)
1199  {
1200  UserData = UpgradeStoreUserData.BuyButton,
1201  Enabled = false
1202  };
1203 
1204  buyButtonOption = Option.Some(new BuyButtonFrame(buyButtonLayout, materialCostList, buyButton, priceText));
1205  }
1206 
1207  description.CalculateHeightFromText();
1208  // cut the description if it overflows and add a tooltip to it
1209  for (int i = 100; i > 0 && description.Rect.Height > descriptionLayout.Rect.Height; i--)
1210  {
1211  var lines = description.WrappedText.Split('\n');
1212  var newString = string.Join('\n', lines.Take(lines.Count - 1));
1213  if (0 >= newString.Length - 4) { break; }
1214 
1215  description.Text = newString.Substring(0, newString.Length - 4) + "...";
1216  description.CalculateHeightFromText();
1217  description.ToolTip = body;
1218  }
1219 
1220  // Recalculate everything to prevent jumping
1221  if (parent.Parent.GUIComponent is GUILayoutGroup group) { group.Recalculate(); }
1222 
1223  descriptionLayout.Recalculate();
1224  prefabLayout.Recalculate();
1225  imageLayout.Recalculate();
1226  textLayout.Recalculate();
1227  progressLayout?.Recalculate();
1228  buyButtonLayout?.Recalculate();
1229 
1230  return new UpgradeFrame(prefabFrame, icon, name, description, buyButtonOption, progressBarOption);
1231  }
1232 
1233  private static void UpdateUpgradePercentageText(GUITextBlock text, UpgradePrefab upgradePrefab, int currentLevel)
1234  {
1235  int maxLevel = upgradePrefab.GetMaxLevelForCurrentSub();
1236  float nextIncrease = upgradePrefab.IncreaseOnTooltip * Math.Min(currentLevel + 1, maxLevel);
1237  if (nextIncrease != 0f)
1238  {
1239  text.Text = $"{Math.Round(nextIncrease, 1)} %";
1240  if (currentLevel == maxLevel)
1241  {
1242  text.TextColor = Color.Gray;
1243  }
1244  }
1245  }
1246 
1247  private void CreateUpgradeEntry(UpgradePrefab prefab, UpgradeCategory category, GUIComponent parent, Submarine submarine, List<Item>? itemsOnSubmarine)
1248  {
1249  Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub;
1250  if (Campaign is null || sub is null) { return; }
1251 
1252  UpgradeFrame prefabFrame = CreateUpgradeFrame(prefab, category, Campaign, rectT(1f, 0.4f, parent));
1253 
1254  if (!prefabFrame.BuyButton.TryUnwrap(out BuyButtonFrame buyButtonFrame)) { return; }
1255 
1256  if (!HasPermission || !prefab.IsApplicable(submarine.Info) || (itemsOnSubmarine != null && !itemsOnSubmarine.Any(it => category.CanBeApplied(it, prefab))))
1257  {
1258  prefabFrame.Frame.Enabled = false;
1259  prefabFrame.Description.Enabled = false;
1260  prefabFrame.Name.Enabled = false;
1261  prefabFrame.Icon.Color = Color.Gray;
1262  buyButtonFrame.BuyButton.Enabled = false;
1263  buyButtonFrame.Layout.UserData = null; // prevent UpdateUpgradeEntry() from enabling the button
1264  }
1265 
1266  buyButtonFrame.BuyButton.OnClicked += (button, o) =>
1267  {
1268  LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody",
1269  ("[upgradename]", prefab.Name),
1270  ("[amount]", prefab.Price.GetBuyPrice(prefab, Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation, characterList).ToString()));
1271  currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () =>
1272  {
1273  if (GameMain.NetworkMember != null)
1274  {
1275  WaitForServerUpdate = true;
1276  }
1277  Campaign.UpgradeManager.PurchaseUpgrade(prefab, category);
1278  GameMain.Client?.SendCampaignState();
1279  return true;
1280  }, overrideConfirmButtonSound: GUISoundType.ConfirmTransaction);
1281 
1282  return true;
1283  };
1284 
1285  UpdateUpgradeEntry(prefabFrame.Frame, prefab, category, Campaign);
1286  }
1287 
1288  private void CreateItemTooltip(MapEntity entity)
1289  {
1290  int slotIndex = -1;
1291  if (entity is Item swappableItem && swappableItem.Prefab.SwappableItem != null)
1292  {
1293  var entitiesOnSub = Submarine.MainSub.GetItems(true).Where(i => i.Prefab.SwappableItem != null && Submarine.MainSub.IsEntityFoundOnThisSub(i, true) && i.Prefab.SwappableItem.SwapIdentifier == swappableItem.Prefab.SwappableItem?.SwapIdentifier).ToList();
1294  slotIndex = entitiesOnSub.IndexOf(entity) + 1;
1295  }
1296 
1297  GUITextBlock? itemName = ItemInfoFrame.FindChild("itemname", true) as GUITextBlock;
1298  GUIListBox? upgradeList = ItemInfoFrame.FindChild("upgradelist", true) as GUIListBox;
1299  GUITextBlock? moreIndicator = ItemInfoFrame.FindChild("moreindicator", true) as GUITextBlock;
1300  GUILayoutGroup layout = ItemInfoFrame.GetChild<GUILayoutGroup>();
1301  Debug.Assert(itemName != null && upgradeList != null && moreIndicator != null && layout != null, "One ore more tooltip elements not found");
1302 
1303  List<Upgrade> upgrades = entity.GetUpgrades();
1304  int upgradesCount = upgrades.Count;
1305  const int maxUpgrades = 4;
1306 
1307  Item? item = entity as Item;
1308  itemName.Text = item?.Prefab.Name ?? TextManager.Get("upgradecategory.walls");
1309  if (slotIndex > -1)
1310  {
1311  itemName.Text = TextManager.GetWithVariables("weaponslotwithname", ("[number]", slotIndex.ToString()), ("[weaponname]", itemName.Text));
1312  }
1313  if (item?.PendingItemSwap != null)
1314  {
1315  itemName.Text = RichString.Rich(itemName.Text + "\n" + TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", item.PendingItemSwap.Name));
1316  }
1317  upgradeList.Content.ClearChildren();
1318  for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++)
1319  {
1320  Upgrade upgrade = upgrades[i];
1321  new GUITextBlock(rectT(1, 0.25f, upgradeList.Content), CreateListEntry(upgrade.Prefab.Name, upgrade.Level)) { AutoScaleHorizontal = true, UserData = Tuple.Create(upgrade.Level, upgrade.Prefab) };
1322  }
1323 
1324  if (!(Campaign?.UpgradeManager is { } upgradeManager)) { return; }
1325 
1326  // include pending upgrades into the tooltip
1327  foreach (var (prefab, category, level) in upgradeManager.PendingUpgrades)
1328  {
1329  if (item != null && category.CanBeApplied(item, prefab) || entity is Structure && category.IsWallUpgrade)
1330  {
1331  bool found = false;
1332  foreach (GUITextBlock textBlock in upgradeList.Content.Children.Where(c => c is GUITextBlock).Cast<GUITextBlock>())
1333  {
1334  if (textBlock.UserData is Tuple<int, UpgradePrefab> tuple && tuple.Item2 == prefab)
1335  {
1336  var tooltip = CreateListEntry(tuple.Item2.Name, level + tuple.Item1);
1337  textBlock.Text = tooltip;
1338  found = true;
1339  break;
1340  }
1341  }
1342 
1343  if (!found)
1344  {
1345  upgradesCount++;
1346  if (upgradeList.Content.CountChildren < maxUpgrades)
1347  {
1348  new GUITextBlock(rectT(1, 0.25f, upgradeList.Content), CreateListEntry(prefab.Name, level)) { AutoScaleHorizontal = true };
1349  }
1350  }
1351  }
1352  }
1353 
1354  if (!upgradeList.Content.Children.Any())
1355  {
1356  new GUITextBlock(rectT(1, 0.25f, upgradeList.Content), TextManager.Get("UpgradeUITooltip.NoUpgradesElement")) { AutoScaleHorizontal = true };
1357  }
1358 
1359  moreIndicator.Text = upgradesCount > maxUpgrades ? TextManager.GetWithVariable("upgradeuitooltip.moreindicator", "[amount]", $"{upgradesCount - maxUpgrades}") : string.Empty;
1360 
1361  itemName.CalculateHeightFromText();
1362  moreIndicator.CalculateHeightFromText();
1363  layout.Recalculate();
1364 
1365  static LocalizedString CreateListEntry(LocalizedString name, int level) => TextManager.GetWithVariables("upgradeuitooltip.upgradelistelement", ("[upgradename]", name), ("[level]", $"{level}"));
1366  }
1367 
1368  public static IEnumerable<UpgradeCategory> GetApplicableCategories(Submarine drawnSubmarine)
1369  {
1370  Item[] entitiesOnSub = drawnSubmarine.GetItems(true).Where(i => drawnSubmarine.IsEntityFoundOnThisSub(i, true)).ToArray();
1371  foreach (UpgradeCategory category in UpgradeCategory.Categories)
1372  {
1373  //hide categories with no upgrades in them
1374  if (UpgradePrefab.Prefabs.None(p => p.UpgradeCategories.Contains(category))) { continue; }
1375  if (entitiesOnSub.Any(item => category.CanBeApplied(item, null)))
1376  {
1377  yield return category;
1378  }
1379  }
1380  }
1381 
1382  private void UpdateSubmarinePreview(float deltaTime, GUICustomComponent parent)
1383  {
1384  if (Campaign == null) { return; }
1385 
1386  if (!parent.Children.Any() ||
1387  Submarine.MainSub != null && Submarine.MainSub != drawnSubmarine ||
1388  GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y)
1389  {
1390  GameMain.GameSession?.SubmarineInfo?.CheckSubsLeftBehind();
1391  drawnSubmarine = Submarine.MainSub;
1392  if (drawnSubmarine != null)
1393  {
1394  CreateSubmarinePreview(drawnSubmarine, parent);
1395  CreateHullBorderVerticies(drawnSubmarine, parent);
1396 
1397  applicableCategories.Clear();
1398  applicableCategories.AddRange(GetApplicableCategories(drawnSubmarine));
1399  }
1400 
1401  screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
1402  // this might be a bit spaghetti, we use the submarine preview's Update() function to refresh the upgrade list when the submarine changes
1403  // we also need this when we first load in so we know which category entries to disable since the CampaignUI is created before the submarine is loaded in.
1404  RefreshAll();
1405  }
1406  if (needsRefresh)
1407  {
1408  RefreshAll();
1409  }
1410 
1411  // accept an active confirmation popup if any
1412  if (PlayerInput.KeyHit(Keys.Enter) && GUIMessageBox.MessageBoxes.Any())
1413  {
1414  for (int i = GUIMessageBox.MessageBoxes.Count - 1; i >= 0; i--)
1415  {
1416  if (GUIMessageBox.MessageBoxes[i] is GUIMessageBox msgBox && msgBox == currectConfirmation)
1417  {
1418  // first button is the ok button
1419  GUIButton? firstButton = msgBox.Buttons.FirstOrDefault();
1420  if (firstButton is null) { continue; }
1421 
1422  firstButton.OnClicked.Invoke(firstButton, firstButton.UserData);
1423  }
1424  }
1425  }
1426 
1427  bool found = false;
1428  foreach (var (item, frame) in itemPreviews)
1429  {
1430  if (GUI.MouseOn == frame)
1431  {
1432  if (HoveredEntity != item) { CreateItemTooltip(item); }
1433  HoveredEntity = item;
1434  if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradeTab == UpgradeTab.Upgrade && currentStoreLayout != null)
1435  {
1436  if (customizeTabOpen)
1437  {
1438  if (selectedUpgradeCategoryLayout != null)
1439  {
1440  var linkedItems = HoveredEntity is Item hoveredItem ? UpgradeManager.GetLinkedItemsToSwap(hoveredItem) : new List<Item>();
1441  if (selectedUpgradeCategoryLayout.FindChild(c => c.UserData is Item item && (item == HoveredEntity || linkedItems.Contains(item)), recursive: true) is GUIButton itemElement)
1442  {
1443  if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); }
1444  (itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement);
1445  }
1446  else
1447  {
1448  ScrollToCategory(data => data.Category.CanBeApplied(item, null));
1449  }
1450  }
1451  }
1452  else
1453  {
1454  ScrollToCategory(data => data.Category.CanBeApplied(item, null));
1455  }
1456  }
1457  found = true;
1458  break;
1459  }
1460  }
1461 
1462  if (!found)
1463  {
1464  bool isMouseOnStructure = false;
1465  if (GUI.MouseOn == submarinePreviewComponent || GUI.MouseOn == subPreviewFrame)
1466  {
1467  // Every wall should have the same upgrades so we can just display the first one in the tooltip
1468  Structure? firstStructure = submarineWalls.FirstOrDefault();
1469  // use pnpoly algorithm to detect if our mouse is within any of the hull polygons
1470  if (subHullVertices.Any(hullVertex => ToolBox.PointIntersectsWithPolygon(PlayerInput.MousePosition, hullVertex)))
1471  {
1472  if (HoveredEntity != firstStructure && !(firstStructure is null)) { CreateItemTooltip(firstStructure); }
1473  HoveredEntity = firstStructure;
1474  isMouseOnStructure = true;
1475  GUI.MouseCursor = CursorState.Hand;
1476 
1477  if (PlayerInput.PrimaryMouseButtonClicked() && selectedUpgradeTab == UpgradeTab.Upgrade && currentStoreLayout != null)
1478  {
1479  ScrollToCategory(data => data.Category.IsWallUpgrade, GUIListBox.PlaySelectSound.Yes);
1480  }
1481  }
1482  }
1483 
1484  if (!isMouseOnStructure) { HoveredEntity = null; }
1485  }
1486 
1487  // flip the tooltip if it is outside of the screen
1488  ItemInfoFrame.RectTransform.ScreenSpaceOffset = (PlayerInput.MousePosition + new Vector2(20, 20)).ToPoint();
1489  if (ItemInfoFrame.Rect.Right > GameMain.GraphicsWidth)
1490  {
1491  ItemInfoFrame.RectTransform.ScreenSpaceOffset = (PlayerInput.MousePosition - new Vector2(20 + ItemInfoFrame.Rect.Width, -20)).ToPoint();
1492  }
1493  }
1494 
1495  private void CreateSubmarinePreview(Submarine submarine, GUIComponent parent)
1496  {
1497  if (mainStoreLayout == null) { return; }
1498 
1499  if (submarineInfoFrame != null && mainStoreLayout == submarineInfoFrame.Parent)
1500  {
1501  mainStoreLayout.RemoveChild(submarineInfoFrame);
1502  }
1503 
1504  parent.ClearChildren();
1505 
1506  /* SUBMARINE INFO BOX
1507  * |--------------------------------------------------|
1508  * | name |
1509  * |--------------------------------------------------|
1510  * | class + tier |
1511  * |--------------------------------------------------|
1512  * | description |
1513  * | |
1514  * | |
1515  * |--------------------------------------------------|
1516  */
1517  submarineInfoFrame = new GUILayoutGroup(rectT(0.25f, 0.2f, mainStoreLayout, Anchor.TopRight)) { IgnoreLayoutGroups = true };
1518  // submarine name
1519  new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.DisplayName, textAlignment: Alignment.Right, font: GUIStyle.LargeFont);
1520 
1521  LocalizedString classText = $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}";
1522  // submarine class + tier
1523  new GUITextBlock(rectT(1.0f, 0.15f, submarineInfoFrame), classText, textAlignment: Alignment.Right, font: GUIStyle.Font)
1524  {
1525  ToolTip = TextManager.Get("submarineclass.description") + "\n\n" + TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}.description")
1526  };
1527  new GUITextBlock(rectT(1.0f, 0.15f, submarineInfoFrame), TextManager.Get($"submarinetier.{submarine.Info.Tier}"), textAlignment: Alignment.Right, font: GUIStyle.Font)
1528  {
1529  ToolTip = TextManager.Get("submarinetier.description")
1530  };
1531 
1532  var description = new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.Description, textAlignment: Alignment.Right, wrap: true);
1533  submarineInfoFrame.RectTransform.ScreenSpaceOffset = new Point(0, (int)(16 * GUI.Scale));
1534 
1535  description.Padding = new Vector4(description.Padding.X, 24 * GUI.Scale, description.Padding.Z, description.Padding.W);
1536  List<Entity> pointsOfInterest = (from category in UpgradeCategory.Categories from item in submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs)
1537  where category.CanBeApplied(item, null) && item.IsPlayerTeamInteractable select item).Cast<Entity>().Distinct().ToList();
1538 
1539  List<ushort> ids = GameMain.GameSession.SubmarineInfo?.LeftBehindDockingPortIDs ?? new List<ushort>();
1540  pointsOfInterest.AddRange(submarine.GetItems(UpgradeManager.UpgradeAlsoConnectedSubs).Where(item => ids.Contains(item.ID)));
1541 
1542  submarine.CreateMiniMap(parent, pointsOfInterest, ignoreOutpost: true);
1543  subPreviewFrame = parent.GetChild<GUIFrame>();
1544  Rectangle dockedBorders = submarine.GetDockedBorders();
1545  GUIFrame hullContainer = parent.GetChild<GUIFrame>();
1546  if (hullContainer == null) { return; }
1547  itemPreviews.Clear();
1548 
1549  foreach (Entity entity in pointsOfInterest)
1550  {
1551  GUIComponent component = parent.FindChild(entity, true);
1552  if (component != null && entity is Item item)
1553  {
1554  GUIComponent itemFrame;
1555  if (item.Prefab.UpgradePreviewSprite is { } icon)
1556  {
1557  float spriteSize = 128f * item.Prefab.UpgradePreviewScale;
1558  Point size = new Point((int) (spriteSize * item.Scale / dockedBorders.Width * hullContainer.Rect.Width));
1559  itemFrame = new GUIImage(rectT(size, component, Anchor.Center), icon, scaleToFit: true)
1560  {
1561  SelectedColor = GUIStyle.Orange,
1562  Color = previewWhite,
1563  HoverCursor = CursorState.Hand,
1564  SpriteEffects = item.Rotation > 90.0f && item.Rotation < 270.0f ? SpriteEffects.FlipVertically : SpriteEffects.None
1565  };
1566  if (item.Prefab.SwappableItem != null)
1567  {
1568  new GUIImage(new RectTransform(new Vector2(0.8f), itemFrame.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(-0.2f) }, "WeaponSwitchIcon.DropShadow", scaleToFit: true)
1569  {
1570  SelectedColor = GUIStyle.Orange,
1571  Color = previewWhite,
1572  CanBeFocused = false
1573  };
1574  }
1575  }
1576  else
1577  {
1578  Point size = new Point((int) (item.Rect.Width * item.Scale / dockedBorders.Width * hullContainer.Rect.Width), (int) (item.Rect.Height * item.Scale / dockedBorders.Height * hullContainer.Rect.Height));
1579  itemFrame = new GUIFrame(rectT(size, component, Anchor.Center), style: "ScanLines")
1580  {
1581  SelectedColor = GUIStyle.Orange,
1582  OutlineColor = previewWhite,
1583  Color = previewWhite,
1584  OutlineThickness = 2,
1585  HoverCursor = CursorState.Hand
1586  };
1587  }
1588 
1589  if (!itemPreviews.ContainsKey(item))
1590  {
1591  itemPreviews.Add(item, itemFrame);
1592  }
1593  }
1594  }
1595  }
1596 
1606  private void CreateHullBorderVerticies(Submarine sub, GUIComponent parent)
1607  {
1608  submarineWalls = sub.GetWalls(UpgradeManager.UpgradeAlsoConnectedSubs);
1609  const float lineWidth = 10;
1610 
1611  if (sub.HullVertices == null) { return; }
1612 
1613  Rectangle dockedBorders = sub.GetDockedBorders();
1614  dockedBorders.Location += sub.WorldPosition.ToPoint();
1615 
1616  float scale = Math.Min(parent.Rect.Width / (float)dockedBorders.Width, parent.Rect.Height / (float)dockedBorders.Height) * 0.9f;
1617 
1618  float displayScale = ConvertUnits.ToDisplayUnits(scale);
1619  Vector2 offset = (sub.WorldPosition - new Vector2(dockedBorders.Center.X, dockedBorders.Y - dockedBorders.Height / 2)) * scale;
1620  Vector2 center = parent.Rect.Center.ToVector2();
1621 
1622  subHullVertices = new Vector2[sub.HullVertices.Count][];
1623 
1624  for (int i = 0; i < sub.HullVertices.Count; i++)
1625  {
1626  Vector2 start = sub.HullVertices[i] * displayScale + offset;
1627  start.Y = -start.Y;
1628  Vector2 end = sub.HullVertices[(i + 1) % sub.HullVertices.Count] * displayScale + offset;
1629  end.Y = -end.Y;
1630 
1631  Vector2 edge = end - start;
1632  float length = edge.Length();
1633  float angle = (float)Math.Atan2(edge.Y, edge.X);
1634  Matrix rotate = Matrix.CreateRotationZ(angle);
1635 
1636  subHullVertices[i] = new[]
1637  {
1638  center + start + Vector2.Transform(new Vector2(length, -lineWidth), rotate),
1639  center + end + Vector2.Transform(new Vector2(-length, -lineWidth), rotate),
1640  center + end + Vector2.Transform(new Vector2(-length, lineWidth), rotate),
1641  center + start + Vector2.Transform(new Vector2(length, lineWidth), rotate),
1642  };
1643  }
1644  }
1645 
1646  private void DrawSubmarine(SpriteBatch spriteBatch, GUICustomComponent component)
1647  {
1648  foreach (Vector2[] hullVertex in subHullVertices)
1649  {
1650  // calculate the center point so we can draw a line from X to Y instead of drawing a rotated rectangle that is filled
1651  Vector2 point1 = hullVertex[1] + (hullVertex[2] - hullVertex[1]) / 2;
1652  Vector2 point2 = hullVertex[0] + (hullVertex[3] - hullVertex[0]) / 2;
1653  GUI.DrawLine(spriteBatch, point1, point2, (highlightWalls ? GUIStyle.Orange * 0.6f : Color.DarkCyan * 0.3f), width: 10);
1654  if (GameMain.DebugDraw)
1655  {
1656  // the "collision box" is a bit bigger than the line we draw so this can be useful data (maybe)
1657  GUI.DrawRectangle(spriteBatch, hullVertex, Color.Red);
1658  }
1659  }
1660  }
1661 
1662  public static void UpdateUpgradeEntry(GUIComponent prefabFrame, UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign)
1663  {
1664  int currentLevel = campaign.UpgradeManager.GetUpgradeLevel(prefab, category);
1665 
1666  int maxLevel = prefab.GetMaxLevelForCurrentSub();
1667  LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", maxLevel.ToString()));
1668  if (prefabFrame.FindChild(UpgradeStoreUserData.ProgressBarLayout, true) is { } progressParent)
1669  {
1670  GUIProgressBar bar = progressParent.GetChild<GUIProgressBar>();
1671  if (bar != null)
1672  {
1673  bar.BarSize = currentLevel / (float)maxLevel;
1674  bar.Color = currentLevel >= maxLevel ? GUIStyle.Green : GUIStyle.Orange;
1675  }
1676 
1677  GUITextBlock block = progressParent.GetChild<GUITextBlock>();
1678  if (block != null) { block.Text = progressText; }
1679  }
1680 
1681  if (prefabFrame.FindChild(UpgradeStoreUserData.BuyButtonLayout, true) is not { } buttonParent) { return; }
1682 
1683  GUITextBlock priceLabel = (GUITextBlock)buttonParent.FindChild(UpgradeStoreUserData.PriceLabel, recursive: true);
1684  priceLabel.Visible = true;
1685  int price = prefab.Price.GetBuyPrice(prefab, campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation, characterList);
1686 
1687  if (!WaitForServerUpdate)
1688  {
1689  priceLabel.Text = TextManager.FormatCurrency(price);
1690  if (currentLevel >= maxLevel)
1691  {
1692  priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade");
1693  }
1694  }
1695 
1696  if (buttonParent.FindChild(UpgradeStoreUserData.IncreaseLabel, recursive: true) is GUITextBlock increaseLabel && !WaitForServerUpdate)
1697  {
1698  UpdateUpgradePercentageText(increaseLabel, prefab, currentLevel);
1699  }
1700 
1701  bool isMax = currentLevel >= maxLevel;
1702 
1703  if (buttonParent.FindChild(UpgradeStoreUserData.BuyButton, recursive: true) is GUIButton button)
1704  {
1705  bool canBuy = !WaitForServerUpdate && !isMax && campaign.GetBalance() >= price && prefab.HasResourcesToUpgrade(Character.Controlled, currentLevel + 1);
1706 
1707  button.Enabled = canBuy;
1708  }
1709 
1710  if (prefabFrame.FindChild(UpgradeStoreUserData.MaterialCostList, true) is GUIListBox itemList)
1711  {
1712  if (isMax)
1713  {
1714  itemList.Visible = false;
1715  }
1716  else
1717  {
1718  CreateMaterialCosts(itemList, prefab, currentLevel + 1);
1719  }
1720  }
1721 
1722  static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel)
1723  {
1724  list.Content.ClearChildren();
1725  var allItems = CargoManager.FindAllItemsOnPlayerAndSub(Character.Controlled);
1726 
1727  var resources = prefab.GetApplicableResources(targetLevel);
1728 
1729  foreach (ApplicableResourceCollection collection in resources)
1730  {
1731  list.Visible = true;
1732 
1733  int length = collection.MatchingItems.Length;
1734 
1735  if (length is 0) { continue; }
1736 
1737  ItemPrefab defaultItemPrefab = collection.MatchingItems.First();
1738 
1739  GUILayoutGroup wrapperLayout = new GUILayoutGroup(rectT(0.25f, 1f, list.Content));
1740 
1741  GUIFrame itemFrame = new GUIFrame(rectT(1f, 1f, wrapperLayout), style: null)
1742  {
1743  ToolTip = defaultItemPrefab.Name
1744  };
1745 
1746  bool hasItems = collection.Cost.Amount <= allItems.Count(collection.Cost.MatchesItem);
1747 
1748  Sprite icon = defaultItemPrefab.InventoryIcon ?? prefab.Sprite;
1749  Color iconColor = defaultItemPrefab.InventoryIcon is null ? defaultItemPrefab.SpriteColor : defaultItemPrefab.InventoryIconColor;
1750 
1751  GUIImage itemIcon = new GUIImage(new RectTransform(Vector2.One, itemFrame.RectTransform, scaleBasis: ScaleBasis.Smallest, anchor: Anchor.Center), sprite: icon, scaleToFit: true)
1752  {
1753  Color = hasItems ? iconColor : iconColor * 0.9f,
1754  CanBeFocused = false
1755  };
1756 
1757  // item count text
1758  new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.5f), itemIcon.RectTransform, anchor: Anchor.BottomRight), $"{collection.Count}", font: GUIStyle.Font, textAlignment: Alignment.BottomRight)
1759  {
1760  Shadow = true,
1761  CanBeFocused = false,
1762  Padding = Vector4.Zero,
1763  TextColor = hasItems ? Color.White : GUIStyle.Red,
1764  };
1765 
1766  if (length is 1) { continue; }
1767 
1768  // we have more than 1 item, show a "slideshow" of the items
1769 
1770  float index = 0f;
1771  GUICustomComponent customComponent = new GUICustomComponent(rectT(1f, 1f, itemFrame), null, (deltaTime, component) =>
1772  {
1773  index += deltaTime / 3f;
1774  if (index > length) { index = 0; }
1775 
1776  ItemPrefab currentPrefab = collection.MatchingItems[(int)MathF.Floor(index)];
1777  Sprite icon = currentPrefab.InventoryIcon ?? prefab.Sprite;
1778  Color iconColor = currentPrefab.InventoryIcon is null ? currentPrefab.SpriteColor : currentPrefab.InventoryIconColor;
1779  itemIcon.Sprite = icon;
1780  itemIcon.Color = hasItems ? iconColor : iconColor * 0.9f;
1781  itemFrame.ToolTip = currentPrefab.Name;
1782  })
1783  {
1784  CanBeFocused = false
1785  };
1786  }
1787  }
1788  }
1789 
1790  private static void UpdateCategoryIndicators(
1791  GUIComponent indicators,
1792  GUIComponent parent,
1793  List<UpgradePrefab> prefabs,
1794  UpgradeCategory category,
1795  CampaignMode campaign,
1796  Submarine? drawnSubmarine,
1797  IEnumerable<UpgradeCategory> applicableCategories)
1798  {
1799  // Disables the parent and only re-enables if the submarine contains valid items
1800  if (!category.IsWallUpgrade && drawnSubmarine?.Info != null)
1801  {
1802  if (UpgradePrefab.Prefabs.None(p => p.UpgradeCategories.Contains(category) && p.GetMaxLevel(drawnSubmarine.Info) > 0))
1803  {
1804  parent.ToolTip = TextManager.Get("upgradecategorynotapplicable");
1805  parent.Enabled = false;
1806  parent.SelectedColor = GUIStyle.Red * 0.5f;
1807  }
1808  else if (applicableCategories.Contains(category))
1809  {
1810  parent.Enabled = true;
1811  parent.SelectedColor = parent.Style.SelectedColor;
1812  }
1813  else
1814  {
1815  parent.Enabled = false;
1816  parent.SelectedColor = GUIStyle.Red * 0.5f;
1817  }
1818  }
1819 
1820  foreach (GUIComponent component in indicators.Children)
1821  {
1822  if (component is not GUIImage image) { continue; }
1823 
1824  foreach (UpgradePrefab prefab in prefabs)
1825  {
1826  if (component.UserData != prefab) { continue; }
1827 
1828  int maxLevel = prefab.GetMaxLevelForCurrentSub();
1829  if (maxLevel == 0)
1830  {
1831  component.Visible = false;
1832  continue;
1833  }
1834 
1835  Dictionary<Identifier, GUIComponentStyle> styles = GUIStyle.GetComponentStyle("upgradeindicator").ChildStyles;
1836  if (!styles.ContainsKey("upgradeindicatoron") || !styles.ContainsKey("upgradeindicatordim") || !styles.ContainsKey("upgradeindicatoroff")) { continue; }
1837 
1838  GUIComponentStyle onStyle = styles["upgradeindicatoron".ToIdentifier()];
1839  GUIComponentStyle dimStyle = styles["upgradeindicatordim".ToIdentifier()];
1840  GUIComponentStyle offStyle = styles["upgradeindicatoroff".ToIdentifier()];
1841 
1842  if (maxLevel == 0)
1843  {
1844  SetOff();
1845  continue;
1846  }
1847 
1848  if (campaign.UpgradeManager.GetUpgradeLevel(prefab, category) >= maxLevel)
1849  {
1850  // we check this to avoid flickering from re-applying the same style
1851  if (image.Style == onStyle) { continue; }
1852  image.ApplyStyle(onStyle);
1853  }
1854  else if (campaign.UpgradeManager.GetUpgradeLevel(prefab, category) > 0)
1855  {
1856  if (image.Style == dimStyle) { continue; }
1857  image.ApplyStyle(dimStyle);
1858  }
1859  else
1860  {
1861  SetOff();
1862  }
1863 
1864  void SetOff()
1865  {
1866  if (image.Style == offStyle) { return; }
1867  image.ApplyStyle(offStyle);
1868  }
1869  }
1870  }
1871  }
1872 
1873  private void ScrollToCategory(Predicate<CategoryData> predicate, GUIListBox.PlaySelectSound playSelectSound = GUIListBox.PlaySelectSound.No)
1874  {
1875  if (currentStoreLayout == null) { return; }
1876 
1877  CategoryData? mostAppropriateCategory = null;
1878  GUIComponent? mostAppropriateChild = null;
1879  foreach (GUIComponent child in currentStoreLayout.Content.Children)
1880  {
1881  if (child.UserData is CategoryData data && predicate(data))
1882  {
1883  //choose the category with least items in it as the "most appropriate"
1884  //e.g. when selecting junction boxes, we want to select the "junction boxes" category instead of "electrical repairs" which contains many electrical devices
1885  if (mostAppropriateCategory == null ||
1886  data.Category.ItemTags.Count() < mostAppropriateCategory.Value.Category.ItemTags.Count())
1887  {
1888  mostAppropriateCategory = data;
1889  mostAppropriateChild = child;
1890  }
1891  }
1892  }
1893  if (mostAppropriateChild != null)
1894  {
1895  currentStoreLayout.ScrollToElement(mostAppropriateChild, playSelectSound);
1896  }
1897  }
1898 
1904  private GUIComponent[] GetFrames(UpgradeCategory category)
1905  {
1906  List<GUIComponent> frames = new List<GUIComponent>();
1907  foreach (var (item, guiFrame) in itemPreviews)
1908  {
1909  if (category.CanBeApplied(item, null))
1910  {
1911  frames.Add(guiFrame);
1912  }
1913  }
1914 
1915  return frames.ToArray();
1916  }
1917 
1918  private bool HasPermission => true;
1919 
1920  // just a shortcut to create new RectTransforms since all the new RectTransform and new Vector2 confuses my IDE (and me)
1921  private static RectTransform rectT(float x, float y, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft, ScaleBasis scaleBasis = ScaleBasis.Normal)
1922  {
1923  return new RectTransform(new Vector2(x, y), parentComponent.RectTransform, anchor, scaleBasis: scaleBasis);
1924  }
1925 
1926  private static RectTransform rectT(Point point, GUIComponent parentComponent, Anchor anchor = Anchor.TopLeft)
1927  {
1928  return new RectTransform(point, parentComponent.RectTransform, anchor);
1929  }
1930  }
1931 }
GUIComponent GetChild(int index)
Definition: GUIComponent.cs:54
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
virtual void ClearChildren()
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
RectTransform RectTransform
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
GUIComponent that can be used to render custom content on the UI
OnSelectedHandler OnSelected
Definition: GUIListBox.cs:17
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
void ScrollToElement(GUIComponent component, PlaySelectSound playSelectSound=PlaySelectSound.No)
Scrolls the list to the specific element.
Definition: GUIListBox.cs:559
int GetAdjustedMechanicalCost(int cost)
Definition: Location.cs:1279
Point ScreenSpaceOffset
Screen space offset. From top left corner. In pixels.
int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo? info=null)
Gets the progress that is shown on the store interface. Includes values stored in the metadata and Pe...
void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool isNetworkMessage=false, Client? client=null)
Purchases an item swap and handles logic for deducting the credit.
void CancelItemSwap(Item itemToRemove, bool force=false)
Cancels the currently pending item swap, or uninstalls the item if there's no swap pending
void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force=false, Client? client=null)
Purchases an upgrade and handles logic for deducting the credit.
GUISoundType
Definition: GUI.cs:21
CursorState
Definition: GUI.cs:40
CharacterType
Definition: Enums.cs:685
readonly? List< UpgradePrefab > Prefabs
Definition: UpgradeStore.cs:25
CategoryData(UpgradeCategory category, List< UpgradePrefab > prefabs)
Definition: UpgradeStore.cs:28
CategoryData(UpgradeCategory category, UpgradePrefab prefab)
Definition: UpgradeStore.cs:35
readonly? UpgradePrefab SinglePrefab
Definition: UpgradeStore.cs:26
readonly UpgradeCategory Category
Definition: UpgradeStore.cs:24