4 using System.Collections.Generic;
5 using System.Collections.Immutable;
9 using Microsoft.Xna.Framework;
10 using Microsoft.Xna.Framework.Input;
16 internal readonly record
struct TalentShowCaseButton(ImmutableHashSet<TalentButton> Buttons,
17 GUIComponent IconComponent);
19 internal readonly record
struct TalentButton(GUIComponent IconComponent,
22 public Identifier Identifier => Prefab.Identifier;
25 internal readonly record
struct TalentCornerIcon(Identifier TalentTree,
27 GUIImage IconComponent,
28 GUIFrame BackgroundComponent,
29 GUIFrame GlowComponent);
31 internal readonly
struct TalentTreeStyle
33 public readonly GUIComponentStyle ComponentStyle;
34 public readonly Color Color;
36 public TalentTreeStyle(
string componentStyle, Color color)
38 ComponentStyle = GUIStyle.GetComponentStyle(componentStyle);
43 internal sealed
class TalentMenu
45 public const string ManageBotTalentsButtonUserData =
"managebottalentsbutton";
48 private CharacterInfo? characterInfo;
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);
57 private static readonly ImmutableDictionary<TalentStages, TalentTreeStyle> talentStageStyles =
58 new Dictionary<TalentStages, TalentTreeStyle>
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();
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>();
73 private readonly Queue<Identifier> showCaseClosureQueue =
new();
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;
83 private GUIButton? talentApplyButton,
86 public void CreateGUI(GUIFrame parent, Character? targetCharacter)
88 parent.ClearChildren();
89 talentButtons.Clear();
90 talentShowCaseButtons.Clear();
91 talentCornerIcons.Clear();
92 showCaseTalentFrames.Clear();
94 character = targetCharacter;
95 characterInfo = targetCharacter?.Info;
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);
101 GUIFrame content =
new GUIFrame(
new RectTransform(
new Vector2(0.98f), frame.RectTransform,
Anchor.Center), style:
null);
103 GUILayoutGroup contentLayout =
new GUILayoutGroup(
new RectTransform(Vector2.One, content.RectTransform, anchor:
Anchor.Center), childAnchor:
Anchor.TopCenter)
105 AbsoluteSpacing = GUI.IntScale(10),
109 if (characterInfo is
null) {
return; }
111 CreateStatPanel(contentLayout, characterInfo);
113 new GUIFrame(
new RectTransform(
new Vector2(1f, 1f), contentLayout.RectTransform), style:
"HorizontalLine");
115 if (JobTalentTrees.TryGet(characterInfo.Job.Prefab.Identifier, out TalentTree? talentTree))
117 CreateTalentMenu(contentLayout, characterInfo, talentTree!);
120 CreateFooter(contentLayout, characterInfo);
123 if (GameMain.NetworkMember !=
null && IsOwnCharacter(characterInfo))
125 CreateMultiplayerCharacterSettings(frame, content);
129 private void CreateMultiplayerCharacterSettings(GUIComponent parent, GUIComponent content)
131 if (skillLayout is
null) {
return; }
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);
139 if (!GameMain.NetLobbyScreen.PermadeathMode)
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")
144 IgnoreLayoutGroups =
false,
147 AutoScaleHorizontal =
true
151 newCharacterBox.OnClicked = (button, o) =>
153 if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
155 GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
157 newCharacterBox.Text = TextManager.Get(
"settings");
158 if (TabMenu.PendingChangesFrame !=
null)
160 NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame);
173 characterSettingsFrame!.Visible =
true;
174 content.Visible =
false;
178 else if (characterInfo !=
null)
180 renameButton =
new GUIButton(
new RectTransform(
new Vector2(0.5f, 0.2f), skillLayout.RectTransform,
Anchor.BottomRight),
181 text: TextManager.Get(
"button.RenameCharacter"), style:
"GUIButtonSmall")
183 Enabled = characterInfo.RenamingEnabled,
184 ToolTip = TextManager.Get(
"permadeath.rename.description"),
185 IgnoreLayoutGroups =
false,
188 AutoScaleHorizontal =
true
190 OnClicked = (_, _) =>
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"))
201 OnClicked = (button, o) =>
203 GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
204 GameMain.NetLobbyScreen.CampaignCharacterDiscarded =
false;
205 characterSettingsFrame.Visible =
false;
206 content.Visible =
true;
212 private void CreateRenamePopup()
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),
"")
219 OnEnterPressed = (textBox, text) =>
221 textBox.Text = text.Trim();
225 renamePopup.Buttons[0].OnClicked += (_, _) =>
227 if (newNameBox.Text?.Trim() is
string newName && newName !=
"")
229 if (characterInfo !=
null)
231 if (newNameBox.Text == characterInfo.Name)
236 if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement)
238 crewManagement.RenameCharacter(characterInfo, newName);
239 if (nameBlock !=
null)
241 nameBlock.Text = newName;
243 if (renameButton !=
null)
245 renameButton.Enabled =
false;
251 DebugConsole.ThrowError(
"Tried to rename character, but CharacterInfo completely missing!");
260 renamePopup.Buttons[1].OnClicked += renamePopup.Close;
263 private void CreateStatPanel(GUIComponent parent, CharacterInfo info)
267 GUILayoutGroup topLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.3f), parent.RectTransform,
Anchor.Center), isHorizontal:
true);
269 new GUICustomComponent(
new RectTransform(
new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) =>
271 info.DrawPortrait(batch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width, false, false);
274 GUILayoutGroup nameLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.3f, 1f), topLayout.RectTransform))
276 AbsoluteSpacing = GUI.IntScale(5),
280 nameBlock =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
282 if (!info.OmitJobInMenus)
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 };
288 if (info.PersonalityTrait !=
null)
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();
296 ImmutableHashSet<TalentPrefab?> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(
static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet();
297 if (talentsOutsideTree.Any())
300 new GUIFrame(
new RectTransform(
new Vector2(1.0f, 0.01f), nameLayout.RectTransform), style:
null);
302 GUILayoutGroup extraTalentLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1.0f, 0.55f), nameLayout.RectTransform), childAnchor:
Anchor.TopCenter);
304 talentPointText =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.3f), extraTalentLayout.RectTransform, anchor:
Anchor.Center), TextManager.Get(
"talentmenu.extratalents"), font: GUIStyle.SubHeadingFont)
306 AutoScaleVertical =
true
308 talentPointText.RectTransform.MaxSize =
new Point(
int.MaxValue, (
int)talentPointText.TextSize.Y);
310 var extraTalentList =
new GUIListBox(
new RectTransform(
new Vector2(0.9f, 0.7f), extraTalentLayout.RectTransform, anchor:
Anchor.Center), isHorizontal:
true)
312 AutoHideScrollBar =
false,
313 ResizeContentToMakeSpaceForScrollBar =
false
315 extraTalentList.ScrollBar.RectTransform.SetPosition(
Anchor.BottomCenter,
Pivot.TopCenter);
316 extraTalentLayout.Recalculate();
317 extraTalentList.ForceLayoutRecalculation();
319 foreach (var extraTalent
in talentsOutsideTree)
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)
324 ToolTip = RichString.Rich($
"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" +
"\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)),
325 Color = GUIStyle.Green
330 skillLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor:
Anchor.TopRight)
332 AbsoluteSpacing = GUI.IntScale(5),
336 GUITextBlock skillBlock =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get(
"skills"), font: GUIStyle.SubHeadingFont);
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);
342 private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree)
344 GUIListBox mainList =
new GUIListBox(
new RectTransform(
new Vector2(1f, 0.9f), parent.RectTransform, anchor:
Anchor.TopCenter));
346 selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet();
348 var specializationCount = tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization);
350 List<GUITextBlock> subTreeNames =
new List<GUITextBlock>();
351 foreach (var subTree
in tree.TalentSubTrees)
353 GUIListBox talentList;
354 GUIComponent talentParent;
356 switch (subTree.Type)
358 case TalentTreeType.Primary:
359 talentList = mainList;
360 treeSize =
new Vector2(1f, 0.5f);
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);
367 throw new ArgumentOutOfRangeException($
"Invalid TalentTreeType \"{subTree.Type}\"");
369 talentParent = talentList.Content;
371 GUILayoutGroup subTreeLayoutGroup =
new GUILayoutGroup(
new RectTransform(treeSize, talentParent.RectTransform), isHorizontal:
false, childAnchor:
Anchor.TopCenter)
376 if (subTree.Type != TalentTreeType.Primary)
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));
387 int optionAmount = subTree.TalentOptionStages.Length;
388 for (
int i = 0; i < optionAmount; i++)
390 TalentOption option = subTree.TalentOptionStages[i];
391 CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info, specializationCount);
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();
398 if (subTree.Type == TalentTreeType.Specialization)
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);
405 var specializationList = GetSpecializationList();
407 specializationList.Content.RectTransform.Resize(
new Point(specializationList.Content.Children.Sum(
static c => c.Rect.Width), specializationList.Rect.Height),
408 resizeChildren: specializationCount < 3);
410 if (specializationCount > 3)
412 specializationList.RectTransform.MinSize =
new Point(specializationList.Rect.Width, specializationList.Content.Rect.Height + (
int)(specializationList.ScrollBar.Rect.Height * 0.9f));
415 GUITextBlock.AutoScaleAndNormalize(subTreeNames);
417 GUIListBox GetSpecializationList()
419 if (mainList.Content.Children.LastOrDefault() is GUIListBox specList)
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;
428 private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree,
int index, TalentOption talentOption, CharacterInfo info,
int specializationCount)
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);
435 Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize;
437 GUIFrame talentBackground =
new GUIFrame(
new RectTransform(
new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor:
Anchor.Center),
438 style:
"TalentBackground")
440 Color = talentStageStyles[Locked].Color
442 GUIFrame talentBackgroundHighlight =
new GUIFrame(
new RectTransform(Vector2.One, talentBackground.RectTransform, anchor:
Anchor.Center), style:
"TalentBackgroundGlow") { Visible =
false };
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)
446 CanBeFocused =
false,
447 Color = talentStageStyles[Locked].Color
450 Point iconSize = cornerIcon.RectTransform.NonScaledSize;
451 cornerIcon.RectTransform.AbsoluteOffset =
new Point(iconSize.X / 2, iconSize.Y / 2);
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 };
456 HashSet<Identifier> talentOptionIdentifiers = talentOption.TalentIdentifiers.OrderBy(
static t => t).ToHashSet();
457 HashSet<TalentButton> buttonsToAdd =
new();
459 Dictionary<GUILayoutGroup, ImmutableHashSet<Identifier>> showCaseTalentParents =
new();
460 Dictionary<Identifier, GUIComponent> showCaseTalentButtonsToAdd =
new();
462 foreach (var (showCaseTalentIdentifier, talents) in talentOption.ShowCaseTalents)
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")
468 UserData = showCaseTalentIdentifier,
469 IgnoreLayoutGroups =
true,
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);
478 foreach (Identifier talentId
in talentOptionIdentifiers)
480 if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) {
continue; }
482 bool isShowCaseTalent = talentOption.ShowCaseTalents.ContainsKey(talentId);
483 GUIComponent talentParent = talentOptionLayoutGroup;
485 foreach (var (key, value) in showCaseTalentParents)
487 if (value.Contains(talentId))
494 GUIFrame talentFrame =
new GUIFrame(
new RectTransform(Vector2.One, talentParent.RectTransform), style:
null)
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)
502 ToolTip = CreateTooltip(talent, characterInfo),
503 UserData = talent.Identifier,
504 PressedColor = pressedColor,
505 Enabled = info.Character !=
null,
506 OnClicked = (button, userData) =>
508 if (isShowCaseTalent)
510 foreach (GUIComponent component
in showCaseTalentFrames)
512 if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == talentId)
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;
519 component.Visible =
false;
526 if (character is
null) {
return false; }
528 Identifier talentIdentifier = (Identifier)userData;
529 if (talentOption.MaxChosenTalents is 1)
532 foreach (Identifier identifier
in selectedTalents)
534 if (character.HasTalent(identifier) || identifier == talentId) {
continue; }
536 if (talentOptionIdentifiers.Contains(identifier))
538 selectedTalents.Remove(identifier);
543 if (character.HasTalent(talentIdentifier))
547 else if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents))
549 if (!selectedTalents.Contains(talentIdentifier))
551 selectedTalents.Add(talentIdentifier);
555 selectedTalents.Remove(talentIdentifier);
560 selectedTalents.Remove(talentIdentifier);
568 static RichString CreateTooltip(TalentPrefab talent, CharacterInfo? character)
570 LocalizedString progress =
string.Empty;
572 if (character is not
null && talent.TrackedStat.TryUnwrap(out var stat))
574 var statValue = character.GetSavedStatValue(
StatTypes.None, stat.PermanentStatIdentifier);
575 var intValue = (int)MathF.Round(statValue);
577 progress += statValue < stat.Max
578 ? TextManager.GetWithVariables(
"talentprogress", (
"[amount]", intValue.ToString()), (
"[max]", stat.Max.ToString()))
579 : TextManager.Get(
"talentprogresscompleted");
582 RichString tooltip = RichString.Rich($
"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖\n\n{ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)}{progress}");
586 talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
588 GUIComponent iconImage;
589 if (talent.Icon is
null)
591 iconImage =
new GUITextBlock(
new RectTransform(Vector2.One, talentButton.RectTransform, anchor:
Anchor.Center), text:
"???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style:
null)
593 OutlineColor = GUIStyle.Red,
594 TextColor = GUIStyle.Red,
595 PressedColor = unselectableColor,
596 DisabledColor = unselectableColor,
597 CanBeFocused =
false,
602 iconImage =
new GUIImage(
new RectTransform(Vector2.One, talentButton.RectTransform, anchor:
Anchor.Center), sprite: talent.Icon, scaleToFit:
true)
604 Color = talent.ColorOverride.TryUnwrap(out Color color) ? color : Color.White,
605 PressedColor = unselectableColor,
606 DisabledColor = unselectableColor * 0.5f,
607 CanBeFocused =
false,
611 iconImage.Enabled = talentButton.Enabled;
612 if (isShowCaseTalent)
614 showCaseTalentButtonsToAdd.Add(talentId, iconImage);
618 buttonsToAdd.Add(
new TalentButton(iconImage, talent));
621 foreach (TalentButton button
in buttonsToAdd)
623 talentButtons.Add(button);
626 foreach (var (key, value) in showCaseTalentButtonsToAdd)
628 HashSet<TalentButton> buttons =
new();
629 foreach (Identifier identifier
in talentOption.ShowCaseTalents[key])
631 if (talentButtons.FirstOrNull(talentButton => talentButton.Identifier == identifier) is not { } button) {
continue; }
636 talentShowCaseButtons.Add(
new TalentShowCaseButton(buttons.ToImmutableHashSet(), value));
639 talentCornerIcons.Add(
new TalentCornerIcon(subTree.Identifier, index, cornerIcon, talentBackground, talentBackgroundHighlight));
642 private void CreateFooter(GUIComponent parent, CharacterInfo info)
644 GUILayoutGroup bottomLayout =
new GUILayoutGroup(
new RectTransform(
new Vector2(1f, 0.07f), parent.RectTransform,
Anchor.TopCenter), isHorizontal:
true)
646 RelativeSpacing = 0.01f,
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);
653 experienceBar =
new GUIProgressBar(
new RectTransform(
new Vector2(1f, 1f), experienceBarFrame.RectTransform,
Anchor.CenterLeft),
654 barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green)
659 experienceText =
new GUITextBlock(
new RectTransform(
new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor:
Anchor.Center),
"", font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
662 ToolTip = TextManager.Get(
"experiencetooltip")
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 };
668 talentResetButton =
new GUIButton(
new RectTransform(
new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get(
"reset"), style:
"GUIButtonFreeScale")
670 OnClicked = ResetTalentSelection
672 talentApplyButton =
new GUIButton(
new RectTransform(
new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get(
"applysettingsbutton"), style:
"GUIButtonFreeScale")
674 OnClicked = ApplyTalentSelection,
676 GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
679 private bool ResetTalentSelection(GUIButton guiButton,
object userData)
681 if (characterInfo is
null) {
return false; }
682 selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet();
687 private void ApplyTalents(Character controlledCharacter)
689 foreach (Identifier talent
in CheckTalentSelection(controlledCharacter, selectedTalents))
691 controlledCharacter.GiveTalent(talent);
692 if (GameMain.Client !=
null)
694 GameMain.Client.CreateEntityEvent(controlledCharacter,
new Character.UpdateTalentsEventData());
701 private bool ApplyTalentSelection(GUIButton guiButton,
object userData)
703 if (character is
null) {
return false; }
705 ApplyTalents(character);
709 public void UpdateTalentInfo()
711 if (character is
null || characterInfo is
null) {
return; }
713 bool unlockedAllTalents = character.HasUnlockedAllTalents();
715 if (experienceBar is
null || experienceText is
null) {
return; }
717 if (unlockedAllTalents)
719 experienceText.Text =
string.Empty;
720 experienceBar.BarSize = 1f;
724 experienceText.Text = $
"{characterInfo.ExperiencePoints - characterInfo.GetExperienceRequiredForCurrentLevel()} / {characterInfo.GetExperienceRequiredToLevelUp() - characterInfo.GetExperienceRequiredForCurrentLevel()}";
725 experienceBar.BarSize = characterInfo.GetProgressTowardsNextLevel();
728 selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet();
730 string pointsLeft = characterInfo.GetAvailableTalentPoints().ToString();
732 int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
734 if (unlockedAllTalents)
736 talentPointText?.SetRichText($
"‖color:{Color.Gray.ToStringHex()}‖{TextManager.Get("talentmenu.alltalentsunlocked
")}‖color:end‖");
738 else if (talentCount > 0)
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);
746 talentPointText?.SetRichText(TextManager.GetWithVariable(
"talentmenu.points",
"[amount]", pointsLeft));
749 foreach (TalentCornerIcon cornerIcon
in talentCornerIcons)
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;
760 foreach (TalentButton talentButton
in talentButtons)
762 TalentStages stage = GetTalentState(character, talentButton.Identifier, selectedTalents);
763 ApplyTalentIconColor(stage, talentButton.IconComponent, talentButton.Prefab.ColorOverride);
766 foreach (TalentShowCaseButton showCaseTalentButton
in talentShowCaseButtons)
768 TalentStages collectiveTalentStage = GetCollectiveTalentState(character, showCaseTalentButton.Buttons, selectedTalents);
769 ApplyTalentIconColor(collectiveTalentStage, showCaseTalentButton.IconComponent, Option<Color>.None());
772 if (skillListBox is
null) {
return; }
774 TabMenu.CreateSkillList(character, characterInfo, skillListBox);
776 static TalentStages GetTalentState(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
778 bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier);
779 TalentStages stage = unselectable ? Locked : Available;
785 if (character.HasTalent(talentIdentifier))
789 else if (selectedTalents.Contains(talentIdentifier))
797 static void ApplyTalentIconColor(TalentStages stage, GUIComponent component, Option<Color> colorOverride)
799 Color color = stage
switch
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)
809 component.Color = color;
810 component.HoverColor = Color.Lerp(color, Color.White, 0.7f);
812 static Color GetColorOrOverride(Color color, Option<Color> colorOverride) => colorOverride.TryUnwrap(out Color overrideColor) ? overrideColor : color;
816 static TalentStages GetCollectiveTalentState(Character character, IReadOnlyCollection<TalentButton> buttons, IReadOnlyCollection<Identifier> selectedTalents)
818 HashSet<TalentStages> talentStages =
new HashSet<TalentStages>();
819 foreach (TalentButton button
in buttons)
821 talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents));
824 TalentStages collectiveStage = talentStages.All(
static stage => stage is Locked)
828 foreach (TalentStages stage
in talentStages)
830 if (stage is Highlighted)
832 collectiveStage = Highlighted;
836 if (stage is Unlocked)
838 collectiveStage = Unlocked;
843 return collectiveStage;
849 if (characterInfo is
null || talentResetButton is
null || talentApplyButton is
null) {
return; }
851 int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
852 talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0;
853 if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
855 talentApplyButton.Flash(GUIStyle.Orange);
858 while (showCaseClosureQueue.TryDequeue(out Identifier identifier))
860 foreach (GUIComponent component
in showCaseTalentFrames)
862 if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == identifier)
864 component.Visible =
false;
869 bool mouseInteracted = PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.ScrollWheelSpeed != 0;
870 bool keyboardInteracted = PlayerInput.KeyHit(Keys.Escape) || GameSettings.CurrentConfig.KeyMap.Bindings[
InputType.InfoTab].IsHit();
872 foreach (GUIComponent component
in showCaseTalentFrames)
874 if (component.UserData is not Identifier identifier) {
continue; }
876 component.AddToGUIUpdateList(order: 1);
877 if (!component.Visible) {
continue; }
879 if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition)))
881 showCaseClosureQueue.Enqueue(identifier);
886 private static bool IsOwnCharacter(CharacterInfo? info)
888 if (info is
null) {
return false; }
890 CharacterInfo? ownCharacterInfo =
Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo;
891 if (ownCharacterInfo is
null) {
return false; }
893 return info.GetIdentifierUsingOriginalName() == ownCharacterInfo.GetIdentifierUsingOriginalName();
896 public static bool CanManageTalents(CharacterInfo targetInfo)
899 if (GameMain.IsSingleplayer) {
return true; }
902 if (IsOwnCharacter(targetInfo)) {
return true; }
905 if (targetInfo.Character is not { IsBot: true }) {
return false; }
908 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.