4 using System.Collections.Generic;
5 using System.Collections.Immutable;
9 using Microsoft.Xna.Framework;
10 using Microsoft.Xna.Framework.Graphics;
11 using Microsoft.Xna.Framework.Input;
17 internal readonly record
struct TalentShowCaseButton(ImmutableHashSet<TalentButton> Buttons,
18 GUIComponent IconComponent);
20 internal readonly record
struct TalentButton(GUIComponent IconComponent,
23 public Identifier Identifier => Prefab.Identifier;
26 internal readonly record
struct TalentCornerIcon(Identifier TalentTree,
28 GUIImage IconComponent,
29 GUIFrame BackgroundComponent,
30 GUIFrame GlowComponent);
32 internal readonly
struct TalentTreeStyle
34 public readonly GUIComponentStyle ComponentStyle;
35 public readonly Color Color;
37 public TalentTreeStyle(
string componentStyle, Color color)
39 ComponentStyle = GUIStyle.GetComponentStyle(componentStyle);
44 internal sealed
class TalentMenu
46 public const string ManageBotTalentsButtonUserData =
"managebottalentsbutton";
49 private CharacterInfo? characterInfo;
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);
58 private static readonly ImmutableDictionary<TalentStages, TalentTreeStyle> talentStageStyles =
59 new Dictionary<TalentStages, TalentTreeStyle>
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();
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>();
74 private readonly Queue<Identifier> showCaseClosureQueue =
new();
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;
84 private GUIButton? talentApplyButton,
87 private delegate
void StartAnimation(RectangleF start, RectangleF end,
float duration);
88 private StartAnimation? startAnimation;
89 private GUIComponent? talentMainArea;
91 public void CreateGUI(GUIFrame parent, Character? targetCharacter)
93 parent.ClearChildren();
94 talentButtons.Clear();
95 talentShowCaseButtons.Clear();
96 talentCornerIcons.Clear();
97 showCaseTalentFrames.Clear();
99 character = targetCharacter;
100 characterInfo = targetCharacter?.Info;
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);
106 GUIFrame content =
new GUIFrame(
new RectTransform(
new Vector2(0.98f), frame.RectTransform,
Anchor.Center), style:
null);
108 GUILayoutGroup contentLayout =
new GUILayoutGroup(
new RectTransform(Vector2.One, content.RectTransform, anchor:
Anchor.Center), childAnchor:
Anchor.TopCenter)
110 AbsoluteSpacing = GUI.IntScale(10),
114 if (characterInfo is
null) {
return; }
116 CreateStatPanel(contentLayout, characterInfo);
118 new GUIFrame(
new RectTransform(
new Vector2(1f, 1f), contentLayout.RectTransform), style:
"HorizontalLine");
120 if (JobTalentTrees.TryGet(characterInfo.Job.Prefab.Identifier, out TalentTree? talentTree))
122 CreateTalentMenu(contentLayout, characterInfo, talentTree!);
125 CreateFooter(contentLayout, characterInfo);
128 if (GameMain.NetworkMember !=
null && IsOwnCharacter(characterInfo))
130 CreateMultiplayerCharacterSettings(frame, content);
134 private void CreateMultiplayerCharacterSettings(GUIComponent parent, GUIComponent content)
136 if (skillLayout is
null) {
return; }
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);
144 if (!GameMain.NetLobbyScreen.PermadeathMode && GameMain.GameSession?.GameMode is not PvPMode)
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")
149 IgnoreLayoutGroups =
false,
152 AutoScaleHorizontal =
true
156 newCharacterBox.OnClicked = (button, o) =>
158 if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
160 GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
162 newCharacterBox.Text = TextManager.Get(
"settings");
163 if (TabMenu.PendingChangesFrame !=
null)
165 NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame);
178 characterSettingsFrame!.Visible =
true;
179 content.Visible =
false;
183 else if (characterInfo !=
null)
185 renameButton =
new GUIButton(
new RectTransform(
new Vector2(0.5f, 0.2f), skillLayout.RectTransform,
Anchor.BottomRight),
186 text: TextManager.Get(
"button.RenameCharacter"), style:
"GUIButtonSmall")
188 Enabled = characterInfo.RenamingEnabled,
189 ToolTip = TextManager.Get(
"permadeath.rename.description"),
190 IgnoreLayoutGroups =
false,
193 AutoScaleHorizontal =
true
195 OnClicked = (_, _) =>
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"))
206 OnClicked = (button, o) =>
208 GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
209 GameMain.NetLobbyScreen.CampaignCharacterDiscarded =
false;
210 characterSettingsFrame.Visible =
false;
211 content.Visible =
true;
217 private void CreateRenamePopup()
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),
"")
224 OnEnterPressed = (textBox, text) =>
226 textBox.Text = text.Trim();
230 renamePopup.Buttons[0].OnClicked += (_, _) =>
232 if (newNameBox.Text?.Trim() is
string newName && newName !=
"")
234 if (characterInfo !=
null)
236 if (newNameBox.Text == characterInfo.Name)
241 if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement)
243 crewManagement.RenameCharacter(characterInfo, newName);
244 if (nameBlock !=
null)
246 nameBlock.Text = newName;
248 if (renameButton !=
null)
250 renameButton.Enabled =
false;
256 DebugConsole.ThrowError(
"Tried to rename character, but CharacterInfo completely missing!");
265 renamePopup.Buttons[1].OnClicked += renamePopup.Close;
268 private void CreateStatPanel(GUIComponent parent, CharacterInfo info)
272 GUILayoutGroup topLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.3f), parent.RectTransform,
Anchor.Center), isHorizontal:
true);
274 new GUICustomComponent(
new RectTransform(
new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) =>
276 info.DrawPortrait(batch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width, false, false);
279 GUILayoutGroup nameLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.3f, 1f), topLayout.RectTransform))
281 AbsoluteSpacing = GUI.IntScale(5),
285 nameBlock =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
287 if (!info.OmitJobInMenus)
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 };
293 if (info.PersonalityTrait !=
null)
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();
301 ImmutableHashSet<TalentPrefab?> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(
static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet();
302 if (talentsOutsideTree.Any())
305 new GUIFrame(
new RectTransform(
new Vector2(1.0f, 0.01f), nameLayout.RectTransform), style:
null);
307 GUILayoutGroup extraTalentLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.55f), nameLayout.RectTransform), childAnchor:
Anchor.TopCenter);
309 talentPointText =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.3f), extraTalentLayout.RectTransform, anchor:
Anchor.Center), TextManager.Get(
"talentmenu.extratalents"), font: GUIStyle.SubHeadingFont)
311 AutoScaleVertical =
true
313 talentPointText.RectTransform.MaxSize =
new Point(
int.MaxValue, (
int)talentPointText.TextSize.Y);
315 var extraTalentList =
new GUIListBox(
new RectTransform(
new Vector2(0.9f, 0.7f), extraTalentLayout.RectTransform, anchor:
Anchor.Center), isHorizontal:
true)
317 AutoHideScrollBar =
false,
318 ResizeContentToMakeSpaceForScrollBar =
false
320 extraTalentList.ScrollBar.RectTransform.SetPosition(
Anchor.BottomCenter,
Pivot.TopCenter);
321 extraTalentLayout.Recalculate();
322 extraTalentList.ForceLayoutRecalculation();
324 foreach (var extraTalent
in talentsOutsideTree)
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)
329 ToolTip = RichString.Rich($
"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" +
"\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)),
330 Color = GUIStyle.Green
335 skillLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor:
Anchor.TopRight)
337 AbsoluteSpacing = GUI.IntScale(5),
341 GUITextBlock skillBlock =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get(
"skills"), font: GUIStyle.SubHeadingFont);
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);
347 private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree)
349 talentMainArea =
new GUIFrame(
new RectTransform(
new Vector2(1f, 0.9f), parent.RectTransform,
Anchor.TopCenter), style:
null );
351 GUIListBox mainList =
new GUIListBox(
new RectTransform(Vector2.One, talentMainArea.RectTransform));
352 startAnimation = CreatePopupAnimationHandler(talentMainArea);
354 if (info is { TalentRefundPoints: > 0, ShowTalentResetPopupOnOpen:
true })
356 CreateTalentResetPopup(talentMainArea);
359 selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet();
361 var specializationCount = tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization);
363 List<GUITextBlock> subTreeNames =
new List<GUITextBlock>();
364 foreach (var subTree
in tree.TalentSubTrees)
366 GUIListBox talentList;
367 GUIComponent talentParent;
369 switch (subTree.Type)
371 case TalentTreeType.Primary:
372 talentList = mainList;
373 treeSize =
new Vector2(1f, 0.5f);
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);
380 throw new ArgumentOutOfRangeException($
"Invalid TalentTreeType \"{subTree.Type}\"");
382 talentParent = talentList.Content;
384 GUILayoutGroup subTreeLayoutGroup =
new GUILayoutGroup(
new RectTransform(treeSize, talentParent.RectTransform), isHorizontal:
false, childAnchor:
Anchor.TopCenter)
389 if (subTree.Type != TalentTreeType.Primary)
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));
400 int optionAmount = subTree.TalentOptionStages.Length;
401 for (
int i = 0; i < optionAmount; i++)
403 TalentOption option = subTree.TalentOptionStages[i];
404 CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info, specializationCount);
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();
411 if (subTree.Type == TalentTreeType.Specialization)
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);
418 var specializationList = GetSpecializationList();
420 specializationList.Content.RectTransform.Resize(
new Point(specializationList.Content.Children.Sum(
static c => c.Rect.Width), specializationList.Rect.Height),
421 resizeChildren: specializationCount < 3);
423 if (specializationCount > 3)
425 specializationList.RectTransform.MinSize =
new Point(specializationList.Rect.Width, specializationList.Content.Rect.Height + (
int)(specializationList.ScrollBar.Rect.Height * 0.9f));
428 GUITextBlock.AutoScaleAndNormalize(subTreeNames);
430 GUIListBox GetSpecializationList()
432 if (mainList.Content.Children.LastOrDefault() is GUIListBox specList)
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;
441 private void CreateTalentResetPopup(GUIComponent parent)
443 bool hasResetTalentsBefore = character?.Info.TalentResetCount > 0;
444 var bgBlocker =
new GUIFrame(
new RectTransform(Vector2.One, parent.RectTransform, anchor:
Anchor.Center), style:
"GUIBackgroundBlocker")
446 IgnoreLayoutGroups =
true
449 var popup =
new GUIFrame(
new RectTransform(
new Vector2(0.6f, 0.8f), bgBlocker.RectTransform,
Anchor.Center));
451 var popupLayout =
new GUILayoutGroup(
new RectTransform(ToolBox.PaddingSizeParentRelative(popup.RectTransform, 0.95f), popup.RectTransform,
Anchor.Center), isHorizontal:
false);
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);
456 if (hasResetTalentsBefore)
458 new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.25f), popupLayout.RectTransform), TextManager.Get(
"talentresetpromptwarning"), wrap:
true)
460 TextColor = GUIStyle.Red
464 var buttonLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.35f), popupLayout.RectTransform), childAnchor:
Anchor.CenterLeft, isHorizontal:
true);
466 var confirmButton =
new GUIButton(
new RectTransform(
new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get(
"holdtoconfirm"))
469 HoldDurationSeconds = 1.5f,
470 OnClicked = (button, o) =>
472 if (character is
null || characterInfo is
null) {
return false; }
474 characterInfo.RefundTalents();
475 selectedTalents.Clear();
477 bgBlocker.Visible =
false;
481 var denyButton =
new GUIButton(
new RectTransform(
new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get(
"decidelater"))
484 OnClicked = (button, userData) =>
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)
492 characterInfo.ShowTalentResetPopupOnOpen =
false;
499 private static StartAnimation CreatePopupAnimationHandler(GUIComponent parent)
501 bool drawAnimation =
false;
506 RectangleF drawRect = RectangleF.Empty,
507 animStartRect = RectangleF.Empty,
508 animEndRect = RectangleF.Empty;
510 void StartAnimation(RectangleF start, RectangleF end,
float duration)
512 animStartRect = start;
517 drawAnimation =
true;
520 void OnDraw(SpriteBatch batch, GUICustomComponent component)
522 if (!drawAnimation) {
return; }
524 GUIComponentStyle style = GUIStyle.GetComponentStyle(
"GUIFrame");
526 style.Sprites[GUIComponent.ComponentState.None][0].Draw(batch, drawRect, Color.White);
529 void OnUpdate(
float f, GUICustomComponent component)
531 if (!drawAnimation) {
return; }
534 if (animTimer > animDur)
536 drawRect = animEndRect;
537 drawAnimation =
false;
541 float lerp = animTimer / animDur;
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));
550 new GUICustomComponent(
new RectTransform(Vector2.One, parent.RectTransform), onDraw: OnDraw, onUpdate: OnUpdate)
552 IgnoreLayoutGroups =
true,
556 return StartAnimation;
559 private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree,
int index, TalentOption talentOption, CharacterInfo info,
int specializationCount)
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);
566 Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize;
568 GUIFrame talentBackground =
new GUIFrame(
new RectTransform(
new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor:
Anchor.Center),
569 style:
"TalentBackground")
571 Color = talentStageStyles[Locked].Color
573 GUIFrame talentBackgroundHighlight =
new GUIFrame(
new RectTransform(Vector2.One, talentBackground.RectTransform, anchor:
Anchor.Center), style:
"TalentBackgroundGlow") { Visible =
false };
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)
577 CanBeFocused =
false,
578 Color = talentStageStyles[Locked].Color
581 Point iconSize = cornerIcon.RectTransform.NonScaledSize;
582 cornerIcon.RectTransform.AbsoluteOffset =
new Point(iconSize.X / 2, iconSize.Y / 2);
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 };
587 HashSet<Identifier> talentOptionIdentifiers = talentOption.TalentIdentifiers.OrderBy(
static t => t).ToHashSet();
588 HashSet<TalentButton> buttonsToAdd =
new();
590 Dictionary<GUILayoutGroup, ImmutableHashSet<Identifier>> showCaseTalentParents =
new();
591 Dictionary<Identifier, GUIComponent> showCaseTalentButtonsToAdd =
new();
593 foreach (var (showCaseTalentIdentifier, talents) in talentOption.ShowCaseTalents)
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")
599 UserData = showCaseTalentIdentifier,
600 IgnoreLayoutGroups =
true,
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);
609 foreach (Identifier talentId
in talentOptionIdentifiers)
611 if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) {
continue; }
613 bool isShowCaseTalent = talentOption.ShowCaseTalents.ContainsKey(talentId);
614 GUIComponent talentParent = talentOptionLayoutGroup;
616 foreach (var (key, value) in showCaseTalentParents)
618 if (value.Contains(talentId))
625 GUIFrame talentFrame =
new GUIFrame(
new RectTransform(Vector2.One, talentParent.RectTransform), style:
null)
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)
633 ToolTip = CreateTooltip(talent, characterInfo),
634 UserData = talent.Identifier,
635 PressedColor = pressedColor,
636 Enabled = info.Character !=
null,
637 OnClicked = (button, userData) =>
639 if (isShowCaseTalent)
641 foreach (GUIComponent component
in showCaseTalentFrames)
643 if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == talentId)
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;
650 component.Visible =
false;
657 if (character is
null) {
return false; }
659 Identifier talentIdentifier = (Identifier)userData;
660 if (talentOption.MaxChosenTalents is 1)
663 foreach (Identifier identifier
in selectedTalents)
665 if (character.HasTalent(identifier) || identifier == talentId) {
continue; }
667 if (talentOptionIdentifiers.Contains(identifier))
669 selectedTalents.Remove(identifier);
674 if (character.HasTalent(talentIdentifier))
678 else if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents))
680 if (!selectedTalents.Contains(talentIdentifier))
682 selectedTalents.Add(talentIdentifier);
686 selectedTalents.Remove(talentIdentifier);
691 selectedTalents.Remove(talentIdentifier);
699 static RichString CreateTooltip(TalentPrefab talent, CharacterInfo? character)
701 LocalizedString progress =
string.Empty;
703 if (character is not
null && talent.TrackedStat.TryUnwrap(out var stat))
705 var statValue = character.GetSavedStatValue(
StatTypes.None, stat.PermanentStatIdentifier);
706 var intValue = (int)MathF.Round(statValue);
708 progress += statValue < stat.Max
709 ? TextManager.GetWithVariables(
"talentprogress", (
"[amount]", intValue.ToString()), (
"[max]", stat.Max.ToString()))
710 : TextManager.Get(
"talentprogresscompleted");
713 RichString tooltip = RichString.Rich($
"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖\n\n{ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)}{progress}");
717 talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
719 GUIComponent iconImage;
720 if (talent.Icon is
null)
722 iconImage =
new GUITextBlock(
new RectTransform(Vector2.One, talentButton.RectTransform, anchor:
Anchor.Center), text:
"???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style:
null)
724 OutlineColor = GUIStyle.Red,
725 TextColor = GUIStyle.Red,
726 PressedColor = unselectableColor,
727 DisabledColor = unselectableColor,
728 CanBeFocused =
false,
733 iconImage =
new GUIImage(
new RectTransform(Vector2.One, talentButton.RectTransform, anchor:
Anchor.Center), sprite: talent.Icon, scaleToFit:
true)
735 Color = talent.ColorOverride.TryUnwrap(out Color color) ? color : Color.White,
736 PressedColor = unselectableColor,
737 DisabledColor = unselectableColor * 0.5f,
738 CanBeFocused =
false,
742 iconImage.Enabled = talentButton.Enabled;
743 if (isShowCaseTalent)
745 showCaseTalentButtonsToAdd.Add(talentId, iconImage);
749 buttonsToAdd.Add(
new TalentButton(iconImage, talent));
752 foreach (TalentButton button
in buttonsToAdd)
754 talentButtons.Add(button);
757 foreach (var (key, value) in showCaseTalentButtonsToAdd)
759 HashSet<TalentButton> buttons =
new();
760 foreach (Identifier identifier
in talentOption.ShowCaseTalents[key])
762 if (talentButtons.FirstOrNull(talentButton => talentButton.Identifier == identifier) is not { } button) {
continue; }
767 talentShowCaseButtons.Add(
new TalentShowCaseButton(buttons.ToImmutableHashSet(), value));
770 talentCornerIcons.Add(
new TalentCornerIcon(subTree.Identifier, index, cornerIcon, talentBackground, talentBackgroundHighlight));
773 private void CreateFooter(GUIComponent parent, CharacterInfo info)
775 GUILayoutGroup bottomLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1f, 0.07f), parent.RectTransform,
Anchor.TopCenter), isHorizontal:
true)
777 RelativeSpacing = 0.01f,
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);
784 experienceBar =
new GUIProgressBar(
new RectTransform(
new Vector2(1f, 1f), experienceBarFrame.RectTransform,
Anchor.CenterLeft),
785 barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green)
790 experienceText =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor:
Anchor.Center),
"", font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
793 ToolTip = TextManager.Get(
"experiencetooltip")
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 };
799 talentResetButton =
new GUIButton(
new RectTransform(
new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get(
"reset"), style:
"GUIButtonFreeScale")
801 OnClicked = ResetTalentSelection
803 talentApplyButton =
new GUIButton(
new RectTransform(
new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get(
"applysettingsbutton"), style:
"GUIButtonFreeScale")
805 OnClicked = ApplyTalentSelection,
807 GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
810 private bool ResetTalentSelection(GUIButton guiButton,
object userData)
812 if (characterInfo is
null) {
return false; }
814 int newTalentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
816 if (characterInfo.TalentRefundPoints > 0 && newTalentCount == 0)
818 CreateTalentResetPopup(talentMainArea!);
822 selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet();
827 private void ApplyTalents(Character controlledCharacter)
829 foreach (Identifier talent
in CheckTalentSelection(controlledCharacter, selectedTalents))
831 controlledCharacter.GiveTalent(talent);
832 if (GameMain.Client !=
null)
834 GameMain.Client.CreateEntityEvent(controlledCharacter,
new Character.UpdateTalentsEventData());
841 private bool ApplyTalentSelection(GUIButton guiButton,
object userData)
843 if (character is
null) {
return false; }
845 ApplyTalents(character);
849 public void UpdateTalentInfo()
851 if (character is
null || characterInfo is
null) {
return; }
853 bool unlockedAllTalents = character.HasUnlockedAllTalents();
855 if (experienceBar is
null || experienceText is
null) {
return; }
857 if (unlockedAllTalents)
859 experienceText.Text =
string.Empty;
860 experienceBar.BarSize = 1f;
864 experienceText.Text = $
"{characterInfo.ExperiencePoints - characterInfo.GetExperienceRequiredForCurrentLevel()} / {characterInfo.GetExperienceRequiredToLevelUp() - characterInfo.GetExperienceRequiredForCurrentLevel()}";
865 experienceBar.BarSize = characterInfo.GetProgressTowardsNextLevel();
868 selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet();
870 string pointsLeft = characterInfo.GetAvailableTalentPoints().ToString();
872 int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
874 if (unlockedAllTalents)
876 talentPointText?.SetRichText($
"‖color:{Color.Gray.ToStringHex()}‖{TextManager.Get("talentmenu.alltalentsunlocked
")}‖color:end‖");
878 else if (talentCount > 0)
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);
886 talentPointText?.SetRichText(TextManager.GetWithVariable(
"talentmenu.points",
"[amount]", pointsLeft));
889 foreach (TalentCornerIcon cornerIcon
in talentCornerIcons)
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;
900 foreach (TalentButton talentButton
in talentButtons)
902 TalentStages stage = GetTalentState(character, talentButton.Identifier, selectedTalents);
903 ApplyTalentIconColor(stage, talentButton.IconComponent, talentButton.Prefab.ColorOverride);
906 foreach (TalentShowCaseButton showCaseTalentButton
in talentShowCaseButtons)
908 TalentStages collectiveTalentStage = GetCollectiveTalentState(character, showCaseTalentButton.Buttons, selectedTalents);
909 ApplyTalentIconColor(collectiveTalentStage, showCaseTalentButton.IconComponent, Option<Color>.None());
912 if (skillListBox is
null) {
return; }
914 TabMenu.CreateSkillList(character, characterInfo, skillListBox);
916 static TalentStages GetTalentState(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
918 bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier);
919 TalentStages stage = unselectable ? Locked : Available;
925 if (character.HasTalent(talentIdentifier))
929 else if (selectedTalents.Contains(talentIdentifier))
937 static void ApplyTalentIconColor(TalentStages stage, GUIComponent component, Option<Color> colorOverride)
939 Color color = stage
switch
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)
949 component.Color = color;
950 component.HoverColor = Color.Lerp(color, Color.White, 0.7f);
952 static Color GetColorOrOverride(Color color, Option<Color> colorOverride) => colorOverride.TryUnwrap(out Color overrideColor) ? overrideColor : color;
956 static TalentStages GetCollectiveTalentState(Character character, IReadOnlyCollection<TalentButton> buttons, IReadOnlyCollection<Identifier> selectedTalents)
958 HashSet<TalentStages> talentStages =
new HashSet<TalentStages>();
959 foreach (TalentButton button
in buttons)
961 talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents));
964 TalentStages collectiveStage = talentStages.All(
static stage => stage is Locked)
968 foreach (TalentStages stage
in talentStages)
970 if (stage is Highlighted)
972 collectiveStage = Highlighted;
976 if (stage is Unlocked)
978 collectiveStage = Unlocked;
983 return collectiveStage;
987 private static readonly LocalizedString refundText = TextManager.Get(
"refund"),
988 resetText = TextManager.Get(
"reset");
992 if (characterInfo is
null || talentResetButton is
null || talentApplyButton is
null) {
return; }
994 int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
995 talentApplyButton.Enabled = talentCount > 0;
996 talentResetButton.Enabled = talentCount > 0 || characterInfo.TalentRefundPoints > 0;
998 if (talentCount == 0 && characterInfo.TalentRefundPoints > 0)
1000 if (talentResetButton.FlashTimer <= 0.0f)
1002 talentResetButton.Flash(GUIStyle.Orange);
1005 talentResetButton.Text = refundText;
1009 talentResetButton.Text = resetText;
1012 if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
1014 talentApplyButton.Flash(GUIStyle.Orange);
1017 while (showCaseClosureQueue.TryDequeue(out Identifier identifier))
1019 foreach (GUIComponent component
in showCaseTalentFrames)
1021 if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == identifier)
1023 component.Visible =
false;
1028 bool mouseInteracted = PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.ScrollWheelSpeed != 0;
1029 bool keyboardInteracted = PlayerInput.KeyHit(Keys.Escape) || GameSettings.CurrentConfig.KeyMap.Bindings[
InputType.InfoTab].IsHit();
1031 foreach (GUIComponent component
in showCaseTalentFrames)
1033 if (component.UserData is not Identifier identifier) {
continue; }
1035 component.AddToGUIUpdateList(order: 1);
1036 if (!component.Visible) {
continue; }
1038 if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition)))
1040 showCaseClosureQueue.Enqueue(identifier);
1045 private static bool IsOwnCharacter(CharacterInfo? info)
1047 if (info is
null) {
return false; }
1049 CharacterInfo? ownCharacterInfo =
Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo;
1050 if (ownCharacterInfo is
null) {
return false; }
1052 return info.GetIdentifierUsingOriginalName() == ownCharacterInfo.GetIdentifierUsingOriginalName();
1055 private static bool IsOnSameTeam(CharacterInfo? info)
1057 if (info is
null) {
return false; }
1060 if (ownCharacterTeam is
null) {
return false; }
1062 return info.TeamID == ownCharacterTeam;
1065 private static bool IsSpectatingInMultiplayer()
1067 if (GameMain.Client?.MyClient is not { } myClient) {
return false; }
1068 return myClient.Spectating;
1071 public static bool CanManageTalents(CharacterInfo targetInfo)
1074 if (GameMain.IsSingleplayer) {
return true; }
1077 if (IsOwnCharacter(targetInfo)) {
return true; }
1080 if (IsSpectatingInMultiplayer()) {
return false; }
1083 if (targetInfo.Character is not { IsBot: true }) {
return false; }
1086 if (!IsOnSameTeam(targetInfo)) {
return false; }
1089 return GameMain.Client is { } client && client.HasPermission(
ClientPermissions.ManageBotTalents);
StatTypes
StatTypes are used to alter several traits of a character. They are mostly used by talents.
@ Character
Characters only