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