Client LuaCsForBarotrauma
TalentMenu.cs
1 #nullable enable
2 
3 using System;
4 using System.Collections.Generic;
5 using System.Collections.Immutable;
6 using System.Linq;
9 using Microsoft.Xna.Framework;
10 using Microsoft.Xna.Framework.Input;
11 using static Barotrauma.TalentTree;
12 using static Barotrauma.TalentTree.TalentStages;
13 
14 namespace Barotrauma
15 {
16  internal readonly record struct TalentShowCaseButton(ImmutableHashSet<TalentButton> Buttons,
17  GUIComponent IconComponent);
18 
19  internal readonly record struct TalentButton(GUIComponent IconComponent,
20  TalentPrefab Prefab)
21  {
22  public Identifier Identifier => Prefab.Identifier;
23  }
24 
25  internal readonly record struct TalentCornerIcon(Identifier TalentTree,
26  int Index,
27  GUIImage IconComponent,
28  GUIFrame BackgroundComponent,
29  GUIFrame GlowComponent);
30 
31  internal readonly struct TalentTreeStyle
32  {
33  public readonly GUIComponentStyle ComponentStyle;
34  public readonly Color Color;
35 
36  public TalentTreeStyle(string componentStyle, Color color)
37  {
38  ComponentStyle = GUIStyle.GetComponentStyle(componentStyle);
39  Color = color;
40  }
41  }
42 
43  internal sealed class TalentMenu
44  {
45  public const string ManageBotTalentsButtonUserData = "managebottalentsbutton";
46 
47  private Character? character;
48  private CharacterInfo? characterInfo;
49 
50  private static readonly Color unselectedColor = new Color(240, 255, 255, 225),
51  unselectableColor = new Color(100, 100, 100, 225),
52  pressedColor = new Color(60, 60, 60, 225),
53  lockedColor = new Color(48, 48, 48, 255),
54  unlockedColor = new Color(24, 37, 31, 255),
55  availableColor = new Color(50, 47, 33, 255);
56 
57  private static readonly ImmutableDictionary<TalentStages, TalentTreeStyle> talentStageStyles =
58  new Dictionary<TalentStages, TalentTreeStyle>
59  {
60  [Invalid] = new TalentTreeStyle("TalentTreeLocked", lockedColor),
61  [Locked] = new TalentTreeStyle("TalentTreeLocked", lockedColor),
62  [Unlocked] = new TalentTreeStyle("TalentTreePurchased", unlockedColor),
63  [Available] = new TalentTreeStyle("TalentTreeUnlocked", availableColor),
64  [Highlighted] = new TalentTreeStyle("TalentTreeAvailable", availableColor)
65  }.ToImmutableDictionary();
66 
67  private readonly HashSet<TalentButton> talentButtons = new HashSet<TalentButton>();
68  private readonly HashSet<TalentShowCaseButton> talentShowCaseButtons = new HashSet<TalentShowCaseButton>();
69  private readonly HashSet<GUIComponent> showCaseTalentFrames = new HashSet<GUIComponent>();
70  private readonly HashSet<TalentCornerIcon> talentCornerIcons = new HashSet<TalentCornerIcon>();
71  private HashSet<Identifier> selectedTalents = new HashSet<Identifier>();
72 
73  private readonly Queue<Identifier> showCaseClosureQueue = new();
74 
75  private GUITextBlock? nameBlock;
76  private GUIButton? renameButton;
77  private GUIListBox? skillListBox;
78  private GUITextBlock? talentPointText;
79  private GUIProgressBar? experienceBar;
80  private GUITextBlock? experienceText;
81  private GUILayoutGroup? skillLayout;
82 
83  private GUIButton? talentApplyButton,
84  talentResetButton;
85 
86  public void CreateGUI(GUIFrame parent, Character? targetCharacter)
87  {
88  parent.ClearChildren();
89  talentButtons.Clear();
90  talentShowCaseButtons.Clear();
91  talentCornerIcons.Clear();
92  showCaseTalentFrames.Clear();
93 
94  character = targetCharacter;
95  characterInfo = targetCharacter?.Info;
96 
97  GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
98  int padding = GUI.IntScale(15);
99  GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), parent.RectTransform, Anchor.Center), style: null);
100 
101  GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null);
102 
103  GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter)
104  {
105  AbsoluteSpacing = GUI.IntScale(10),
106  Stretch = true
107  };
108 
109  if (characterInfo is null) { return; }
110 
111  CreateStatPanel(contentLayout, characterInfo);
112 
113  new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine");
114 
115  if (JobTalentTrees.TryGet(characterInfo.Job.Prefab.Identifier, out TalentTree? talentTree))
116  {
117  CreateTalentMenu(contentLayout, characterInfo, talentTree!);
118  }
119 
120  CreateFooter(contentLayout, characterInfo);
121  UpdateTalentInfo();
122 
123  if (GameMain.NetworkMember != null && IsOwnCharacter(characterInfo))
124  {
125  CreateMultiplayerCharacterSettings(frame, content);
126  }
127  }
128 
129  private void CreateMultiplayerCharacterSettings(GUIComponent parent, GUIComponent content)
130  {
131  if (skillLayout is null) { return; }
132 
133  GUIFrame characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null) { Visible = false };
134  GUILayoutGroup characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform));
135  GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null);
136  GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter));
137  GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
138 
139  if (!GameMain.NetLobbyScreen.PermadeathMode)
140  {
141  GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
142  text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
143  {
144  IgnoreLayoutGroups = false,
145  TextBlock =
146  {
147  AutoScaleHorizontal = true
148  }
149  };
150 
151  newCharacterBox.OnClicked = (button, o) =>
152  {
153  if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
154  {
155  GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
156  {
157  newCharacterBox.Text = TextManager.Get("settings");
158  if (TabMenu.PendingChangesFrame != null)
159  {
160  NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame);
161  }
162 
163  OpenMenu();
164  });
165  return true;
166  }
167 
168  OpenMenu();
169  return true;
170 
171  void OpenMenu()
172  {
173  characterSettingsFrame!.Visible = true;
174  content.Visible = false;
175  }
176  };
177  }
178  else if (characterInfo != null)
179  {
180  renameButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
181  text: TextManager.Get("button.RenameCharacter"), style: "GUIButtonSmall")
182  {
183  Enabled = characterInfo.RenamingEnabled,
184  ToolTip = TextManager.Get("permadeath.rename.description"),
185  IgnoreLayoutGroups = false,
186  TextBlock =
187  {
188  AutoScaleHorizontal = true
189  },
190  OnClicked = (_, _) =>
191  {
192  CreateRenamePopup();
193  return true;
194  }
195  };
196  }
197 
198  GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter);
199  new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages?
200  {
201  OnClicked = (button, o) =>
202  {
203  GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
204  GameMain.NetLobbyScreen.CampaignCharacterDiscarded = false;
205  characterSettingsFrame.Visible = false;
206  content.Visible = true;
207  return true;
208  }
209  };
210  }
211 
212  private void CreateRenamePopup()
213  {
214  GUIMessageBox renamePopup = new(
215  TextManager.Get("button.RenameCharacter"), TextManager.Get("permadeath.rename.description"),
216  new LocalizedString[] { TextManager.Get("Confirm"), TextManager.Get("Cancel") }, minSize: new Point(0, GUI.IntScale(230)));
217  GUITextBox newNameBox = new(new(Vector2.One, renamePopup.Content.RectTransform), "")
218  {
219  OnEnterPressed = (textBox, text) =>
220  {
221  textBox.Text = text.Trim();
222  return true;
223  }
224  };
225  renamePopup.Buttons[0].OnClicked += (_, _) =>
226  {
227  if (newNameBox.Text?.Trim() is string newName && newName != "")
228  {
229  if (characterInfo != null)
230  {
231  if (newNameBox.Text == characterInfo.Name)
232  {
233  renamePopup.Close();
234  return true;
235  }
236  if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement)
237  {
238  crewManagement.RenameCharacter(characterInfo, newName);
239  if (nameBlock != null)
240  {
241  nameBlock.Text = newName;
242  }
243  if (renameButton != null)
244  {
245  renameButton.Enabled = false;
246  }
247  renamePopup.Close();
248  }
249  return true;
250  }
251  DebugConsole.ThrowError("Tried to rename character, but CharacterInfo completely missing!");
252  return true;
253  }
254  else
255  {
256  newNameBox.Flash();
257  return false;
258  }
259  };
260  renamePopup.Buttons[1].OnClicked += renamePopup.Close;
261  }
262 
263  private void CreateStatPanel(GUIComponent parent, CharacterInfo info)
264  {
265  Job job = info.Job;
266 
267  GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform, Anchor.Center), isHorizontal: true);
268 
269  new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) =>
270  {
271  info.DrawPortrait(batch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width, false, false);
272  });
273 
274  GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform))
275  {
276  AbsoluteSpacing = GUI.IntScale(5),
277  CanBeFocused = true
278  };
279 
280  nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
281 
282  if (!info.OmitJobInMenus)
283  {
284  nameBlock.TextColor = job.Prefab.UIColor;
285  GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor };
286  }
287 
288  if (info.PersonalityTrait != null)
289  {
290  LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName);
291  Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString);
292  GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont);
293  traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint();
294  }
295 
296  ImmutableHashSet<TalentPrefab?> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet();
297  if (talentsOutsideTree.Any())
298  {
299  //spacing
300  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), nameLayout.RectTransform), style: null);
301 
302  GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.55f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter);
303 
304  talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont)
305  {
306  AutoScaleVertical = true
307  };
308  talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y);
309 
310  var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.7f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true)
311  {
312  AutoHideScrollBar = false,
313  ResizeContentToMakeSpaceForScrollBar = false
314  };
315  extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter);
316  extraTalentLayout.Recalculate();
317  extraTalentList.ForceLayoutRecalculation();
318 
319  foreach (var extraTalent in talentsOutsideTree)
320  {
321  if (extraTalent is null) { continue; }
322  GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true)
323  {
324  ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)),
325  Color = GUIStyle.Green
326  };
327  }
328  }
329 
330  skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight)
331  {
332  AbsoluteSpacing = GUI.IntScale(5),
333  Stretch = true
334  };
335 
336  GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont);
337 
338  skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null);
339  TabMenu.CreateSkillList(info.Character, info, skillListBox);
340  }
341 
342  private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree)
343  {
344  GUIListBox mainList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, anchor: Anchor.TopCenter));
345 
346  selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet();
347 
348  var specializationCount = tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization);
349 
350  List<GUITextBlock> subTreeNames = new List<GUITextBlock>();
351  foreach (var subTree in tree.TalentSubTrees)
352  {
353  GUIListBox talentList;
354  GUIComponent talentParent;
355  Vector2 treeSize;
356  switch (subTree.Type)
357  {
358  case TalentTreeType.Primary:
359  talentList = mainList;
360  treeSize = new Vector2(1f, 0.5f);
361  break;
362  case TalentTreeType.Specialization:
363  talentList = GetSpecializationList();
364  treeSize = new Vector2(Math.Max(0.333f, 1.0f / tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization)), 1f);
365  break;
366  default:
367  throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\"");
368  }
369  talentParent = talentList.Content;
370 
371  GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(treeSize, talentParent.RectTransform), isHorizontal: false, childAnchor: Anchor.TopCenter)
372  {
373  Stretch = true
374  };
375 
376  if (subTree.Type != TalentTreeType.Primary)
377  {
378  GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter)
379  { MinSize = new Point(0, GUI.IntScale(30)) }, style: null);
380  subtreeTitleFrame.RectTransform.IsFixedSize = true;
381  int elementPadding = GUI.IntScale(8);
382  Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize;
383  GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader");
384  subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center));
385  }
386 
387  int optionAmount = subTree.TalentOptionStages.Length;
388  for (int i = 0; i < optionAmount; i++)
389  {
390  TalentOption option = subTree.TalentOptionStages[i];
391  CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info, specializationCount);
392  }
393  subTreeLayoutGroup.RectTransform.Resize(new Point(subTreeLayoutGroup.Rect.Width,
394  subTreeLayoutGroup.Children.Sum(c => c.Rect.Height + subTreeLayoutGroup.AbsoluteSpacing)));
395  subTreeLayoutGroup.RectTransform.MinSize = new Point(subTreeLayoutGroup.Rect.Width, subTreeLayoutGroup.Rect.Height);
396  subTreeLayoutGroup.Recalculate();
397 
398  if (subTree.Type == TalentTreeType.Specialization)
399  {
400  talentList.RectTransform.Resize(new Point(talentList.Rect.Width, Math.Max(subTreeLayoutGroup.Rect.Height, talentList.Rect.Height)));
401  talentList.RectTransform.MinSize = new Point(0, talentList.Rect.Height);
402  }
403  }
404 
405  var specializationList = GetSpecializationList();
406  //resize (scale up) children if there's less than 3 of them to make them cover the whole width of the menu
407  specializationList.Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height),
408  resizeChildren: specializationCount < 3);
409  //make room for scrollbar if there's more than the default amount of specializations
410  if (specializationCount > 3)
411  {
412  specializationList.RectTransform.MinSize = new Point(specializationList.Rect.Width, specializationList.Content.Rect.Height + (int)(specializationList.ScrollBar.Rect.Height * 0.9f));
413  }
414 
415  GUITextBlock.AutoScaleAndNormalize(subTreeNames);
416 
417  GUIListBox GetSpecializationList()
418  {
419  if (mainList.Content.Children.LastOrDefault() is GUIListBox specList)
420  {
421  return specList;
422  }
423  GUIListBox newSpecializationList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), mainList.Content.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null);
424  return newSpecializationList;
425  }
426  }
427 
428  private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info, int specializationCount)
429  {
430  int elementPadding = GUI.IntScale(8);
431  int height = GUI.IntScale((GameMain.GameSession?.Campaign == null ? 65 : 60) * (specializationCount > 3 ? 0.97f : 1.0f));
432  GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.01f), parent.RectTransform, anchor: Anchor.TopCenter)
433  { MinSize = new Point(0, height) }, style: null);
434 
435  Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize;
436 
437  GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center),
438  style: "TalentBackground")
439  {
440  Color = talentStageStyles[Locked].Color
441  };
442  GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false };
443 
444  GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null)
445  {
446  CanBeFocused = false,
447  Color = talentStageStyles[Locked].Color
448  };
449 
450  Point iconSize = cornerIcon.RectTransform.NonScaledSize;
451  cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2);
452 
453  GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 0.9f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
454  GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
455 
456  HashSet<Identifier> talentOptionIdentifiers = talentOption.TalentIdentifiers.OrderBy(static t => t).ToHashSet();
457  HashSet<TalentButton> buttonsToAdd = new();
458 
459  Dictionary<GUILayoutGroup, ImmutableHashSet<Identifier>> showCaseTalentParents = new();
460  Dictionary<Identifier, GUIComponent> showCaseTalentButtonsToAdd = new();
461 
462  foreach (var (showCaseTalentIdentifier, talents) in talentOption.ShowCaseTalents)
463  {
464  talentOptionIdentifiers.Add(showCaseTalentIdentifier);
465  Point parentSize = talentBackground.RectTransform.NonScaledSize;
466  GUIFrame showCaseFrame = new GUIFrame(new RectTransform(new Point((int)(parentSize.X / 3f * (talents.Count - 1)), parentSize.Y)), style: "GUITooltip")
467  {
468  UserData = showCaseTalentIdentifier,
469  IgnoreLayoutGroups = true,
470  Visible = false
471  };
472  GUILayoutGroup showcaseCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.7f), showCaseFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
473  GUILayoutGroup showcaseLayout = new GUILayoutGroup(new RectTransform(Vector2.One, showcaseCenterGroup.RectTransform), isHorizontal: true) { Stretch = true };
474  showCaseTalentParents.Add(showcaseLayout, talents);
475  showCaseTalentFrames.Add(showCaseFrame);
476  }
477 
478  foreach (Identifier talentId in talentOptionIdentifiers)
479  {
480  if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) { continue; }
481 
482  bool isShowCaseTalent = talentOption.ShowCaseTalents.ContainsKey(talentId);
483  GUIComponent talentParent = talentOptionLayoutGroup;
484 
485  foreach (var (key, value) in showCaseTalentParents)
486  {
487  if (value.Contains(talentId))
488  {
489  talentParent = key;
490  break;
491  }
492  }
493 
494  GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentParent.RectTransform), style: null)
495  {
496  CanBeFocused = false
497  };
498 
499  GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null);
500  GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null)
501  {
502  ToolTip = CreateTooltip(talent, characterInfo),
503  UserData = talent.Identifier,
504  PressedColor = pressedColor,
505  Enabled = info.Character != null,
506  OnClicked = (button, userData) =>
507  {
508  if (isShowCaseTalent)
509  {
510  foreach (GUIComponent component in showCaseTalentFrames)
511  {
512  if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == talentId)
513  {
514  component.RectTransform.ScreenSpaceOffset = new Point((int)(button.Rect.Location.X - component.Rect.Width / 2f + button.Rect.Width / 2f), button.Rect.Location.Y - component.Rect.Height);
515  component.Visible = true;
516  }
517  else
518  {
519  component.Visible = false;
520  }
521  }
522 
523  return true;
524  }
525 
526  if (character is null) { return false; }
527 
528  Identifier talentIdentifier = (Identifier)userData;
529  if (talentOption.MaxChosenTalents is 1)
530  {
531  // deselect other buttons in tier by removing their selected talents from pool
532  foreach (Identifier identifier in selectedTalents)
533  {
534  if (character.HasTalent(identifier) || identifier == talentId) { continue; }
535 
536  if (talentOptionIdentifiers.Contains(identifier))
537  {
538  selectedTalents.Remove(identifier);
539  }
540  }
541  }
542 
543  if (character.HasTalent(talentIdentifier))
544  {
545  return true;
546  }
547  else if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents))
548  {
549  if (!selectedTalents.Contains(talentIdentifier))
550  {
551  selectedTalents.Add(talentIdentifier);
552  }
553  else
554  {
555  selectedTalents.Remove(talentIdentifier);
556  }
557  }
558  else
559  {
560  selectedTalents.Remove(talentIdentifier);
561  }
562 
563  UpdateTalentInfo();
564  return true;
565  },
566  };
567 
568  static RichString CreateTooltip(TalentPrefab talent, CharacterInfo? character)
569  {
570  LocalizedString progress = string.Empty;
571 
572  if (character is not null && talent.TrackedStat.TryUnwrap(out var stat))
573  {
574  var statValue = character.GetSavedStatValue(StatTypes.None, stat.PermanentStatIdentifier);
575  var intValue = (int)MathF.Round(statValue);
576  progress = "\n\n";
577  progress += statValue < stat.Max
578  ? TextManager.GetWithVariables("talentprogress", ("[amount]", intValue.ToString()), ("[max]", stat.Max.ToString()))
579  : TextManager.Get("talentprogresscompleted");
580  }
581 
582  RichString tooltip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖\n\n{ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)}{progress}");
583  return tooltip;
584  }
585 
586  talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
587 
588  GUIComponent iconImage;
589  if (talent.Icon is null)
590  {
591  iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null)
592  {
593  OutlineColor = GUIStyle.Red,
594  TextColor = GUIStyle.Red,
595  PressedColor = unselectableColor,
596  DisabledColor = unselectableColor,
597  CanBeFocused = false,
598  };
599  }
600  else
601  {
602  iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true)
603  {
604  Color = talent.ColorOverride.TryUnwrap(out Color color) ? color : Color.White,
605  PressedColor = unselectableColor,
606  DisabledColor = unselectableColor * 0.5f,
607  CanBeFocused = false,
608  };
609  }
610 
611  iconImage.Enabled = talentButton.Enabled;
612  if (isShowCaseTalent)
613  {
614  showCaseTalentButtonsToAdd.Add(talentId, iconImage);
615  continue;
616  }
617 
618  buttonsToAdd.Add(new TalentButton(iconImage, talent));
619  }
620 
621  foreach (TalentButton button in buttonsToAdd)
622  {
623  talentButtons.Add(button);
624  }
625 
626  foreach (var (key, value) in showCaseTalentButtonsToAdd)
627  {
628  HashSet<TalentButton> buttons = new();
629  foreach (Identifier identifier in talentOption.ShowCaseTalents[key])
630  {
631  if (talentButtons.FirstOrNull(talentButton => talentButton.Identifier == identifier) is not { } button) { continue; }
632 
633  buttons.Add(button);
634  }
635 
636  talentShowCaseButtons.Add(new TalentShowCaseButton(buttons.ToImmutableHashSet(), value));
637  }
638 
639  talentCornerIcons.Add(new TalentCornerIcon(subTree.Identifier, index, cornerIcon, talentBackground, talentBackgroundHighlight));
640  }
641 
642  private void CreateFooter(GUIComponent parent, CharacterInfo info)
643  {
644  GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), parent.RectTransform, Anchor.TopCenter), isHorizontal: true)
645  {
646  RelativeSpacing = 0.01f,
647  Stretch = true
648  };
649 
650  GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform));
651  GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null);
652 
653  experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft),
654  barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green)
655  {
656  IsHorizontal = true,
657  };
658 
659  experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
660  {
661  Shadow = true,
662  ToolTip = TextManager.Get("experiencetooltip")
663  };
664 
665  talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight)
666  { AutoScaleVertical = true };
667 
668  talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale")
669  {
670  OnClicked = ResetTalentSelection
671  };
672  talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale")
673  {
674  OnClicked = ApplyTalentSelection,
675  };
676  GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
677  }
678 
679  private bool ResetTalentSelection(GUIButton guiButton, object userData)
680  {
681  if (characterInfo is null) { return false; }
682  selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet();
683  UpdateTalentInfo();
684  return true;
685  }
686 
687  private void ApplyTalents(Character controlledCharacter)
688  {
689  foreach (Identifier talent in CheckTalentSelection(controlledCharacter, selectedTalents))
690  {
691  controlledCharacter.GiveTalent(talent);
692  if (GameMain.Client != null)
693  {
694  GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData());
695  }
696  }
697 
698  UpdateTalentInfo();
699  }
700 
701  private bool ApplyTalentSelection(GUIButton guiButton, object userData)
702  {
703  if (character is null) { return false; }
704 
705  ApplyTalents(character);
706  return true;
707  }
708 
709  public void UpdateTalentInfo()
710  {
711  if (character is null || characterInfo is null) { return; }
712 
713  bool unlockedAllTalents = character.HasUnlockedAllTalents();
714 
715  if (experienceBar is null || experienceText is null) { return; }
716 
717  if (unlockedAllTalents)
718  {
719  experienceText.Text = string.Empty;
720  experienceBar.BarSize = 1f;
721  }
722  else
723  {
724  experienceText.Text = $"{characterInfo.ExperiencePoints - characterInfo.GetExperienceRequiredForCurrentLevel()} / {characterInfo.GetExperienceRequiredToLevelUp() - characterInfo.GetExperienceRequiredForCurrentLevel()}";
725  experienceBar.BarSize = characterInfo.GetProgressTowardsNextLevel();
726  }
727 
728  selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet();
729 
730  string pointsLeft = characterInfo.GetAvailableTalentPoints().ToString();
731 
732  int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
733 
734  if (unlockedAllTalents)
735  {
736  talentPointText?.SetRichText($"‖color:{Color.Gray.ToStringHex()}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖");
737  }
738  else if (talentCount > 0)
739  {
740  string pointsUsed = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{-talentCount}‖color:end‖";
741  LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed));
742  talentPointText?.SetRichText(localizedString);
743  }
744  else
745  {
746  talentPointText?.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft));
747  }
748 
749  foreach (TalentCornerIcon cornerIcon in talentCornerIcons)
750  {
751  TalentStages state = GetTalentOptionStageState(character, cornerIcon.TalentTree, cornerIcon.Index, selectedTalents);
752  TalentTreeStyle style = talentStageStyles[state];
753  GUIComponentStyle newStyle = style.ComponentStyle;
754  cornerIcon.IconComponent.ApplyStyle(newStyle);
755  cornerIcon.IconComponent.Color = newStyle.Color;
756  cornerIcon.BackgroundComponent.Color = style.Color;
757  cornerIcon.GlowComponent.Visible = state == Highlighted;
758  }
759 
760  foreach (TalentButton talentButton in talentButtons)
761  {
762  TalentStages stage = GetTalentState(character, talentButton.Identifier, selectedTalents);
763  ApplyTalentIconColor(stage, talentButton.IconComponent, talentButton.Prefab.ColorOverride);
764  }
765 
766  foreach (TalentShowCaseButton showCaseTalentButton in talentShowCaseButtons)
767  {
768  TalentStages collectiveTalentStage = GetCollectiveTalentState(character, showCaseTalentButton.Buttons, selectedTalents);
769  ApplyTalentIconColor(collectiveTalentStage, showCaseTalentButton.IconComponent, Option<Color>.None());
770  }
771 
772  if (skillListBox is null) { return; }
773 
774  TabMenu.CreateSkillList(character, characterInfo, skillListBox);
775 
776  static TalentStages GetTalentState(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
777  {
778  bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier);
779  TalentStages stage = unselectable ? Locked : Available;
780  if (unselectable)
781  {
782  stage = Locked;
783  }
784 
785  if (character.HasTalent(talentIdentifier))
786  {
787  stage = Unlocked;
788  }
789  else if (selectedTalents.Contains(talentIdentifier))
790  {
791  stage = Highlighted;
792  }
793 
794  return stage;
795  }
796 
797  static void ApplyTalentIconColor(TalentStages stage, GUIComponent component, Option<Color> colorOverride)
798  {
799  Color color = stage switch
800  {
801  Invalid => unselectableColor,
802  Locked => unselectableColor,
803  Unlocked => GetColorOrOverride(GUIStyle.Green, colorOverride),
804  Highlighted => GetColorOrOverride(GUIStyle.Orange, colorOverride),
805  Available => GetColorOrOverride(unselectedColor, colorOverride),
806  _ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null)
807  };
808 
809  component.Color = color;
810  component.HoverColor = Color.Lerp(color, Color.White, 0.7f);
811 
812  static Color GetColorOrOverride(Color color, Option<Color> colorOverride) => colorOverride.TryUnwrap(out Color overrideColor) ? overrideColor : color;
813  }
814 
815  // this could also be reused for setting colors for talentCornerIcons but that's for another time
816  static TalentStages GetCollectiveTalentState(Character character, IReadOnlyCollection<TalentButton> buttons, IReadOnlyCollection<Identifier> selectedTalents)
817  {
818  HashSet<TalentStages> talentStages = new HashSet<TalentStages>();
819  foreach (TalentButton button in buttons)
820  {
821  talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents));
822  }
823 
824  TalentStages collectiveStage = talentStages.All(static stage => stage is Locked)
825  ? Locked
826  : Available;
827 
828  foreach (TalentStages stage in talentStages)
829  {
830  if (stage is Highlighted)
831  {
832  collectiveStage = Highlighted;
833  break;
834  }
835 
836  if (stage is Unlocked)
837  {
838  collectiveStage = Unlocked;
839  break;
840  }
841  }
842 
843  return collectiveStage;
844  }
845  }
846 
847  public void Update()
848  {
849  if (characterInfo is null || talentResetButton is null || talentApplyButton is null) { return; }
850 
851  int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
852  talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0;
853  if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
854  {
855  talentApplyButton.Flash(GUIStyle.Orange);
856  }
857 
858  while (showCaseClosureQueue.TryDequeue(out Identifier identifier))
859  {
860  foreach (GUIComponent component in showCaseTalentFrames)
861  {
862  if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == identifier)
863  {
864  component.Visible = false;
865  }
866  }
867  }
868 
869  bool mouseInteracted = PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.ScrollWheelSpeed != 0;
870  bool keyboardInteracted = PlayerInput.KeyHit(Keys.Escape) || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.InfoTab].IsHit();
871 
872  foreach (GUIComponent component in showCaseTalentFrames)
873  {
874  if (component.UserData is not Identifier identifier) { continue; }
875 
876  component.AddToGUIUpdateList(order: 1);
877  if (!component.Visible) { continue; }
878 
879  if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition)))
880  {
881  showCaseClosureQueue.Enqueue(identifier);
882  }
883  }
884  }
885 
886  private static bool IsOwnCharacter(CharacterInfo? info)
887  {
888  if (info is null) { return false; }
889 
890  CharacterInfo? ownCharacterInfo = Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo;
891  if (ownCharacterInfo is null) { return false; }
892 
893  return info.GetIdentifierUsingOriginalName() == ownCharacterInfo.GetIdentifierUsingOriginalName();
894  }
895 
896  public static bool CanManageTalents(CharacterInfo targetInfo)
897  {
898  // in singleplayer we can do whatever we want
899  if (GameMain.IsSingleplayer) { return true; }
900 
901  // always allow managing talents for own character
902  if (IsOwnCharacter(targetInfo)) { return true; }
903 
904  // don't allow controlling non-bot characters
905  if (targetInfo.Character is not { IsBot: true }) { return false; }
906 
907  // lastly check if we have the permission to do this
908  return GameMain.Client is { } client && client.HasPermission(ClientPermissions.ManageBotTalents);
909  }
910  }
911 }
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
Definition: Enums.cs:180