Client LuaCsForBarotrauma
TabMenu.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Collections.Immutable;
4 using Microsoft.Xna.Framework;
5 using Microsoft.Xna.Framework.Graphics;
6 using System.Linq;
8 using System.Globalization;
10 
11 namespace Barotrauma
12 {
13  class TabMenu
14  {
15  public static bool PendingChanges = false;
16 
17  private static bool initialized = false;
18 
19  private static UISprite spectateIcon, disconnectedIcon;
20  private static Sprite ownerIcon, moderatorIcon;
21 
22  public enum InfoFrameTab { Crew, Mission, Reputation, Submarine, Talents };
23  public static InfoFrameTab SelectedTab { get; private set; }
24  private GUIFrame infoFrame, contentFrame;
25 
26  private readonly List<GUIButton> tabButtons = new List<GUIButton>();
27  private GUIFrame infoFrameHolder;
28  private List<LinkedGUI> linkedGUIList;
29  private GUIListBox logList;
30  private GUIListBox[] crewListArray;
31  private float sizeMultiplier = 1f;
32 
33  private IEnumerable<Character> crew;
34  private List<CharacterTeamType> teamIDs;
35  private const string inLobbyString = "\u2022 \u2022 \u2022";
36 
37  public static GUIFrame PendingChangesFrame = null;
38 
39  public static Color OwnCharacterBGColor = Color.Gold * 0.7f;
40  private bool isTransferMenuOpen;
41  private bool isSending;
42  private GUIComponent transferMenu;
43  private GUIButton transferMenuButton;
44  private float transferMenuOpenState;
45  private bool transferMenuStateCompleted;
46  private readonly HashSet<Identifier> registeredEvents = new HashSet<Identifier>();
47  private readonly TalentMenu talentMenu = new TalentMenu();
48 
49  private class LinkedGUI
50  {
51  private const ushort lowPingThreshold = 100;
52  private const ushort mediumPingThreshold = 200;
53 
54  public readonly Client Client;
55 
56  private ushort currentPing;
57  private readonly Character character;
58  private readonly bool wasCharacterAlive;
59  private readonly GUITextBlock textBlock;
60  private readonly GUIFrame frame;
61 
62  private readonly GUIImage permissionIcon;
63 
64  public LinkedGUI(Client client, GUIFrame frame, GUITextBlock textBlock, GUIImage permissionIcon)
65  {
66  this.Client = client;
67  this.textBlock = textBlock;
68  this.frame = frame;
69  this.permissionIcon = permissionIcon;
70  character = client?.Character;
71  wasCharacterAlive = client?.Character != null && !client.Character.IsDead;
72  }
73 
74  public LinkedGUI(Character character, GUIFrame frame, GUITextBlock textBlock)
75  {
76  this.character = character;
77  this.textBlock = textBlock;
78  this.frame = frame;
79  wasCharacterAlive = character != null && !character.IsDead;
80  }
81 
82  public bool HasMultiplayerCharacterChanged()
83  {
84  if (Client == null) { return false; }
85 
86  if (GameSettings.CurrentConfig.VerboseLogging)
87  {
88  if (Client.Character != character)
89  {
90  DebugConsole.Log($"Refreshing tab menu crew list (client \"{Client.Name}\"'s character changed from \"{character?.Name ?? "null"}\" to \"{Client.Character?.Name ?? "null"}\")");
91  }
92  }
93  return Client.Character != character;
94  }
95 
96  public bool HasCharacterDied()
97  {
98  if (character == null) { return false; }
99  bool isAlive = !(character?.IsDead ?? true);
100  if (GameSettings.CurrentConfig.VerboseLogging)
101  {
102  if (wasCharacterAlive && !isAlive)
103  {
104  DebugConsole.Log(Client == null ?
105  $"Refreshing tab menu crew list (character \"{character?.Name ?? "null"}\" died)" :
106  $"Refreshing tab menu crew list (client \"{Client.Name}\"'s character \"{character?.Name ?? "null"}\" died)");
107  }
108  else if (!wasCharacterAlive && isAlive)
109  {
110  DebugConsole.Log(Client == null ?
111 
112  $"Refreshing tab menu crew list (character \"{character?.Name ?? "null"}\" came back to life)" :
113  $"Refreshing tab menu crew list (client \"{Client.Name}\"'s character \"{character?.Name ?? "null"}\" came back to life)");
114  }
115  }
116  return isAlive != wasCharacterAlive;
117  }
118 
119  public void TryPingRefresh()
120  {
121  if (Client == null) { return; }
122  if (currentPing == Client.Ping) { return; }
123  currentPing = Client.Ping;
124  textBlock.Text = currentPing.ToString();
125  textBlock.TextColor = GetPingColor();
126  }
127 
128  public void TryPermissionIconRefresh(Sprite icon)
129  {
130  if (Client == null || permissionIcon == null) { return; }
131  permissionIcon.Sprite = icon;
132  }
133 
134  private Color GetPingColor()
135  {
136  if (currentPing < lowPingThreshold)
137  {
138  return GUIStyle.Green;
139  }
140  else if (currentPing < mediumPingThreshold)
141  {
142  return GUIStyle.Yellow;
143  }
144  else
145  {
146  return GUIStyle.Red;
147  }
148  }
149 
150  public void Remove(GUIFrame parent)
151  {
152  parent.RemoveChild(frame);
153  }
154  }
155 
156  public void Initialize()
157  {
158  spectateIcon = GUIStyle.GetComponentStyle("SpectateIcon").Sprites[GUIComponent.ComponentState.None][0];
159  disconnectedIcon = GUIStyle.GetComponentStyle("DisconnectedIcon").Sprites[GUIComponent.ComponentState.None][0];
160  ownerIcon = GUIStyle.GetComponentStyle("OwnerIcon").GetDefaultSprite();
161  moderatorIcon = GUIStyle.GetComponentStyle("ModeratorIcon").GetDefaultSprite();
162  initialized = true;
163  }
164 
165  public TabMenu()
166  {
167  if (!initialized) { Initialize(); }
168  if (Level.Loaded == null)
169  {
170  //make sure we're not trying to view e.g. mission or reputation info if the tab menu is opened in the test mode
171  SelectedTab = InfoFrameTab.Crew;
172  }
173  CreateInfoFrame(SelectedTab);
175  }
176 
177  public void Update(float deltaTime)
178  {
179  float menuOpenSpeed = deltaTime * 10f;
180  if (isTransferMenuOpen)
181  {
182  if (transferMenuStateCompleted)
183  {
184  transferMenuOpenState = transferMenuOpenState < 0.25f ? Math.Min(0.25f, transferMenuOpenState + (menuOpenSpeed / 2f)) : 0.25f;
185  }
186  else
187  {
188  if (transferMenuOpenState > 0.15f)
189  {
190  transferMenuStateCompleted = false;
191  transferMenuOpenState = Math.Max(0.15f, transferMenuOpenState - menuOpenSpeed);
192  }
193  else
194  {
195  transferMenuStateCompleted = true;
196  }
197  }
198  }
199  else
200  {
201  transferMenuStateCompleted = false;
202  if (transferMenuOpenState < 1f)
203  {
204  transferMenuOpenState = Math.Min(1f, transferMenuOpenState + menuOpenSpeed);
205  }
206  }
207 
208  if (transferMenu != null && transferMenuButton != null)
209  {
210  int pos = (int)(transferMenuOpenState * -transferMenu.Rect.Height);
211  transferMenu.RectTransform.AbsoluteOffset = new Point(0, pos);
212  transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height);
213  }
214  GameSession.UpdateTalentNotificationIndicator(talentPointNotification);
215 
216  talentMenu?.Update();
217 
218  if (SelectedTab != InfoFrameTab.Crew) { return; }
219  if (linkedGUIList == null) { return; }
220 
222  {
223  for (int i = 0; i < linkedGUIList.Count; i++)
224  {
225  linkedGUIList[i].TryPingRefresh();
226  linkedGUIList[i].TryPermissionIconRefresh(GetPermissionIcon(linkedGUIList[i].Client));
227  if (linkedGUIList[i].HasMultiplayerCharacterChanged() || linkedGUIList[i].HasCharacterDied())
228  {
229  RemoveCurrentElements();
230  CreateMultiPlayerList(true);
231  return;
232  }
233  }
234  }
235  else
236  {
237  for (int i = 0; i < linkedGUIList.Count; i++)
238  {
239  if (linkedGUIList[i].HasCharacterDied())
240  {
241  RemoveCurrentElements();
242  CreateSinglePlayerList(true);
243  return;
244  }
245  }
246  }
247  }
248 
249  public void AddToGUIUpdateList()
250  {
251  infoFrame?.AddToGUIUpdateList();
253  }
254 
255  public static void OnRoundEnded()
256  {
257  storedMessages.Clear();
258  PendingChanges = false;
259  }
260 
261  private void CreateInfoFrame(InfoFrameTab selectedTab)
262  {
263  tabButtons.Clear();
264 
265  infoFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null);
266  new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, infoFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
267 
268  //this used to be a switch expression but i changed it because it killed enc :(
269  //now it's not even a switch statement anymore :(
270  Vector2 contentFrameSize = new Vector2(0.45f, 0.667f);
271  contentFrame = new GUIFrame(new RectTransform(contentFrameSize, infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.12f) });
272 
273  var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.958f, 0.943f), contentFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(25f)) }, isHorizontal: true)
274  {
275  RelativeSpacing = 0.01f
276  };
277 
278  var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(0.07f, 1f), parent: horizontalLayoutGroup.RectTransform), isHorizontal: false)
279  {
280  AbsoluteSpacing = GUI.IntScale(5f)
281  };
282  var innerLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.92f, 1f), horizontalLayoutGroup.RectTransform))
283  {
284  RelativeSpacing = 0.01f,
285  Stretch = true
286  };
287 
288  float absoluteSpacing = innerLayoutGroup.RelativeSpacing * innerLayoutGroup.Rect.Height;
289  int multiplier = GameMain.GameSession?.GameMode is CampaignMode ? 2 : 1;
290  int infoFrameHolderHeight = Math.Min((int)(0.97f * innerLayoutGroup.Rect.Height), (int)(innerLayoutGroup.Rect.Height - multiplier * (GUI.IntScale(15f) + absoluteSpacing)));
291  infoFrameHolder = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: null);
292 
293  GUIButton createTabButton(InfoFrameTab tab, string textTag)
294  {
295  var newButton = new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.BothWidth), style: $"InfoFrameTabButton.{tab}")
296  {
297  UserData = tab,
298  ToolTip = TextManager.Get(textTag),
299  OnClicked = (btn, userData) => { SelectInfoFrameTab((InfoFrameTab)userData); return true; }
300  };
301  tabButtons.Add(newButton);
302  return newButton;
303  }
304 
305  var crewButton = createTabButton(InfoFrameTab.Crew, "crew");
306 
307  if (GameMain.GameSession?.GameMode is not TestGameMode)
308  {
309  var missionBtn = createTabButton(InfoFrameTab.Mission, "mission");
310  eventLogNotification = GameSession.CreateNotificationIcon(missionBtn);
311  eventLogNotification.Visible = GameMain.GameSession?.EventManager?.EventLog?.UnreadEntries ?? false;
312  if (eventLogNotification.Visible)
313  {
314  eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f);
315  }
316  }
317 
318  if (GameMain.GameSession?.GameMode is CampaignMode campaignMode)
319  {
320  var reputationButton = createTabButton(InfoFrameTab.Reputation, "reputation");
321 
322  var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame");
323  GUILayoutGroup salaryFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.66f, 1f), balanceFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
324 
325  GUIScrollBar salaryScrollBar = null;
326  GUITextBlock salaryPercentage = null;
327  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign)
328  {
329  float value = campaignMode.Bank.RewardDistribution;
330  GUITextBlock salaryText = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), salaryFrame.RectTransform), TextManager.Get("defaultsalary"), textAlignment: Alignment.Center)
331  {
332  AutoScaleHorizontal = true
333  };
334  salaryScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1f), salaryFrame.RectTransform), barSize: 0.1f, style: "GUISlider")
335  {
336  Range = new Vector2(0, 1),
337  BarScrollValue = value / 100f,
338  Step = 0.01f,
339  BarSize = 0.1f,
340  };
341 
342  salaryPercentage = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1f), salaryFrame.RectTransform), "0", textAlignment: Alignment.Center)
343  {
344  Text = ValueToPercentage(RoundRewardDistribution(salaryScrollBar.BarScroll, salaryScrollBar.Step))
345  };
346 
347  salaryScrollBar.OnMoved = (scrollBar, value) =>
348  {
349  salaryPercentage.Text = ValueToPercentage(RoundRewardDistribution(value, scrollBar.Step));
350  return true;
351  };
352  salaryScrollBar.OnReleased = (bar, scroll) =>
353  {
354  int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step);
355  SetRewardDistribution(Option.None, newRewardDistribution);
356  return true;
357  };
358 
359  var resetButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), salaryFrame.RectTransform), TextManager.Get("ResetSalaries"), style: "GUIButtonSmall")
360  {
361  TextBlock = { AutoScaleHorizontal = true },
362  ToolTip = TextManager.Get("resetsalaries.tooltip"),
363  OnClicked = (button, userData) =>
364  {
365  GUI.AskForConfirmation(TextManager.Get("ResetSalaries"), TextManager.Get("ResetSalaries.Warning"), onConfirm: ResetRewardDistributions);
366  return true;
367  }
368  };
369 
370  void UpdateSliderEnabled()
371  => salaryScrollBar.Enabled = resetButton.Enabled = CampaignMode.AllowedToManageWallets();
372  UpdateSliderEnabled();
373 
374  Identifier defaultSalaryEventIdentifier = "DefaultSalarySlider".ToIdentifier();
375  GameMain.Client?.OnPermissionChanged?.RegisterOverwriteExisting(defaultSalaryEventIdentifier, _ => UpdateSliderEnabled());
376  }
377  GUITextBlock balanceText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1f), balanceFrame.RectTransform, Anchor.TopRight), string.Empty, textAlignment: Alignment.Right);
378  if (GameMain.IsMultiplayer)
379  {
380  balanceText.ToolTip = TextManager.Get("bankdescription");
381  }
382  GUIFrame bottomDisclaimerFrame = new GUIFrame(new RectTransform(new Vector2(contentFrameSize.X, 0.1f), infoFrame.RectTransform)
383  {
384  AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8))
385  }, style: null);
386 
387  PendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null);
388 
389  if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false)
390  {
391  NetLobbyScreen.CreateChangesPendingFrame(PendingChangesFrame);
392  }
393 
394  SetBalanceText(balanceText, campaignMode.Bank.Balance);
395  Identifier eventIdentifier = nameof(CreateInfoFrame).ToIdentifier();
396  campaignMode.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e =>
397  {
398  if (!e.Owner.IsNone()) { return; }
399  SetBalanceText(balanceText, e.Wallet.Balance);
400 
401  if (salaryPercentage is not null && salaryScrollBar is not null)
402  {
403  float rewardDistribution = e.Wallet.RewardDistribution;
404  salaryScrollBar.BarScrollValue = rewardDistribution / 100f;
405  salaryPercentage.Text = ValueToPercentage(rewardDistribution);
406  }
407  });
408  registeredEvents.Add(eventIdentifier);
409 
410  static void SetBalanceText(GUITextBlock text, int balance)
411  {
412  text.Text = TextManager.GetWithVariable("bankbalanceformat", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", balance));
413  }
414 
415  LocalizedString ValueToPercentage(float value)
416  => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(value)}");
417  }
418 
419  var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine");
420 
421  var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character");
422  talentsButton.OnAddedToGUIUpdateList += (component) =>
423  {
424  talentsButton.Enabled = Character.Controlled?.Info != null || GameMain.Client?.CharacterInfo != null;
425  if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents)
426  {
428  }
429  };
430 
431  talentPointNotification = GameSession.CreateNotificationIcon(talentsButton);
432  }
433 
434  public void SelectInfoFrameTab(InfoFrameTab selectedTab)
435  {
436  SelectedTab = selectedTab;
437 
438  CreateInfoFrame(selectedTab);
439  tabButtons.ForEach(tb => tb.Selected = (InfoFrameTab)tb.UserData == selectedTab);
440 
441  switch (selectedTab)
442  {
443  case InfoFrameTab.Crew:
444  CreateCrewListFrame(infoFrameHolder);
445  break;
446  case InfoFrameTab.Mission:
447  CreateMissionInfo(infoFrameHolder);
448  break;
449  case InfoFrameTab.Reputation:
450  if (GameMain.GameSession?.RoundSummary != null && GameMain.GameSession?.GameMode is CampaignMode campaignMode)
451  {
452  infoFrameHolder.ClearChildren();
453  GUIFrame reputationFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrameHolder.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
454  GameMain.GameSession.RoundSummary.CreateReputationInfoPanel(reputationFrame, campaignMode);
455  }
456  break;
457  case InfoFrameTab.Submarine:
458  CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub);
459  break;
460  case InfoFrameTab.Talents:
461  talentMenu.CreateGUI(infoFrameHolder, Character.Controlled ?? GameMain.Client?.Character);
462  break;
463  }
464  }
465 
466  private const float jobColumnWidthPercentage = 0.138f,
467  characterColumnWidthPercentage = 0.45f,
468  pingColumnWidthPercentage = 0.206f,
469  walletColumnWidthPercentage = 0.206f;
470 
471  private int jobColumnWidth, characterColumnWidth, pingColumnWidth, walletColumnWidth;
472 
473  private void CreateCrewListFrame(GUIFrame crewFrame)
474  {
475  crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? new List<Character>() { TestScreen.dummyCharacter};
476  teamIDs = crew.Select(c => c.TeamID).Distinct().ToList();
477 
478  // Show own team first when there's more than one team
479  if (teamIDs.Count > 1 && GameMain.Client?.Character != null)
480  {
481  CharacterTeamType ownTeam = GameMain.Client.Character.TeamID;
482  teamIDs = teamIDs.OrderBy(i => i != ownTeam).ThenBy(i => i).ToList();
483  }
484 
485  if (!teamIDs.Any()) { teamIDs.Add(CharacterTeamType.None); }
486 
487  var content = new GUILayoutGroup(new RectTransform(Vector2.One, crewFrame.RectTransform));
488 
489  crewListArray = new GUIListBox[teamIDs.Count];
490  GUILayoutGroup[] headerFrames = new GUILayoutGroup[teamIDs.Count];
491 
492  float nameHeight = 0.075f;
493 
494  Vector2 crewListSize = new Vector2(1f, 1f / teamIDs.Count - (teamIDs.Count > 1 ? nameHeight * 1.1f : 0f));
495  for (int i = 0; i < teamIDs.Count; i++)
496  {
497  if (teamIDs.Count > 1)
498  {
499  new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUIStyle.Green : GUIStyle.Orange) { ForceUpperCase = ForceUpperCase.Yes };
500  }
501 
502  headerFrames[i] = new GUILayoutGroup(new RectTransform(Vector2.Zero, content.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(2, -1) }, isHorizontal: true)
503  {
504  AbsoluteSpacing = 2,
505  UserData = i
506  };
507 
508  GUIListBox crewList = new GUIListBox(new RectTransform(crewListSize, content.RectTransform))
509  {
510  Padding = new Vector4(2, 5, 0, 0),
511  AutoHideScrollBar = false,
512  PlaySoundOnSelect = true
513  };
514  crewList.UpdateDimensions();
515 
516  if (teamIDs.Count > 1)
517  {
518  crewList.OnSelected = (component, obj) =>
519  {
520  for (int i = 0; i < crewListArray.Length; i++)
521  {
522  if (crewListArray[i] == crewList) continue;
523  crewListArray[i].Deselect();
524  }
525  SelectElement(component.UserData, crewList);
526  return true;
527  };
528  }
529  else
530  {
531  crewList.OnSelected = (component, obj) =>
532  {
533  SelectElement(component.UserData, crewList);
534  return true;
535  };
536  }
537 
538  crewListArray[i] = crewList;
539  }
540 
541  for (int i = 0; i < teamIDs.Count; i++)
542  {
543  headerFrames[i].RectTransform.RelativeSize = new Vector2(1f - crewListArray[i].ScrollBar.Rect.Width / (float)crewListArray[i].Rect.Width, GUIStyle.HotkeyFont.Size / (float)crewFrame.RectTransform.Rect.Height * 1.5f);
544 
545  if (!GameMain.IsMultiplayer)
546  {
547  CreateSinglePlayerListContentHolder(headerFrames[i]);
548  }
549  else
550  {
551  CreateMultiPlayerListContentHolder(headerFrames[i]);
552  }
553  }
554 
555  crewFrame.RectTransform.AbsoluteOffset = new Point(0, (int)(headerFrames[0].Rect.Height * headerFrames.Length) - (teamIDs.Count > 1 ? GUI.IntScale(10f) : 0));
556 
557  float totalRelativeHeight = 0.0f;
558  if (teamIDs.Count > 1) { totalRelativeHeight += teamIDs.Count * nameHeight; }
559  headerFrames.ForEach(f => totalRelativeHeight += f.RectTransform.RelativeSize.Y);
560  crewListArray.ForEach(f => totalRelativeHeight += f.RectTransform.RelativeSize.Y);
561  if (totalRelativeHeight > 1.0f)
562  {
563  float heightOverflow = totalRelativeHeight - 1.0f;
564  float heightToReduce = heightOverflow / crewListArray.Length;
565  crewListArray.ForEach(l =>
566  {
567  l.RectTransform.Resize(l.RectTransform.RelativeSize - new Vector2(0.0f, heightToReduce));
568  l.UpdateDimensions();
569  });
570  }
571 
572  if (GameMain.IsMultiplayer)
573  {
574  CreateMultiPlayerList(false);
575  CreateMultiPlayerLogContent(crewFrame);
576  }
577  else
578  {
579  CreateSinglePlayerList(false);
580  }
581  }
582 
583  private void CreateSinglePlayerListContentHolder(GUILayoutGroup headerFrame)
584  {
585  GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
586  GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
587 
588  sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width;
589 
590  jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f);
591  characterButton.RectTransform.RelativeSize = new Vector2((1f - jobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f);
592 
593  jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUIStyle.HotkeyFont;
594  jobButton.CanBeFocused = characterButton.CanBeFocused = false;
595  jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = ForceUpperCase.Yes;
596 
597  jobColumnWidth = jobButton.Rect.Width;
598  characterColumnWidth = characterButton.Rect.Width;
599  }
600 
601  private void CreateSinglePlayerList(bool refresh)
602  {
603  if (refresh)
604  {
605  crew = GameMain.GameSession.CrewManager.GetCharacters();
606  }
607 
608  linkedGUIList = new List<LinkedGUI>();
609 
610  for (int i = 0; i < teamIDs.Count; i++)
611  {
612  foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i]))
613  {
614  CreateSinglePlayerCharacterElement(character, i);
615  }
616  }
617  }
618 
619  private void CreateSinglePlayerCharacterElement(Character character, int i)
620  {
621  GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement")
622  {
623  UserData = character,
624  Color = (Character.Controlled == character) ? OwnCharacterBGColor : Color.Transparent
625  };
626 
627  var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
628  {
629  AbsoluteSpacing = 2
630  };
631 
632  new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect))
633  {
634  CanBeFocused = false,
635  HoverColor = Color.White,
636  SelectedColor = Color.White
637  };
638 
639  GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
640  ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor);
641 
642  linkedGUIList.Add(new LinkedGUI(character, frame, textBlock: null));
643  }
644 
645  private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame)
646  {
647  bool isCampaign = GameMain.GameSession?.Campaign is MultiPlayerCampaign;
648  GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
649  GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
650  GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale");
651  if (isCampaign)
652  {
653  GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform)
654  {
655  RelativeSize = new Vector2(walletColumnWidthPercentage * sizeMultiplier, 1f)
656  }, TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale")
657  {
658  TextBlock = { Font = GUIStyle.HotkeyFont },
659  CanBeFocused = false,
661  };
662  walletColumnWidth = walletButton.Rect.Width;
663  }
664 
665  sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width;
666 
667  jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f);
668  characterButton.RectTransform.RelativeSize = new Vector2((characterColumnWidthPercentage + (isCampaign ? 0 : walletColumnWidthPercentage)) * sizeMultiplier, 1f);
669  pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f);
670 
671  jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUIStyle.HotkeyFont;
672  jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false;
673  jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = ForceUpperCase.Yes;
674 
675  jobColumnWidth = jobButton.Rect.Width;
676  characterColumnWidth = characterButton.Rect.Width;
677  pingColumnWidth = pingButton.Rect.Width;
678  }
679 
680  private void CreateMultiPlayerList(bool refresh)
681  {
682  if (refresh)
683  {
684  crew = GameMain.GameSession.CrewManager.GetCharacters();
685  }
686 
687  linkedGUIList = new List<LinkedGUI>();
688 
689  var connectedClients = GameMain.Client.ConnectedClients;
690 
691  for (int i = 0; i < teamIDs.Count; i++)
692  {
693  foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i]))
694  {
695  if (!(character is AICharacter) && connectedClients.Any(c => c.Character == null && c.Name == character.Name)) { continue; }
696  CreateMultiPlayerCharacterElement(character, GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.Character == character), i);
697  }
698  }
699 
700  for (int j = 0; j < connectedClients.Count; j++)
701  {
702  Client client = connectedClients[j];
703  if (!client.InGame || client.Character == null || client.Character.IsDead)
704  {
705  CreateMultiPlayerClientElement(client);
706  }
707  }
708  }
709 
710  private void CreateMultiPlayerCharacterElement(Character character, Client client, int i)
711  {
712  GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement")
713  {
714  UserData = character,
715  Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? OwnCharacterBGColor : Color.Transparent
716  };
717 
718  frame.OnSecondaryClicked += (component, data) =>
719  {
720  NetLobbyScreen.CreateModerationContextMenu(client);
721  return true;
722  };
723 
724  var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
725  {
726  AbsoluteSpacing = 2
727  };
728 
729  new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect))
730  {
731  CanBeFocused = false,
732  HoverColor = Color.White,
733  SelectedColor = Color.White
734  };
735 
736  if (client != null)
737  {
738  CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon);
739  linkedGUIList.Add(new LinkedGUI(client, frame,
740  new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center),
741  permissionIcon));
742  }
743  else
744  {
745  GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
746  ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor);
747 
748  if (character is AICharacter)
749  {
750  linkedGUIList.Add(new LinkedGUI(character, frame,
751  new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = ForceUpperCase.Yes }));
752  }
753  else
754  {
755  linkedGUIList.Add(new LinkedGUI(client: null, frame, textBlock: null, permissionIcon: null));
756 
757  new GUICustomComponent(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => DrawDisconnectedIcon(sb, component.Rect))
758  {
759  CanBeFocused = false,
760  HoverColor = Color.White,
761  SelectedColor = Color.White
762  };
763  }
764  }
765 
766  CreateWalletCrewFrame(character, paddedFrame);
767  }
768 
769  private void CreateMultiPlayerClientElement(Client client)
770  {
771  int teamIndex = GetTeamIndex(client);
772  if (teamIndex == -1) teamIndex = 0;
773 
774  GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[teamIndex].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[teamIndex].Content.RectTransform), style: "ListBoxElement")
775  {
776  UserData = client,
777  Color = Color.Transparent
778  };
779 
780  frame.OnSecondaryClicked += (component, data) =>
781  {
782  NetLobbyScreen.CreateModerationContextMenu(client);
783  return true;
784  };
785 
786  var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
787  {
788  AbsoluteSpacing = 2
789  };
790 
791  new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center),
792  onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client))
793  {
794  CanBeFocused = false,
795  HoverColor = Color.White,
796  SelectedColor = Color.White
797  };
798 
799  CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon);
800  linkedGUIList.Add(new LinkedGUI(client, frame,
801  new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center),
802  permissionIcon));
803 
804  if (client.Character is { } character)
805  {
806  CreateWalletCrewFrame(character, paddedFrame);
807  }
808  }
809 
810  private int GetTeamIndex(Client client)
811  {
812  if (teamIDs.Count <= 1) { return 0; }
813 
814  if (client.Character != null)
815  {
816  return teamIDs.IndexOf(client.Character.TeamID);
817  }
818 
819  if (client.CharacterID != 0)
820  {
821  foreach (Character c in crew)
822  {
823  if (client.CharacterID == c.ID)
824  {
825  return teamIDs.IndexOf(c.TeamID);
826  }
827  }
828  }
829  else
830  {
831  foreach (Character c in crew)
832  {
833  if (client.Name == c.Name)
834  {
835  return teamIDs.IndexOf(c.TeamID);
836  }
837  }
838  }
839 
840  return 0;
841  }
842 
843  private void CreateWalletCrewFrame(Character character, GUILayoutGroup paddedFrame)
844  {
845  if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign)) { return; }
846 
847  GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(new Point(walletColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.Center)
848  {
849  CanBeFocused = false
850  };
851 
852  GUILayoutGroup paddedLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1f), walletLayout.RectTransform, Anchor.Center), isHorizontal: true)
853  {
854  Stretch = true
855  };
856 
857  new GUIFrame(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), style: null)
858  {
859  IgnoreLayoutGroups = true,
860  ToolTip = TextManager.Get("walletdescription")
861  };
862 
863  if (character.IsBot) { return; }
864 
865  Sprite walletSprite = GUIStyle.CrewWalletIconSmall.Value.Sprite;
866 
867  GUIImage icon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), walletSprite, scaleToFit: true) { CanBeFocused = false };
868  GUITextBlock walletBlock = new GUITextBlock(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), string.Empty, textAlignment: Alignment.Right, font: GUIStyle.Font)
869  {
870  AutoScaleHorizontal = true,
871  Padding = Vector4.Zero,
872  CanBeFocused = false
873  };
874 
875  GUIImage largeIcon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), walletSprite, scaleToFit: true)
876  {
877  CanBeFocused = false,
878  IgnoreLayoutGroups = true,
879  Visible = false
880  };
881 
882  if (character.IsBot)
883  {
884  largeIcon.Visible = true;
885  icon.Visible = false;
886  walletBlock.Visible = false;
887  largeIcon.Enabled = false;
888  return;
889  }
890 
891  walletLayout.Recalculate();
892  paddedLayoutGroup.Recalculate();
893  SetWalletText(walletBlock, character.Wallet, icon, largeIcon);
894 
895  if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign)
896  {
897  Identifier eventIdentifier = new Identifier($"{nameof(CreateWalletCrewFrame)}.{character.ID}");
898  campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e =>
899  {
900  if (!e.Owner.TryUnwrap(out var owner) || owner != character) { return; }
901  SetWalletText(walletBlock, e.Wallet, icon, largeIcon);
902  });
903  registeredEvents.Add(eventIdentifier);
904  }
905 
906  static void SetWalletText(GUITextBlock block, Wallet wallet, GUIImage icon, GUIImage largeIcon)
907  {
908  const int million = 1000000,
909  tooSmallPixelTreshold = 50; // 50 pixels is just not enough to see any meaningful info
910 
911  block.Text = TextManager.FormatCurrency(wallet.Balance);
912  block.ToolTip = string.Empty;
913 
914  if (wallet.Balance >= million)
915  {
916  block.Text = TextManager.Get("crewwallet.balance.toomuchtoshow");
917  block.ToolTip = block.Text;
918  }
919 
920  largeIcon.Visible = false;
921  icon.Visible = true;
922  block.Visible = true;
923 
924  if (tooSmallPixelTreshold > block.Rect.Width)
925  {
926  largeIcon.Visible = true;
927  icon.Visible = false;
928  block.Visible = false;
929  largeIcon.ToolTip = block.Text;
930  }
931  }
932  }
933 
934  private void CreateNameWithPermissionIcon(Client client, GUILayoutGroup paddedFrame, out GUIImage permissionIcon)
935  {
936  GUITextBlock characterNameBlock;
937  Sprite permissionIconSprite = GetPermissionIcon(client);
938  JobPrefab prefab = client.Character?.Info?.Job?.Prefab;
939  Color nameColor = prefab != null ? prefab.UIColor : Color.White;
940 
941  Point iconSize = new Point((int)(paddedFrame.Rect.Height * 0.8f));
942  float characterNameWidthAdjustment = (iconSize.X + paddedFrame.AbsoluteSpacing) / characterColumnWidth;
943 
944  characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
945  ToolBox.LimitString(client.Name, GUIStyle.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor);
946 
947  float iconWidth = iconSize.X / (float)characterColumnWidth;
948  int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUIStyle.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width);
949  permissionIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIconSprite) { IgnoreLayoutGroups = true };
950 
951 
952  if (client.Character != null && client.Character.IsDead)
953  {
954  characterNameBlock.Strikethrough = new GUITextBlock.StrikethroughSettings(null, GUI.IntScale(1f), GUI.IntScale(5f));
955  }
956  }
957 
958  private Sprite GetPermissionIcon(Client client)
959  {
960  if (GameMain.NetworkMember == null || client == null || !client.HasPermissions) { return null; }
961 
962  if (client.IsOwner) // Owner cannot be kicked
963  {
964  return ownerIcon;
965  }
966  else
967  {
968  return moderatorIcon;
969  }
970  }
971 
972  private void DrawNotInGameIcon(SpriteBatch spriteBatch, Rectangle area, Client client)
973  {
974  if (client.Spectating)
975  {
976  spectateIcon.Draw(spriteBatch, area, Color.White);
977  }
978  else if (client.Character != null && client.Character.IsDead)
979  {
980  if (client.Character.Info != null)
981  {
982  client.Character.Info.DrawJobIcon(spriteBatch, area);
983  }
984  }
985  else
986  {
987  Vector2 stringOffset = GUIStyle.Font.MeasureString(inLobbyString) / 2f;
988  GUIStyle.Font.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White);
989  }
990  }
991 
992  private void DrawDisconnectedIcon(SpriteBatch spriteBatch, Rectangle area)
993  {
994  disconnectedIcon.Draw(spriteBatch, area, GUIStyle.Red);
995  }
996 
1000  private bool SelectElement(object userData, GUIComponent crewList)
1001  {
1002  Character character = userData as Character;
1003  Client client = userData as Client;
1004 
1005  GUIComponent existingPreview = infoFrameHolder.FindChild("SelectedCharacter");
1006  if (existingPreview != null) { infoFrameHolder.RemoveChild(existingPreview); }
1007 
1008  GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.69f), infoFrameHolder.RectTransform, Anchor.TopRight, Pivot.TopLeft) { RelativeOffset = new Vector2(-0.061f, 0) })
1009  {
1010  UserData = "SelectedCharacter"
1011  };
1012 
1013  if (character != null)
1014  {
1015  if (GameMain.Client is null)
1016  {
1017  GUIComponent preview = character.Info.CreateInfoFrame(background, false, null);
1018  }
1019  else
1020  {
1021  GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character)));
1022 
1023  GameMain.Client.SelectCrewCharacter(character, preview);
1024  if (!character.IsBot && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { CreateWalletFrame(background, character, mpCampaign); }
1025  }
1026 
1027  if (background.FindChild(TalentMenu.ManageBotTalentsButtonUserData, recursive: true) is GUIButton { Enabled: true } talentButton)
1028  {
1029  talentButton.OnClicked = (button, o) =>
1030  {
1031  talentMenu.CreateGUI(infoFrameHolder, character);
1032  return true;
1033  };
1034  }
1035  }
1036  else if (client != null)
1037  {
1038  GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client));
1039  GameMain.Client?.SelectCrewClient(client, preview);
1040  if (client.Character != null && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1041  {
1042  CreateWalletFrame(background, client.Character, mpCampaign);
1043  }
1044  }
1045 
1046  return true;
1047  }
1048 
1049  private void CreateWalletFrame(GUIComponent parent, Character character, MultiPlayerCampaign campaign)
1050  {
1051  if (campaign is null) { throw new ArgumentNullException(nameof(campaign), "Tried to create a wallet frame when campaign was null"); }
1052  if (character is null) { throw new ArgumentNullException(nameof(character), "Tried to create a wallet frame for a null character");}
1053  isTransferMenuOpen = false;
1054  transferMenuOpenState = 1f;
1055  ImmutableHashSet<Character> salaryCrew = GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != character).ToImmutableHashSet();
1056 
1057  Wallet targetWallet = character.Wallet;
1058 
1059  GUIFrame walletFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), parent.RectTransform, anchor: Anchor.TopLeft)
1060  {
1061  RelativeOffset = new Vector2(0, 1.02f)
1062  });
1063 
1064  GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(walletFrame.RectTransform, 0.9f), walletFrame.RectTransform, anchor: Anchor.Center));
1065 
1066  GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), walletLayout.RectTransform), isHorizontal: true);
1067  GUIImage icon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewWalletIconLarge", scaleToFit: true);
1068  float relativeX = icon.RectTransform.NonScaledSize.X / (float)icon.Parent.RectTransform.NonScaledSize.X;
1069  GUILayoutGroup headerTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativeX, 1f), headerLayout.RectTransform), isHorizontal: true) { Stretch = true };
1070  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.Get("crewwallet.wallet"), font: GUIStyle.LargeFont);
1071  GUIFrame walletTooltipFrame = new GUIFrame(new RectTransform(Vector2.One, headerLayout.RectTransform), style: null)
1072  {
1073  IgnoreLayoutGroups = true,
1074  ToolTip = TextManager.Get("walletdescription")
1075  };
1076  GUITextBlock moneyBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.FormatCurrency(targetWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right);
1077 
1078  GUILayoutGroup middleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), walletLayout.RectTransform));
1079  GUILayoutGroup salaryTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true);
1080  GUITextBlock salaryTitle = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), TextManager.Get("crewwallet.salary"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft);
1081  GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), string.Empty, textAlignment: Alignment.BottomRight);
1082  GUIFrame salaryTooltipFrame = new GUIFrame(new RectTransform(Vector2.One, middleLayout.RectTransform), style: null)
1083  {
1084  IgnoreLayoutGroups = true,
1085  ToolTip = TextManager.Get("crewwallet.salary.tooltip")
1086  };
1087  GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center);
1088  GUIScrollBar salarySlider = new GUIScrollBar(new RectTransform(new Vector2(0.9f, 1f), sliderLayout.RectTransform), style: "GUISlider", barSize: 0.03f)
1089  {
1090  Range = new Vector2(0, 1),
1091  BarScrollValue = targetWallet.RewardDistribution / 100f,
1092  Step = 0.01f,
1093  BarSize = 0.1f,
1094  OnMoved = (bar, scroll) =>
1095  {
1096  int rewardDistribution = RoundRewardDistribution(scroll, bar.Step);
1097  SetRewardText(rewardDistribution, rewardBlock);
1098  return true;
1099  },
1100  OnReleased = (bar, scroll) =>
1101  {
1102  int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step);
1103  if (newRewardDistribution == targetWallet.RewardDistribution) { return false; }
1104  SetRewardDistribution(Option.Some(character), newRewardDistribution);
1105  return true;
1106  }
1107  };
1108 
1109  SetRewardText(targetWallet.RewardDistribution, rewardBlock);
1110 
1111 // @formatter:off
1112  GUIScissorComponent scissorComponent = new GUIScissorComponent(new RectTransform(new Vector2(0.85f, 1.25f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter))
1113  {
1114  CanBeFocused = false
1115  };
1116  transferMenu = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform));
1117 
1118  GUILayoutGroup transferMenuLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), transferMenu.RectTransform, Anchor.BottomLeft), childAnchor: Anchor.Center);
1119  GUILayoutGroup paddedTransferMenuLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(transferMenuLayout.RectTransform, 0.85f), transferMenuLayout.RectTransform));
1120  GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), paddedTransferMenuLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
1121  GUILayoutGroup leftLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform));
1122  GUITextBlock leftName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), character.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont);
1123  GUITextBlock leftBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), TextManager.FormatCurrency(targetWallet.Balance), textAlignment: Alignment.Left) { TextColor = GUIStyle.Blue };
1124  GUILayoutGroup rightLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform), childAnchor: Anchor.TopRight);
1125  GUITextBlock rightName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight);
1126  GUITextBlock rightBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, textAlignment: Alignment.Right) { TextColor = GUIStyle.Red };
1127  GUILayoutGroup centerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), mainLayout.RectTransform, Anchor.Center), childAnchor: Anchor.Center) { IgnoreLayoutGroups = true };
1128  new GUIFrame(new RectTransform(new Vector2(0f, 1f), centerLayout.RectTransform, Anchor.Center), style: "VerticalLine") { IgnoreLayoutGroups = true };
1129  GUIButton centerButton = new GUIButton(new RectTransform(new Vector2(1f), centerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight, anchor: Anchor.Center), style: "GUIButtonTransferArrow");
1130 
1131  GUILayoutGroup inputLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center);
1132  GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), NumberType.Int, buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden)
1133  {
1134  MinValueInt = 0
1135  };
1136 
1137  GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center);
1138  GUILayoutGroup centerButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1f), buttonLayout.RectTransform), isHorizontal: true);
1139  GUIButton resetButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("reset"), style: "GUIButtonFreeScale") { Enabled = false };
1140  GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false };
1141 // @formatter:on
1142  ImmutableArray<GUILayoutGroup> layoutGroups = ImmutableArray.Create(transferMenuLayout, paddedTransferMenuLayout, mainLayout, leftLayout, rightLayout);
1143  MedicalClinicUI.EnsureTextDoesntOverflow(character.Name, leftName, leftLayout.Rect, layoutGroups);
1144  transferMenuButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), style: "UIToggleButtonVertical")
1145  {
1146  ToolTip = TextManager.Get("crewwallet.transfer.tooltip"),
1147  OnClicked = (button, o) =>
1148  {
1149  isTransferMenuOpen = !isTransferMenuOpen;
1150  if (!isTransferMenuOpen)
1151  {
1152  transferAmountInput.IntValue = 0;
1153  }
1154  ToggleTransferMenuIcon(button, open: isTransferMenuOpen);
1155  return true;
1156  }
1157  };
1158 
1159  Identifier eventIdentifier = nameof(CreateWalletFrame).ToIdentifier();
1160 
1161  ToggleTransferMenuIcon(transferMenuButton, open: isTransferMenuOpen);
1162  ToggleCenterButton(centerButton, isSending);
1163 
1164  Wallet otherWallet;
1165  GameMain.Client?.OnPermissionChanged.RegisterOverwriteExisting(eventIdentifier, e => UpdateWalletInterface(registerEvents: false));
1166  UpdateWalletInterface(registerEvents: true);
1167 
1168  void UpdateWalletInterface(bool registerEvents)
1169  {
1170  if (!(Character.Controlled is { } myCharacter))
1171  {
1172  salarySlider.Enabled = false;
1173  transferAmountInput.Enabled = false;
1174  centerButton.Enabled = false;
1175  confirmButton.Enabled = false;
1176  return;
1177  }
1178 
1179  bool hasMoneyPermissions = CampaignMode.AllowedToManageWallets();
1180  salarySlider.Enabled = hasMoneyPermissions;
1181 
1182  switch (hasMoneyPermissions)
1183  {
1184  case true:
1185  rightName.Text = TextManager.Get("crewwallet.bank");
1186  otherWallet = campaign.Bank;
1187  break;
1188  case false when character == myCharacter:
1189  rightName.Text = TextManager.Get("crewwallet.bank");
1190  otherWallet = campaign.Bank;
1191  isSending = true;
1192  ToggleCenterButton(centerButton, isSending);
1193  break;
1194  default:
1195  rightName.Text = myCharacter.Name;
1196  otherWallet = campaign.PersonalWallet;
1197  break;
1198  }
1199 
1200  MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups);
1201 
1202  UpdatedConfirmButtonText();
1203 
1204  if (!hasMoneyPermissions)
1205  {
1206  if (character != Character.Controlled)
1207  {
1208  centerButton.Enabled = centerButton.CanBeFocused = false;
1209  }
1210 
1211  salarySlider.Enabled = salarySlider.CanBeFocused = false;
1212  }
1213 
1214  leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance);
1215 
1216  UpdateAllInputs();
1217 
1218  if (!registerEvents) { return; }
1219 
1220  centerButton.OnClicked = (btn, o) =>
1221  {
1222  isSending = !isSending;
1223  UpdatedConfirmButtonText();
1224  ToggleCenterButton(btn, isSending);
1225  UpdateAllInputs();
1226  return true;
1227  };
1228 
1229  transferAmountInput.OnValueChanged = input =>
1230  {
1231  UpdateInputs();
1232  };
1233 
1234  transferAmountInput.OnValueEntered = input =>
1235  {
1236  UpdateAllInputs();
1237  };
1238 
1239  resetButton.OnClicked = (button, o) =>
1240  {
1241  transferAmountInput.IntValue = 0;
1242  UpdateAllInputs();
1243  return true;
1244  };
1245 
1246  confirmButton.OnClicked = (button, o) =>
1247  {
1248  int amount = transferAmountInput.IntValue;
1249  if (amount == 0) { return false; }
1250 
1251  Option<Character> target1 = Option<Character>.Some(character),
1252  target2 = otherWallet == campaign.Bank ? Option<Character>.None() : Option<Character>.Some(myCharacter);
1253  if (isSending) { (target1, target2) = (target2, target1); }
1254 
1255  SendTransaction(target1, target2, amount);
1256  isTransferMenuOpen = false;
1257  ToggleTransferMenuIcon(transferMenuButton, isTransferMenuOpen);
1258  return true;
1259  };
1260 
1261  campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e =>
1262  {
1263  if (e.Wallet == targetWallet)
1264  {
1265  moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance);
1266  salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f;
1267  SetRewardText(e.Info.RewardDistribution, rewardBlock);
1268  }
1269 
1270  UpdateAllInputs();
1271  });
1272 
1273  registeredEvents.Add(eventIdentifier);
1274 
1275  void UpdatedConfirmButtonText()
1276  {
1277  confirmButton.Text = TextManager.Get(hasMoneyPermissions || isSending ? "confirm" : "crewwallet.requestmoney");
1278  }
1279 
1280  void UpdateAllInputs()
1281  {
1282  UpdateInputs();
1283  UpdateMaxInput();
1284  }
1285 
1286  void UpdateInputs()
1287  {
1288  confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0;
1289  if (transferAmountInput.IntValue == 0)
1290  {
1291  rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance);
1292  rightBalance.TextColor = GUIStyle.TextColorNormal;
1293  leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance);
1294  leftBalance.TextColor = GUIStyle.TextColorNormal;
1295  }
1296  else if (isSending)
1297  {
1298  rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue);
1299  rightBalance.TextColor = GUIStyle.Blue;
1300  leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue);
1301  leftBalance.TextColor = GUIStyle.Red;
1302  }
1303  else
1304  {
1305  rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue);
1306  rightBalance.TextColor = GUIStyle.Red;
1307  leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue);
1308  leftBalance.TextColor = GUIStyle.Blue;
1309  }
1310  }
1311 
1312  void UpdateMaxInput()
1313  {
1314  int maxValue = isSending ? targetWallet.Balance : otherWallet.Balance;
1315  transferAmountInput.MaxValueInt = maxValue;
1316 
1317  transferAmountInput.Enabled = true;
1318  transferAmountInput.ToolTip = string.Empty;
1319 
1320  if (!hasMoneyPermissions && GameMain.Client?.ServerSettings is { } serverSettings)
1321  {
1322  transferAmountInput.MaxValueInt = Math.Min(maxValue, serverSettings.MaximumMoneyTransferRequest);
1323  if (serverSettings.MaximumMoneyTransferRequest <= 0)
1324  {
1325  transferAmountInput.Enabled = false;
1326  transferAmountInput.ToolTip = TextManager.Get("wallettransferrequestdisabled");
1327  }
1328  }
1329  }
1330  }
1331 
1332  void SetRewardText(int value, GUITextBlock block)
1333  {
1334  var (_, percentage, sum) = Mission.GetRewardShare(value, salaryCrew, Option<int>.None());
1335  LocalizedString tooltip = string.Empty;
1336  block.TextColor = GUIStyle.TextColorNormal;
1337 
1338  if (sum > 100)
1339  {
1340  tooltip = TextManager.GetWithVariables("crewwallet.salary.over100toolitp", ("[sum]", $"{(int)sum}"), ("[newvalue]", $"{percentage}"));
1341  block.TextColor = GUIStyle.Orange;
1342  }
1343 
1344  LocalizedString text = TextManager.GetWithVariable("percentageformat", "[value]", $"{value}");
1345 
1346  block.Text = text;
1347  block.ToolTip = RichString.Rich(tooltip);
1348  }
1349 
1350  static void ToggleTransferMenuIcon(GUIButton btn, bool open)
1351  {
1352  foreach (GUIComponent child in btn.Children)
1353  {
1354  child.SpriteEffects = open ? SpriteEffects.None : SpriteEffects.FlipVertically;
1355  }
1356  }
1357 
1358  static void ToggleCenterButton(GUIButton btn, bool isSending)
1359  {
1360  foreach (GUIComponent child in btn.Children)
1361  {
1362  child.SpriteEffects = isSending ? SpriteEffects.None : SpriteEffects.FlipHorizontally;
1363  }
1364  }
1365 
1366  static void SendTransaction(Option<Character> to, Option<Character> from, int amount)
1367  {
1368  INetSerializableStruct transfer = new NetWalletTransfer
1369  {
1370  Sender = from.Select(option => option.ID),
1371  Receiver = to.Select(option => option.ID),
1372  Amount = amount
1373  };
1374  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.TRANSFER_MONEY);
1375  transfer.Write(msg);
1376  GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
1377  }
1378  }
1379 
1380  static void SetRewardDistribution(Option<Character> character, int newValue)
1381  {
1382  INetSerializableStruct transfer = new NetWalletSetSalaryUpdate
1383  {
1384  Target = character.Select(c => c.ID),
1385  NewRewardDistribution = newValue
1386  };
1387  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.REWARD_DISTRIBUTION);
1388  transfer.Write(msg);
1389  GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
1390  }
1391 
1392  static void ResetRewardDistributions()
1393  {
1394  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.RESET_REWARD_DISTRIBUTION);
1395  GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
1396  }
1397 
1398  static int RoundRewardDistribution(float scroll, float step)
1399  => (int)MathUtils.RoundTowardsClosest(scroll * 100, step * 100);
1400 
1401  private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null)
1402  {
1403  GUIComponent paddedFrame;
1404 
1405  if (client.Character?.Info == null)
1406  {
1407  paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.874f, 0.58f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) })
1408  {
1409  RelativeSpacing = 0.05f
1410  //Stretch = true
1411  };
1412 
1413  var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true);
1414 
1415  new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform),
1416  onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client));
1417 
1418  GUIFont font = paddedFrame.Rect.Width < 280 ? GUIStyle.SmallFont : GUIStyle.Font;
1419 
1420  var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform))
1421  {
1422  RelativeSpacing = 0.02f,
1423  Stretch = true
1424  };
1425 
1426  GUITextBlock clientNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(client.Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: Color.White, font: GUIStyle.Font)
1427  {
1429  Padding = Vector4.Zero
1430  };
1431 
1432  if (permissionIcon != null)
1433  {
1434  Point iconSize = permissionIcon.SourceRect.Size;
1435  int iconWidth = (int)((float)clientNameBlock.Rect.Height / iconSize.Y * iconSize.X);
1436  new GUIImage(new RectTransform(new Point(iconWidth, clientNameBlock.Rect.Height), clientNameBlock.RectTransform) { AbsoluteOffset = new Point(-iconWidth - 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true };
1437  }
1438 
1439  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), client.Spectating ? TextManager.Get("playingasspectator") : TextManager.Get("tabmenu.inlobby"), textColor: Color.White, font: font, wrap: true)
1440  {
1441  Padding = Vector4.Zero
1442  };
1443  }
1444  else
1445  {
1446  paddedFrame = client.Character.Info.CreateInfoFrame(frame, false, permissionIcon);
1447  }
1448 
1449  return paddedFrame;
1450  }
1451 
1452  private void CreateMultiPlayerLogContent(GUIFrame crewFrame)
1453  {
1454  var logContainer = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.145f, 0) });
1455  var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.900f, 0.900f), logContainer.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, 0.0475f) }, style: null);
1456  var content = new GUILayoutGroup(new RectTransform(Vector2.One, innerFrame.RectTransform))
1457  {
1458  Stretch = true
1459  };
1460 
1461  logList = new GUIListBox(new RectTransform(Vector2.One, content.RectTransform))
1462  {
1463  Padding = new Vector4(0, 10 * GUI.Scale, 0, 10 * GUI.Scale),
1464  UserData = crewFrame,
1465  AutoHideScrollBar = false,
1466  Spacing = (int)(5 * GUI.Scale)
1467  };
1468 
1469  foreach ((string message, PlayerConnectionChangeType type) in storedMessages)
1470  {
1471  AddLineToLog(message, type);
1472  }
1473 
1474  logList.BarScroll = 1f;
1475  }
1476 
1477  private static readonly List<(string message, PlayerConnectionChangeType type)> storedMessages = new List<(string message, PlayerConnectionChangeType type)>();
1478 
1480  {
1481  if (!GameMain.GameSession?.IsRunning ?? true) { return; }
1482 
1483  string msg = ChatMessage.GetTimeStamp() + message.TextWithSender;
1484  storedMessages.Add((msg, message.ChangeType));
1485 
1487  {
1488  TabMenu instance = GameSession.TabMenuInstance;
1489  instance.AddLineToLog(msg, message.ChangeType);
1490  instance.RemoveCurrentElements();
1491  instance.CreateMultiPlayerList(true);
1492  }
1493  }
1494 
1495  private void RemoveCurrentElements()
1496  {
1497  for (int i = 0; i < crewListArray.Length; i++)
1498  {
1499  for (int j = 0; j < linkedGUIList.Count; j++)
1500  {
1501  linkedGUIList[j].Remove(crewListArray[i].Content);
1502  }
1503  }
1504 
1505  linkedGUIList.Clear();
1506  }
1507 
1508  private void AddLineToLog(string line, PlayerConnectionChangeType type)
1509  {
1510  Color textColor = Color.White;
1511 
1512  switch (type)
1513  {
1514  case PlayerConnectionChangeType.Joined:
1515  textColor = GUIStyle.Green;
1516  break;
1517  case PlayerConnectionChangeType.Kicked:
1518  textColor = GUIStyle.Orange;
1519  break;
1520  case PlayerConnectionChangeType.Disconnected:
1521  textColor = GUIStyle.Yellow;
1522  break;
1523  case PlayerConnectionChangeType.Banned:
1524  textColor = GUIStyle.Red;
1525  break;
1526  }
1527 
1528  if (logList != null)
1529  {
1530  var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), RichString.Rich(line), wrap: true, font: GUIStyle.SmallFont)
1531  {
1532  TextColor = textColor,
1533  CanBeFocused = false,
1534  UserData = line
1535  };
1536  textBlock.CalculateHeightFromText();
1537  if (textBlock.HasColorHighlight)
1538  {
1539  foreach (var data in textBlock.RichTextData)
1540  {
1541  textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea()
1542  {
1543  Data = data,
1544  OnClick = GameMain.NetLobbyScreen.SelectPlayer,
1545  OnSecondaryClick = GameMain.NetLobbyScreen.ShowPlayerContextMenu
1546  });
1547  }
1548  }
1549  }
1550  }
1551 
1552  private void CreateMissionInfo(GUIFrame infoFrame)
1553  {
1554  if (Level.Loaded?.LevelData == null)
1555  {
1556  DebugConsole.ThrowError("Failed to display mission info in the tab menu (no level loaded).\n" + Environment.StackTrace);
1557  return;
1558  }
1559 
1560  infoFrame.ClearChildren();
1561  GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
1562  int padding = (int)(0.0245f * missionFrame.Rect.Height);
1563  GUIFrame missionFrameContent = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, missionFrame.Rect.Height - padding * 2), infoFrame.RectTransform, Anchor.Center), style: null);
1564  Location location = GameMain.GameSession.StartLocation;
1565  if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
1566  {
1567  location ??= GameMain.GameSession.EndLocation;
1568  }
1569 
1570  GUILayoutGroup locationInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), missionFrameContent.RectTransform))
1571  {
1572  AbsoluteSpacing = GUI.IntScale(10)
1573  };
1574 
1575  Sprite portrait = location.Type.GetPortrait(location.PortraitId);
1576  bool hasPortrait = portrait != null && portrait.SourceRect.Width > 0 && portrait.SourceRect.Height > 0;
1577  int contentWidth = missionFrameContent.Rect.Width;
1578  if (hasPortrait)
1579  {
1580  float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height;
1581  GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(0.5f, 1f), locationInfoContainer.RectTransform, Anchor.CenterRight), portrait, scaleToFit: true)
1582  {
1583  IgnoreLayoutGroups = true
1584  };
1585  locationInfoContainer.Recalculate();
1586  portraitImage.RectTransform.NonScaledSize = new Point(Math.Min((int)(portraitImage.Rect.Size.Y * portraitAspectRatio), portraitImage.Rect.Width), portraitImage.Rect.Size.Y);
1587  }
1588 
1589  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.DisplayName, font: GUIStyle.LargeFont);
1590  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont);
1591 
1592  if (location.Faction?.Prefab != null)
1593  {
1594  var factionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform),
1595  TextManager.Get("Faction"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft);
1596  new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), factionLabel.RectTransform), location.Faction.Prefab.Name, textAlignment: Alignment.CenterRight);
1597  }
1598  var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform),
1599  TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft);
1600  new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight);
1601  var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform),
1602  TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft);
1603  new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", ((int)Level.Loaded.LevelData.Difficulty).ToString()), textAlignment: Alignment.CenterRight);
1604 
1605  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine")
1606  {
1607  CanBeFocused = false
1608  };
1609 
1610  int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2;
1611 
1612  GUIListBox missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrameContent.Rect.Height - locationInfoYOffset), missionFrameContent.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) });
1613  missionList.ContentBackground.Color = Color.Transparent;
1614  missionList.Spacing = GUI.IntScale(15);
1615 
1616  if (GameMain.GameSession?.Missions != null)
1617  {
1618  foreach (Mission mission in GameMain.GameSession.Missions)
1619  {
1620  if (!mission.Prefab.ShowInMenus) { continue; }
1621 
1622  var textContent = new List<LocalizedString>()
1623  {
1624  mission.GetMissionRewardText(Submarine.MainSub),
1625  mission.GetReputationRewardText(),
1626  mission.Description
1627  };
1628  textContent.AddRange(mission.ShownMessages);
1629 
1630  RoundSummary.CreateMissionEntry(
1631  missionList.Content,
1632  mission.Name,
1633  textContent,
1634  mission.Difficulty ?? 0,
1635  mission.Prefab.Icon, mission.Prefab.IconColor,
1636  out GUIImage missionIcon);
1637  if (missionIcon != null)
1638  {
1639  UpdateMissionStateIcon();
1640  mission.OnMissionStateChanged += (mission) => UpdateMissionStateIcon();
1641 
1642  void UpdateMissionStateIcon()
1643  {
1644  if (mission.DisplayAsCompleted || mission.DisplayAsFailed)
1645  {
1646  RoundSummary.UpdateMissionStateIcon(mission.DisplayAsCompleted, missionIcon);
1647  }
1648  }
1649  }
1650  }
1651  }
1652  else
1653  {
1654  GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionList.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft);
1655  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUIStyle.LargeFont);
1656  }
1657 
1658  GameMain.GameSession?.EventManager?.EventLog?.CreateEventLogUI(missionList.Content);
1659  GameMain.GameSession.EnableEventLogNotificationIcon(enabled: false);
1660 
1661  RoundSummary.AddSeparators(missionList.Content);
1662  }
1663 
1664  private static void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub)
1665  {
1666  GUIFrame subInfoFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
1667  GUIFrame paddedFrame = new GUIFrame(new RectTransform(Vector2.One * 0.97f, subInfoFrame.RectTransform, Anchor.Center), style: null);
1668 
1669  var previewButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.43f), paddedFrame.RectTransform), style: null)
1670  {
1671  OnClicked = (btn, obj) => { SubmarinePreview.Create(sub.Info); return false; },
1672  };
1673 
1674  var previewImage = sub.Info.PreviewImage ?? SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(sub.Info.Name, StringComparison.OrdinalIgnoreCase))?.PreviewImage;
1675  if (previewImage == null)
1676  {
1677  new GUITextBlock(new RectTransform(Vector2.One, previewButton.RectTransform), TextManager.Get("SubPreviewImageNotFound"));
1678  }
1679  else
1680  {
1681  var submarinePreviewBackground = new GUIFrame(new RectTransform(Vector2.One, previewButton.RectTransform), style: null)
1682  {
1683  Color = Color.Black,
1684  HoverColor = Color.Black,
1685  SelectedColor = Color.Black,
1686  PressedColor = Color.Black,
1687  CanBeFocused = false,
1688  };
1689  new GUIImage(new RectTransform(new Vector2(0.98f), submarinePreviewBackground.RectTransform, Anchor.Center), previewImage, scaleToFit: true) { CanBeFocused = false };
1690  new GUIFrame(new RectTransform(Vector2.One, submarinePreviewBackground.RectTransform), "InnerGlow", color: Color.Black) { CanBeFocused = false };
1691  }
1692 
1693  new GUIFrame(new RectTransform(Vector2.One * 0.12f, previewButton.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight)
1694  {
1695  AbsoluteOffset = new Point((int)(0.03f * previewButton.Rect.Height))
1696  },
1697  "ExpandButton", Color.White)
1698  {
1699  Color = Color.White,
1700  HoverColor = Color.White,
1701  PressedColor = Color.White
1702  };
1703 
1704  var subInfoTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedFrame.RectTransform));
1705 
1706  LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ?
1707  TextManager.GetWithVariables("submarine.classandtier",
1708  ("[class]", TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}")),
1709  ("[tier]", TextManager.Get($"submarinetier.{sub.Info.Tier}"))) :
1710  TextManager.Get("shuttle");
1711 
1712  int nameHeight = (int)GUIStyle.LargeFont.MeasureString(sub.Info.DisplayName, true).Y;
1713  int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y;
1714 
1715  var submarineNameText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, nameHeight + HUDLayoutSettings.Padding / 2), subInfoTextLayout.RectTransform), sub.Info.DisplayName, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false };
1716  submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y);
1717  var submarineClassText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, classHeight), subInfoTextLayout.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { CanBeFocused = false };
1718  submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y);
1719 
1720  if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1721  {
1722  GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.09f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0f, 0.43f) }, isHorizontal: true) { Stretch = true };
1723  GUIImage headerIcon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SubmarineIcon");
1724  new GUITextBlock(new RectTransform(Vector2.One, headerLayout.RectTransform), TextManager.Get("uicategory.upgrades"), font: GUIStyle.LargeFont);
1725 
1726  var upgradeRootLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.48f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft), isHorizontal: true);
1727 
1728  var upgradeCategoryPanel = UpgradeStore.CreateUpgradeCategoryList(new RectTransform(new Vector2(0.4f, 1f), upgradeRootLayout.RectTransform));
1729  upgradeCategoryPanel.HideChildrenOutsideFrame = true;
1730  UpgradeStore.UpdateCategoryList(upgradeCategoryPanel, campaign, sub, UpgradeStore.GetApplicableCategories(sub).ToArray());
1731  GUIComponent[] toRemove = upgradeCategoryPanel.Content.FindChildren(c => !c.Enabled).ToArray();
1732  toRemove.ForEach(c => upgradeCategoryPanel.RemoveChild(c));
1733 
1734  var upgradePanel = new GUIListBox(new RectTransform(new Vector2(0.6f, 1f), upgradeRootLayout.RectTransform));
1735  upgradeCategoryPanel.OnSelected = (component, userData) =>
1736  {
1737  upgradePanel.ClearChildren();
1738  if (userData is UpgradeStore.CategoryData categoryData && Submarine.MainSub != null)
1739  {
1740  foreach (UpgradePrefab prefab in categoryData.Prefabs)
1741  {
1742  var frame = UpgradeStore.CreateUpgradeFrame(prefab, categoryData.Category, campaign, new RectTransform(new Vector2(1f, 0.3f), upgradePanel.Content.RectTransform), addBuyButton: false).Frame;
1743  UpgradeStore.UpdateUpgradeEntry(frame, prefab, categoryData.Category, campaign);
1744  }
1745  }
1746  return true;
1747  };
1748  }
1749  else
1750  {
1751  var specsListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.57f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft))
1752  {
1753  CurrentSelectMode = GUIListBox.SelectMode.None
1754  };
1755  sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font,
1756  includeTitle: false,
1757  includeClass: false,
1758  includeDescription: true);
1759  }
1760  }
1761 
1762  private GUIImage talentPointNotification, eventLogNotification;
1763 
1764  public static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
1765  {
1766  parent.Content.ClearChildren();
1767  List<GUITextBlock> skillNames = new List<GUITextBlock>();
1768  foreach (Skill skill in info.Job.GetSkills())
1769  {
1770  GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.0f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = true };
1771  var skillName = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value));
1772  skillNames.Add(skillName);
1773  skillName.RectTransform.MinSize = new Point(0, skillName.Rect.Height);
1774  skillContainer.RectTransform.MinSize = new Point(0, skillName.Rect.Height);
1775 
1776  new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.TopRight);
1777 
1778  float modifiedSkillLevel = MathF.Floor(character?.GetSkillLevel(skill.Identifier) ?? skill.Level);
1779  if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level)))
1780  {
1781  int skillChange = (int)MathF.Floor(modifiedSkillLevel - MathF.Floor(skill.Level));
1782  string stringColor = skillChange switch
1783  {
1784  > 0 => XMLExtensions.ToStringHex(GUIStyle.Green),
1785  < 0 => XMLExtensions.ToStringHex(GUIStyle.Red),
1786  _ => XMLExtensions.ToStringHex(GUIStyle.TextColorNormal)
1787  };
1788 
1789  RichString changeText = RichString.Rich($"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)");
1790  new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText) { Padding = Vector4.Zero };
1791  }
1792  skillContainer.Recalculate();
1793  }
1794 
1795  parent.RecalculateChildren();
1796  GUITextBlock.AutoScaleAndNormalize(skillNames);
1797  }
1798 
1799  public void OnExperienceChanged(Character character)
1800  {
1801  if (character != Character.Controlled) { return; }
1802  talentMenu.UpdateTalentInfo();
1803  }
1804 
1805  public void OnClose()
1806  {
1807  if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; }
1808  foreach (Identifier identifier in registeredEvents)
1809  {
1810  campaign.OnMoneyChanged.TryDeregister(identifier);
1811  }
1812  }
1813  }
1814 }
Stores information about the Character that is needed between rounds in the menu etc....
GUIComponent CreateInfoFrame(GUIFrame frame, bool returnParent, Sprite permissionIcon=null)
void DrawJobIcon(SpriteBatch spriteBatch, Rectangle area, bool evaluateDisguise=false)
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
void Pulsate(Vector2 startScale, Vector2 endScale, float duration)
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
virtual void ClearChildren()
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
virtual Rectangle Rect
RectTransform RectTransform
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
static void AutoScaleAndNormalize(params GUITextBlock[] textBlocks)
Set the text scale of the GUITextBlocks so that they all use the same scale and can fit the text with...
static GameSession?? GameSession
Definition: GameMain.cs:88
static bool IsMultiplayer
Definition: GameMain.cs:35
static GameClient Client
Definition: GameMain.cs:188
JobPrefab Prefab
Definition: Job.cs:18
Point AbsoluteOffset
Absolute in pixels but relative to the anchor point. Calculated away from the anchor point,...
Vector2 RelativeSize
Relative to the parent rect.
Point?? MinSize
Min size in pixels. Does not affect scaling.
static RichString Rich(LocalizedString str, Func< string, string >? postProcess=null)
Definition: RichString.cs:67
void CreateReputationInfoPanel(GUIComponent parent, CampaignMode campaignMode)
readonly Identifier Identifier
Definition: Skill.cs:7
float Level
Definition: Skill.cs:19
static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
Definition: TabMenu.cs:1764
void Update(float deltaTime)
Definition: TabMenu.cs:177
static Color OwnCharacterBGColor
Definition: TabMenu.cs:39
void SelectInfoFrameTab(InfoFrameTab selectedTab)
Definition: TabMenu.cs:434
static GUIFrame PendingChangesFrame
Definition: TabMenu.cs:37
void AddToGUIUpdateList()
Definition: TabMenu.cs:249
static InfoFrameTab SelectedTab
Definition: TabMenu.cs:23
static bool PendingChanges
Definition: TabMenu.cs:15
void Initialize()
Definition: TabMenu.cs:156
void OnExperienceChanged(Character character)
Definition: TabMenu.cs:1799
static void OnRoundEnded()
Definition: TabMenu.cs:255
static void StorePlayerConnectionChangeMessage(ChatMessage message)
Definition: TabMenu.cs:1479
void Draw(SpriteBatch spriteBatch, RectangleF rect, Color color, SpriteEffects spriteEffects=SpriteEffects.None, Vector2? uvOffset=null)
NumberType
Definition: Enums.cs:715
CharacterType
Definition: Enums.cs:685