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
Triggers when some element is clicked on the listbox. Note that SelectedData is not set yet when this...
Definition: GUIListBox.cs:21
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:42
void ScrollToElement(GUIComponent component, PlaySelectSound playSelectSound=PlaySelectSound.No)
Scrolls the list to the specific element.
Definition: GUIListBox.cs:568
int GetAdjustedMechanicalCost(int cost)
Definition: Location.cs:1295
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:711
@ Character
Characters only
@ Structure
Structures and hulls, but also items (for backwards support)!
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