v1.6.17.0 (Unto the Breach update)

This commit is contained in:
Regalis11
2024-10-22 17:29:04 +03:00
parent e74b3cdb17
commit 6e6c17e100
417 changed files with 17166 additions and 5870 deletions
@@ -58,7 +58,7 @@ internal class DeathPrompt
const float FadeInDuration = 1.0f;
bool permadeath = GameMain.NetworkMember is { ServerSettings.RespawnMode: RespawnMode.Permadeath };
bool ironman = GameMain.NetworkMember is { ServerSettings: { RespawnMode: RespawnMode.Permadeath, IronmanMode: true } };
bool ironman = GameMain.NetworkMember is { ServerSettings.IronmanModeActive: true };
var background = new GUICustomComponent(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), onDraw: DrawBackground)
{
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Text;
using Barotrauma.Extensions;
namespace Barotrauma
@@ -84,16 +83,31 @@ namespace Barotrauma
{
currentDirectory += "/";
}
fileSystemWatcher?.Dispose();
fileSystemWatcher = new System.IO.FileSystemWatcher(currentDirectory)
try
{
Filter = "*",
NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName
};
fileSystemWatcher.Created += OnFileSystemChanges;
fileSystemWatcher.Deleted += OnFileSystemChanges;
fileSystemWatcher.Renamed += OnFileSystemChanges;
fileSystemWatcher.EnableRaisingEvents = true;
fileSystemWatcher?.Dispose();
fileSystemWatcher = new System.IO.FileSystemWatcher(currentDirectory)
{
Filter = "*",
NotifyFilter = System.IO.NotifyFilters.LastWrite | System.IO.NotifyFilters.FileName | System.IO.NotifyFilters.DirectoryName
};
fileSystemWatcher.Created += OnFileSystemChanges;
fileSystemWatcher.Deleted += OnFileSystemChanges;
fileSystemWatcher.Renamed += OnFileSystemChanges;
fileSystemWatcher.EnableRaisingEvents = true;
}
catch (System.IO.FileNotFoundException exception)
{
DebugConsole.ThrowError("Failed to set the current directory, possibly due to insufficient access permissions.", exception);
}
catch (ArgumentException exception)
{
DebugConsole.ThrowError("Failed to set the current directory, possibly because it was deleted.", exception);
}
catch (Exception exception)
{
DebugConsole.ThrowError("Failed to set the current directory for an unknown reason.", exception);
}
RefreshFileList();
}
}
@@ -218,6 +232,14 @@ namespace Barotrauma
{
if (Directory.Exists(txt))
{
var attributes = System.IO.File.GetAttributes(txt);
if (attributes.HasAnyFlag(System.IO.FileAttributes.System) || attributes.HasAnyFlag(System.IO.FileAttributes.Hidden))
{
// System and hidden folders should be filtered out when populating the options, but the user can still write or copy-paste the path in the text field,
// which will throw a file not found exception when the file system watcher starts. Therefore, this extra check.
tb.Text = CurrentDirectory;
return false;
}
CurrentDirectory = txt;
return true;
}
@@ -354,20 +376,19 @@ namespace Barotrauma
var directories = Directory.EnumerateDirectories(currentDirectory, "*" + filterBox!.Text + "*");
foreach (var directory in directories)
{
string txt = directory;
if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); }
if (!txt.EndsWith("/")) { txt += "/"; }
//get directory info
DirectoryInfo dirInfo = new DirectoryInfo(directory);
try
{
//this will throw an exception if the directory can't be opened
Directory.GetDirectories(directory);
//this will intentionally throw an exception if the directory can't be opened
System.IO.Directory.GetDirectories(directory);
}
catch (UnauthorizedAccessException)
{
// Skip the folders that can't be accessed.
continue;
}
string txt = directory;
if (txt.StartsWith(currentDirectory)) { txt = txt.Substring(currentDirectory.Length); }
if (!txt.EndsWith("/")) { txt += "/"; }
var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt)
{
UserData = ItemIsDirectory.Yes
@@ -469,7 +469,7 @@ namespace Barotrauma
"Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
soundTextY += yStep;
for (int i = 0; i < SoundManager.SOURCE_COUNT; i++)
for (int i = 0; i < SoundManager.SourceCount; i++)
{
Color clr = Color.White;
string soundStr = i + ": ";
@@ -1546,7 +1546,7 @@ namespace Barotrauma
private static readonly VertexPositionColorTexture[] donutVerts = new VertexPositionColorTexture[DonutSegments * 4];
public static void DrawDonutSection(
SpriteBatch sb, Vector2 center, Range<float> radii, float sectionRad, Color clr, float depth = 0.0f)
SpriteBatch sb, Vector2 center, Range<float> radii, float sectionRad, Color clr, float depth = 0.0f, float rotationRad = 0.0f)
{
float getRadius(int vertexIndex)
=> (vertexIndex % 4) switch
@@ -1589,7 +1589,7 @@ namespace Barotrauma
for (int vertexIndex = 0; vertexIndex < maxDirectionIndex * 4; vertexIndex++)
{
donutVerts[vertexIndex].Color = clr;
donutVerts[vertexIndex].Position = new Vector3(center + getDirection(vertexIndex) * getRadius(vertexIndex), 0.0f);
donutVerts[vertexIndex].Position = new Vector3(center + Vector2.Transform(getDirection(vertexIndex) * getRadius(vertexIndex), Matrix.CreateRotationZ(rotationRad)), 0.0f);
}
sb.Draw(solidWhiteTexture, donutVerts, depth, count: maxDirectionIndex);
}
@@ -1856,9 +1856,16 @@ namespace Barotrauma
Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2();
sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale));
}
#endregion
#region Element creation
public static void DrawCapsule(SpriteBatch sb, Vector2 origin, float length, float radius, float rotation, Color clr, float depth = 0, float thickness = 1)
{
DrawDonutSection(sb, origin + Vector2.Transform(-new Vector2(length / 2, 0), Matrix.CreateRotationZ(rotation)), new Range<float>(radius - thickness / 2, radius + thickness / 2), MathHelper.Pi, clr, depth, rotation - MathHelper.Pi);
DrawRectangle(sb, origin, new Vector2(length, radius * 2), new Vector2(length / 2, radius), rotation, clr, depth, thickness);
DrawDonutSection(sb, origin + Vector2.Transform(new Vector2(length / 2, 0), Matrix.CreateRotationZ(rotation)), new Range<float>(radius - thickness / 2, radius + thickness / 2), MathHelper.Pi, clr, depth, rotation);
}
#endregion
#region Element creation
public static Texture2D CreateCircle(int radius, bool filled = false)
{
@@ -159,6 +159,34 @@ namespace Barotrauma
}
}
private GUIComponent holdOverlay;
private bool requireHold;
public bool RequireHold
{
get => requireHold;
set
{
requireHold = value;
if (value)
{
holdOverlay ??= new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), Frame.RectTransform, Anchor.CenterLeft), style: null)
{
Color = GUIStyle.Yellow * 0.33f,
CanBeFocused = false,
IgnoreLayoutGroups = true,
Visible = true
};
}
else if (holdOverlay != null)
{
holdOverlay.Visible = false;
}
}
}
public float HoldDurationSeconds { get; set; } = 5f;
private float holdTimer;
public bool Pulse { get; set; }
private float pulseTimer;
private float pulseExpand;
@@ -220,7 +248,7 @@ namespace Barotrauma
Rectangle expandRect = Rect;
float expand = (pulseExpand * 20.0f) * GUI.Scale;
expandRect.Inflate(expand, expand);
GUIStyle.EndRoundButtonPulse.Draw(spriteBatch, expandRect, ToolBox.GradientLerp(pulseExpand, Color.White, Color.White, Color.Transparent));
}
}
@@ -240,6 +268,11 @@ namespace Barotrauma
}
if (PlayerInput.PrimaryMouseButtonHeld())
{
if (RequireHold)
{
holdTimer += deltaTime;
}
if (OnPressed != null)
{
if (OnPressed())
@@ -254,25 +287,34 @@ namespace Barotrauma
}
else if (PlayerInput.PrimaryMouseButtonClicked())
{
if (PlaySoundOnSelect)
if (!RequireHold || holdTimer > HoldDurationSeconds)
{
SoundPlayer.PlayUISound(ClickSound);
}
if (OnClicked != null)
{
if (OnClicked(this, UserData))
if (PlaySoundOnSelect)
{
State = ComponentState.Selected;
SoundPlayer.PlayUISound(ClickSound);
}
if (OnClicked != null)
{
if (OnClicked(this, UserData))
{
State = ComponentState.Selected;
}
}
else
{
Selected = !Selected;
}
}
else
{
Selected = !Selected;
}
}
else
{
holdTimer = 0.0f;
}
}
else
{
holdTimer = 0.0f;
if (!ExternalHighlight)
{
State = Selected ? ComponentState.Selected : ComponentState.None;
@@ -283,6 +325,20 @@ namespace Barotrauma
}
}
if (RequireHold)
{
float width = MathHelper.Clamp(holdTimer / HoldDurationSeconds, 0f, 1f);
if (!MathUtils.NearlyEqual(width, holdOverlay.RectTransform.RelativeSize.X))
{
holdOverlay.RectTransform.RelativeSize = new Vector2(width, 1f);
}
holdOverlay.Color =
holdTimer >= HoldDurationSeconds
? Color.Green * 0.33f
: Color.Red * 0.33f;
}
foreach (GUIComponent child in Children)
{
child.State = State;
@@ -9,8 +9,21 @@ namespace Barotrauma
{
public class GUIDropDown : GUIComponent, IKeyboardSubscriber
{
/// <param name="selected">The component that was selected from the dropdown.</param>
/// <param name="obj"><see cref="GUIComponent.UserData"/> of the component selected from the dropdown.</param>
public delegate bool OnSelectedHandler(GUIComponent selected, object obj = null);
/// <summary>
/// Triggers when some item is cliecked from the dropdown.
/// Note that <see cref="SelectedData"/> is not set yet when this callback triggers, and returning false from the callback disallows selecting it.
/// If you want to access the new value, use the obj argument.
/// </summary>
public OnSelectedHandler OnSelected;
/// <summary>
/// Triggers after an item has been selected from the dropdown, all validation has been done and the new value has been set.
/// </summary>
public OnSelectedHandler AfterSelected;
public OnSelectedHandler OnDropped;
private readonly GUIButton button;
@@ -166,7 +179,7 @@ namespace Barotrauma
public Vector4 Padding => button.TextBlock.Padding;
public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft) : base(style, rectT)
public GUIDropDown(RectTransform rectT, LocalizedString text = null, int elementCount = 4, string style = "", bool selectMultiple = false, bool dropAbove = false, Alignment textAlignment = Alignment.CenterLeft, float listBoxScale = 1) : base(style, rectT)
{
text ??= LocalizedString.EmptyString;
@@ -185,13 +198,21 @@ namespace Barotrauma
Anchor listAnchor = dropAbove ? Anchor.TopCenter : Anchor.BottomCenter;
Pivot listPivot = dropAbove ? Pivot.BottomCenter : Pivot.TopCenter;
listBox = new GUIListBox(new RectTransform(new Point(Rect.Width, Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot)
listBox = new GUIListBox(new RectTransform(new Point((int)(Rect.Width * listBoxScale), Rect.Height * MathHelper.Clamp(elementCount, 2, 10)), rectT, listAnchor, listPivot)
{ IsFixedSize = false }, style: null)
{
Enabled = !selectMultiple,
PlaySoundOnSelect = true,
};
if (!selectMultiple) { listBox.OnSelected = SelectItem; }
if (!selectMultiple)
{
listBox.AfterSelected = (component, obj) =>
{
SelectItem(component, obj);
AfterSelected?.Invoke(component, obj);
return true;
};
}
GUIStyle.Apply(listBox, "GUIListBox", this);
GUIStyle.Apply(listBox.ContentBackground, "GUIListBox", this);
@@ -199,6 +220,8 @@ namespace Barotrauma
{
icon = new GUIImage(new RectTransform(new Vector2(0.6f, 0.6f), button.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) }, null, scaleToFit: true);
icon.ApplyStyle(button.Style.ChildStyles["dropdownicon".ToIdentifier()]);
//move the text away from the icon
button.TextBlock.Padding += new Vector4(0, 0, icon.Rect.Width, 0);
}
currentHighestParent = FindHighestParent();
@@ -249,12 +272,12 @@ namespace Barotrauma
toolTip ??= "";
if (selectMultiple)
{
var frame = new GUIFrame(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, style: "ListBoxElement", color: color)
var frame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, style: "ListBoxElement", color: color)
{
UserData = userData,
ToolTip = toolTip
};
new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text)
var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.8f), frame.RectTransform, anchor: Anchor.CenterLeft) { MaxSize = new Point(int.MaxValue, (int)(button.Rect.Height * 0.8f)) }, text)
{
UserData = userData,
ToolTip = toolTip,
@@ -266,6 +289,11 @@ namespace Barotrauma
return false;
}
if (OnSelected != null && !OnSelected.Invoke(tb.Parent, tb.Parent.UserData))
{
return false;
}
List<LocalizedString> texts = new List<LocalizedString>();
selectedDataMultiple.Clear();
selectedIndexMultiple.Clear();
@@ -282,8 +310,7 @@ namespace Barotrauma
i++;
}
button.Text = LocalizedString.Join(", ", texts);
// TODO: The callback is called at least twice, remove this?
OnSelected?.Invoke(tb.Parent, tb.Parent.UserData);
AfterSelected?.Invoke(tb.Parent, SelectedData);
return true;
}
};
@@ -291,7 +318,7 @@ namespace Barotrauma
}
else
{
return new GUITextBlock(new RectTransform(new Point(button.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, text, style: "ListBoxElement", color: color, textColor: textColor)
return new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, button.Rect.Height), listBox.Content.RectTransform) { IsFixedSize = false }, text, style: "ListBoxElement", color: color, textColor: textColor)
{
UserData = userData,
ToolTip = toolTip
@@ -328,9 +355,8 @@ namespace Barotrauma
}
button.Text = textBlock?.Text ?? "";
}
OnSelected?.Invoke(component, obj);
Dropped = false;
// TODO: OnSelected can be called multiple times and when it shouldn't be called -> turn into an event so that nobody else can call it.
OnSelected?.Invoke(component, component.UserData);
return true;
}
@@ -344,6 +370,7 @@ namespace Barotrauma
{
listBox.Select(userData);
}
AfterSelected?.Invoke(SelectedComponent, SelectedData);
}
public void Select(int index)
@@ -360,6 +387,7 @@ namespace Barotrauma
{
listBox.Select(index);
}
AfterSelected?.Invoke(this, SelectedData);
}
private bool wasOpened;
@@ -14,8 +14,17 @@ namespace Barotrauma
protected List<GUIComponent> selected;
public delegate bool OnSelectedHandler(GUIComponent component, object obj);
/// <summary>
/// Triggers when some element is clicked on the listbox.
/// Note that <see cref="SelectedData"/> is not set yet when this callback triggers, and returning false from the callback disallows selecting it.
/// </summary>
public OnSelectedHandler OnSelected;
/// <summary>
/// Triggers after some element has been selected from the listbox.
/// </summary>
public OnSelectedHandler AfterSelected;
public delegate object CheckSelectedHandler();
public CheckSelectedHandler CheckSelected;
@@ -1151,6 +1160,8 @@ namespace Barotrauma
{
SoundPlayer.PlayUISound(GUISoundType.Select);
}
AfterSelected?.Invoke(child, SelectedData);
}
public void Select(IEnumerable<GUIComponent> children)
@@ -1160,8 +1171,9 @@ namespace Barotrauma
selected.Clear();
selected.AddRange(children.Where(c => Content.Children.Contains(c)));
foreach (var child in selected) { OnSelected?.Invoke(child, child.UserData); }
AfterSelected?.Invoke(children.FirstOrDefault(), SelectedData);
}
public void Deselect()
{
Selected = false;
@@ -1172,6 +1184,15 @@ namespace Barotrauma
selected.Clear();
}
public void DeselectElement(GUIComponent child)
{
if (child == null) { return; }
if (selected.Contains(child))
{
selected.Remove(child);
}
}
public void UpdateScrollBarSize()
{
scrollBarNeedsRecalculation = false;
@@ -1268,7 +1289,7 @@ namespace Barotrauma
ContentBackground.DrawManually(spriteBatch, alsoChildren: false);
Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
if (HideChildrenOutsideFrame)
if (HideChildrenOutsideFrame && Content.CountChildren > 0)
{
spriteBatch.End();
spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, Content.Rect);
@@ -1306,7 +1327,7 @@ namespace Barotrauma
GUI.DrawRectangle(spriteBatch, drawRect, Color.White * 0.5f, thickness: 2f);
}
if (HideChildrenOutsideFrame)
if (HideChildrenOutsideFrame && Content.CountChildren > 0)
{
spriteBatch.End();
spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
@@ -51,6 +51,16 @@ namespace Barotrauma
public readonly static GUISprite InteractionLabelBackground = new GUISprite("InteractionLabelBackground");
public readonly static GUISprite BrokenIcon = new GUISprite("BrokenIcon");
public readonly static GUISprite YouAreHereCircle = new GUISprite("YouAreHereCircle");
public readonly static GUISprite SubLocationIcon = new GUISprite("SubLocationIcon");
public readonly static GUISprite ShuttleIcon = new GUISprite("ShuttleIcon");
public readonly static GUISprite WreckIcon = new GUISprite("WreckIcon");
public readonly static GUISprite CaveIcon = new GUISprite("CaveIcon");
public readonly static GUISprite OutpostIcon = new GUISprite("OutpostIcon");
public readonly static GUISprite RuinIcon = new GUISprite("RuinIcon");
public readonly static GUISprite EnemyIcon = new GUISprite("EnemyIcon");
public readonly static GUISprite CorpseIcon = new GUISprite("CorpseIcon");
public readonly static GUISprite BeaconIcon = new GUISprite("BeaconIcon");
public readonly static GUISprite Radiation = new GUISprite("Radiation");
public readonly static GUISpriteSheet RadiationAnimSpriteSheet = new GUISpriteSheet("RadiationAnimSpriteSheet");
@@ -71,7 +81,7 @@ namespace Barotrauma
public readonly static GUISprite EndRoundButtonPulse = new GUISprite("EndRoundButtonPulse");
public readonly static GUISpriteSheet FocusIndicator = new GUISpriteSheet("FocusIndicator");
public readonly static GUISprite IconOverflowIndicator = new GUISprite("IconOverflowIndicator");
/// <summary>
@@ -742,8 +742,8 @@ namespace Barotrauma
private (LocalizedString header, LocalizedString body) GetItemTransferWarningText()
{
var header = TextManager.Get("itemtransferheader").Fallback("lowfuelheader", useDefaultLanguageIfFound: false);
var body = TextManager.Get("itemtransferwarning").Fallback("lowfuelwarning", useDefaultLanguageIfFound: false);
var header = TextManager.Get("itemtransferheader").Fallback(TextManager.Get("lowfuelheader"), useDefaultLanguageIfFound: false);
var body = TextManager.Get("itemtransferwarning").Fallback(TextManager.Get("lowfuelwarning"), useDefaultLanguageIfFound: false);
return (header, body);
}
@@ -416,7 +416,10 @@ namespace Barotrauma
=> TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(value)}");
}
var submarineButton = createTabButton(InfoFrameTab.Submarine, "submarine");
if (Submarine.MainSub != null)
{
createTabButton(InfoFrameTab.Submarine, "submarine");
}
var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character");
talentsButton.OnAddedToGUIUpdateList += (component) =>
@@ -463,12 +466,14 @@ namespace Barotrauma
}
}
private const float jobColumnWidthPercentage = 0.138f,
characterColumnWidthPercentage = 0.45f,
pingColumnWidthPercentage = 0.206f,
walletColumnWidthPercentage = 0.206f;
private const float JobColumnWidthPercentage = 0.138f,
CharacterColumnWidthPercentage = 0.45f,
KillColumnWidthPercentage = 0.1f,
DeathColumnWidthPercentage = 0.1f,
PingColumnWidthPercentage = 0.15f,
WalletColumnWidthPercentage = 0.206f;
private int jobColumnWidth, characterColumnWidth, pingColumnWidth, walletColumnWidth;
private int jobColumnWidth, characterColumnWidth, pingColumnWidth, walletColumnWidth, deathColumnWidth, killColumnWidth;
private void CreateCrewListFrame(GUIFrame crewFrame)
{
@@ -496,11 +501,21 @@ namespace Barotrauma
{
if (teamIDs.Count > 1)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: i == 0 ? GUIStyle.Green : GUIStyle.Orange) { ForceUpperCase = ForceUpperCase.Yes };
var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: CombatMission.GetTeamColor(teamIDs[i]))
{
ForceUpperCase = ForceUpperCase.Yes
};
var teamIcon = new GUIImage(new RectTransform(Vector2.One, nameText.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight),
style: teamIDs[i] == CharacterTeamType.Team2 ? "SeparatistIcon" : "CoalitionIcon")
{
Color = nameText.TextColor
};
nameText.Padding = new Vector4(teamIcon.Rect.Width + nameText.Padding.X, nameText.Padding.Y, nameText.Padding.Z, nameText.Padding.W);
}
headerFrames[i] = new GUILayoutGroup(new RectTransform(Vector2.Zero, content.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(2, -1) }, isHorizontal: true)
{
Stretch = true,
AbsoluteSpacing = 2,
UserData = i
};
@@ -587,8 +602,8 @@ namespace Barotrauma
sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width;
jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f);
characterButton.RectTransform.RelativeSize = new Vector2((1f - jobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f);
jobButton.RectTransform.RelativeSize = new Vector2(JobColumnWidthPercentage * sizeMultiplier, 1f);
characterButton.RectTransform.RelativeSize = new Vector2((1f - JobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f);
jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUIStyle.HotkeyFont;
jobButton.CanBeFocused = characterButton.CanBeFocused = false;
@@ -626,7 +641,8 @@ namespace Barotrauma
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
{
AbsoluteSpacing = 2
AbsoluteSpacing = 2,
Stretch = true
};
new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect))
@@ -639,21 +655,29 @@ namespace Barotrauma
GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor);
paddedFrame.Recalculate();
linkedGUIList.Add(new LinkedGUI(character, frame, textBlock: null));
}
private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame)
{
bool isCampaign = GameMain.GameSession?.Campaign is MultiPlayerCampaign;
GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale");
GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(JobColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(CharacterColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
if (GameMain.GameSession?.GameMode is PvPMode)
{
var killButton = new GUIButton(new RectTransform(new Vector2(KillColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("killcount"), style: "GUIButtonSmallFreeScale");
killColumnWidth = killButton.Rect.Width;
var deathButton = new GUIButton(new RectTransform(new Vector2(DeathColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("deathcount"), style: "GUIButtonSmallFreeScale");
deathColumnWidth = deathButton.Rect.Width;
}
GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(PingColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale");
if (isCampaign)
{
GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform)
{
RelativeSize = new Vector2(walletColumnWidthPercentage * sizeMultiplier, 1f)
}, TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale")
GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(WalletColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale")
{
TextBlock = { Font = GUIStyle.HotkeyFont },
CanBeFocused = false,
@@ -662,15 +686,12 @@ namespace Barotrauma
walletColumnWidth = walletButton.Rect.Width;
}
sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width;
jobButton.RectTransform.RelativeSize = new Vector2(jobColumnWidthPercentage * sizeMultiplier, 1f);
characterButton.RectTransform.RelativeSize = new Vector2((characterColumnWidthPercentage + (isCampaign ? 0 : walletColumnWidthPercentage)) * sizeMultiplier, 1f);
pingButton.RectTransform.RelativeSize = new Vector2(pingColumnWidthPercentage * sizeMultiplier, 1f);
jobButton.TextBlock.Font = characterButton.TextBlock.Font = pingButton.TextBlock.Font = GUIStyle.HotkeyFont;
jobButton.CanBeFocused = characterButton.CanBeFocused = pingButton.CanBeFocused = false;
jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = pingButton.ForceUpperCase = ForceUpperCase.Yes;
foreach (var btn in headerFrame.GetAllChildren<GUIButton>())
{
btn.TextBlock.Font = GUIStyle.HotkeyFont;
btn.CanBeFocused = false;
btn.ForceUpperCase = ForceUpperCase.Yes;
}
jobColumnWidth = jobButton.Rect.Width;
characterColumnWidth = characterButton.Rect.Width;
@@ -692,7 +713,7 @@ namespace Barotrauma
{
foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i]))
{
if (!(character is AICharacter) && connectedClients.Any(c => c.Character == null && c.Name == character.Name)) { continue; }
if (character is not AICharacter && connectedClients.Any(c => c.Character == null && c.Name == character.Name)) { continue; }
CreateMultiPlayerCharacterElement(character, GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.Character == character), i);
}
}
@@ -723,7 +744,8 @@ namespace Barotrauma
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
{
AbsoluteSpacing = 2
AbsoluteSpacing = 2,
Stretch = true
};
new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect))
@@ -736,6 +758,19 @@ namespace Barotrauma
if (client != null)
{
CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon);
if (GameMain.GameSession?.GameMode is PvPMode)
{
new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientKillCount(client) ?? 0).ToString()
};
new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientDeathCount(client) ?? 0).ToString()
};
}
linkedGUIList.Add(new LinkedGUI(client, frame,
new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center),
permissionIcon));
@@ -745,6 +780,18 @@ namespace Barotrauma
GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor);
if (GameMain.GameSession?.GameMode is PvPMode)
{
new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetBotKillCount(character.Info) ?? 0).ToString()
};
new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetBotDeathCount(character.Info) ?? 0).ToString()
};
}
if (character is AICharacter)
{
linkedGUIList.Add(new LinkedGUI(character, frame,
@@ -764,6 +811,8 @@ namespace Barotrauma
}
CreateWalletCrewFrame(character, paddedFrame);
paddedFrame.Recalculate();
}
private void CreateMultiPlayerClientElement(Client client)
@@ -785,7 +834,8 @@ namespace Barotrauma
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
{
AbsoluteSpacing = 2
AbsoluteSpacing = 2,
Stretch = true
};
new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center),
@@ -797,6 +847,19 @@ namespace Barotrauma
};
CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon);
if (GameMain.GameSession?.GameMode is PvPMode)
{
new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientKillCount(client) ?? 0).ToString()
};
new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
{
TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientDeathCount(client) ?? 0).ToString()
};
}
linkedGUIList.Add(new LinkedGUI(client, frame,
new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center),
permissionIcon));
@@ -805,6 +868,8 @@ namespace Barotrauma
{
CreateWalletCrewFrame(character, paddedFrame);
}
paddedFrame.Recalculate();
}
private int GetTeamIndex(Client client)
@@ -842,7 +907,7 @@ namespace Barotrauma
private void CreateWalletCrewFrame(Character character, GUILayoutGroup paddedFrame)
{
if (!(GameMain.GameSession?.Campaign is MultiPlayerCampaign)) { return; }
if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign) { return; }
GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(new Point(walletColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.Center)
{
@@ -947,7 +1012,6 @@ namespace Barotrauma
float iconWidth = iconSize.X / (float)characterColumnWidth;
int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUIStyle.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width);
permissionIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIconSprite) { IgnoreLayoutGroups = true };
if (client.Character != null && client.Character.IsDead)
{
@@ -977,10 +1041,7 @@ namespace Barotrauma
}
else if (client.Character != null && client.Character.IsDead)
{
if (client.Character.Info != null)
{
client.Character.Info.DrawJobIcon(spriteBatch, area);
}
client.Character.Info?.DrawJobIcon(spriteBatch, area);
}
else
{
@@ -1663,6 +1724,8 @@ namespace Barotrauma
private static void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub)
{
if (sub == null) { return; }
GUIFrame subInfoFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
GUIFrame paddedFrame = new GUIFrame(new RectTransform(Vector2.One * 0.97f, subInfoFrame.RectTransform, Anchor.Center), style: null);
@@ -1765,7 +1828,7 @@ namespace Barotrauma
{
parent.Content.ClearChildren();
List<GUITextBlock> skillNames = new List<GUITextBlock>();
foreach (Skill skill in info.Job.GetSkills())
foreach (Skill skill in info.Job.GetSkills().OrderByDescending(static s => s.Level))
{
GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.0f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = true };
var skillName = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value));
@@ -7,6 +7,7 @@ using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using static Barotrauma.TalentTree;
using static Barotrauma.TalentTree.TalentStages;
@@ -83,6 +84,10 @@ namespace Barotrauma
private GUIButton? talentApplyButton,
talentResetButton;
private delegate void StartAnimation(RectangleF start, RectangleF end, float duration);
private StartAnimation? startAnimation;
private GUIComponent? talentMainArea;
public void CreateGUI(GUIFrame parent, Character? targetCharacter)
{
parent.ClearChildren();
@@ -136,7 +141,7 @@ namespace Barotrauma
GUILayoutGroup playerFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), containerFrame.RectTransform, Anchor.TopCenter));
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
if (!GameMain.NetLobbyScreen.PermadeathMode)
if (!GameMain.NetLobbyScreen.PermadeathMode && GameMain.GameSession?.GameMode is not PvPMode)
{
GUIButton newCharacterBox = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), skillLayout.RectTransform, Anchor.BottomRight),
text: GameMain.NetLobbyScreen.CampaignCharacterDiscarded ? TextManager.Get("settings") : TextManager.Get("createnew"), style: "GUIButtonSmall")
@@ -341,7 +346,15 @@ namespace Barotrauma
private void CreateTalentMenu(GUIComponent parent, CharacterInfo info, TalentTree tree)
{
GUIListBox mainList = new GUIListBox(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, anchor: Anchor.TopCenter));
talentMainArea = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), parent.RectTransform, Anchor.TopCenter), style: null );
GUIListBox mainList = new GUIListBox(new RectTransform(Vector2.One, talentMainArea.RectTransform));
startAnimation = CreatePopupAnimationHandler(talentMainArea);
if (info is { TalentRefundPoints: > 0, ShowTalentResetPopupOnOpen: true })
{
CreateTalentResetPopup(talentMainArea);
}
selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet();
@@ -425,6 +438,124 @@ namespace Barotrauma
}
}
private void CreateTalentResetPopup(GUIComponent parent)
{
bool hasResetTalentsBefore = character?.Info.TalentResetCount > 0;
var bgBlocker = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, anchor: Anchor.Center), style: "GUIBackgroundBlocker")
{
IgnoreLayoutGroups = true
};
var popup = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.8f), bgBlocker.RectTransform, Anchor.Center));
var popupLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(popup.RectTransform, 0.95f), popup.RectTransform, Anchor.Center), isHorizontal: false);
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), popupLayout.RectTransform), TextManager.Get("talentresetheader"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center);
new GUITextBlock(new RectTransform(new Vector2(1.0f, hasResetTalentsBefore ? 0.25f : 0.5f), popupLayout.RectTransform), TextManager.Get("talentresetprompt"), wrap: true);
if (hasResetTalentsBefore)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.25f), popupLayout.RectTransform), TextManager.Get("talentresetpromptwarning"), wrap: true)
{
TextColor = GUIStyle.Red
};
}
var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.35f), popupLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true);
var confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get("holdtoconfirm"))
{
RequireHold = true,
HoldDurationSeconds = 1.5f,
OnClicked = (button, o) =>
{
if (character is null || characterInfo is null) { return false; }
characterInfo.RefundTalents();
selectedTalents.Clear();
UpdateTalentInfo();
bgBlocker.Visible = false;
return true;
}
};
var denyButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get("decidelater"))
{
RequireHold = false,
OnClicked = (button, userData) =>
{
if (talentResetButton is not { } resetButton) { return false; }
startAnimation?.Invoke(popup.Rect, resetButton.Rect, 0.25f);
resetButton.Flash(GUIStyle.Green);
bgBlocker.Visible = false;
if (characterInfo != null)
{
characterInfo.ShowTalentResetPopupOnOpen = false;
}
return true;
}
};
}
private static StartAnimation CreatePopupAnimationHandler(GUIComponent parent)
{
bool drawAnimation = false;
float animDur = 1f,
animTimer = 0f;
RectangleF drawRect = RectangleF.Empty,
animStartRect = RectangleF.Empty,
animEndRect = RectangleF.Empty;
void StartAnimation(RectangleF start, RectangleF end, float duration)
{
animStartRect = start;
animEndRect = end;
animTimer = 0;
animDur = duration;
drawRect = start;
drawAnimation = true;
}
void OnDraw(SpriteBatch batch, GUICustomComponent component)
{
if (!drawAnimation) { return; }
GUIComponentStyle style = GUIStyle.GetComponentStyle("GUIFrame");
style.Sprites[GUIComponent.ComponentState.None][0].Draw(batch, drawRect, Color.White);
}
void OnUpdate(float f, GUICustomComponent component)
{
if (!drawAnimation) { return; }
animTimer += f;
if (animTimer > animDur)
{
drawRect = animEndRect;
drawAnimation = false;
return;
}
float lerp = animTimer / animDur;
drawRect = new RectangleF(
MathHelper.Lerp(animStartRect.X, animEndRect.X, lerp),
MathHelper.Lerp(animStartRect.Y, animEndRect.Y, lerp),
MathHelper.Lerp(animStartRect.Width, animEndRect.Width, lerp),
MathHelper.Lerp(animStartRect.Height, animEndRect.Height, lerp));
}
new GUICustomComponent(new RectTransform(Vector2.One, parent.RectTransform), onDraw: OnDraw, onUpdate: OnUpdate)
{
IgnoreLayoutGroups = true,
CanBeFocused = false
};
return StartAnimation;
}
private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info, int specializationCount)
{
int elementPadding = GUI.IntScale(8);
@@ -679,6 +810,15 @@ namespace Barotrauma
private bool ResetTalentSelection(GUIButton guiButton, object userData)
{
if (characterInfo is null) { return false; }
int newTalentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
// if we don't have talents selected, and we have points to refund, show the refund popup
if (characterInfo.TalentRefundPoints > 0 && newTalentCount == 0)
{
CreateTalentResetPopup(talentMainArea!);
return true;
}
selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet();
UpdateTalentInfo();
return true;
@@ -844,12 +984,31 @@ namespace Barotrauma
}
}
private static readonly LocalizedString refundText = TextManager.Get("refund"),
resetText = TextManager.Get("reset");
public void Update()
{
if (characterInfo is null || talentResetButton is null || talentApplyButton is null) { return; }
int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0;
talentApplyButton.Enabled = talentCount > 0;
talentResetButton.Enabled = talentCount > 0 || characterInfo.TalentRefundPoints > 0;
if (talentCount == 0 && characterInfo.TalentRefundPoints > 0)
{
if (talentResetButton.FlashTimer <= 0.0f)
{
talentResetButton.Flash(GUIStyle.Orange);
}
talentResetButton.Text = refundText;
}
else
{
talentResetButton.Text = resetText;
}
if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
{
talentApplyButton.Flash(GUIStyle.Orange);
@@ -893,6 +1052,22 @@ namespace Barotrauma
return info.GetIdentifierUsingOriginalName() == ownCharacterInfo.GetIdentifierUsingOriginalName();
}
private static bool IsOnSameTeam(CharacterInfo? info)
{
if (info is null) { return false; }
CharacterTeamType? ownCharacterTeam = Character.Controlled?.TeamID ?? GameMain.Client?.MyClient?.TeamID;
if (ownCharacterTeam is null) { return false; }
return info.TeamID == ownCharacterTeam;
}
private static bool IsSpectatingInMultiplayer()
{
if (GameMain.Client?.MyClient is not { } myClient) { return false; }
return myClient.Spectating;
}
public static bool CanManageTalents(CharacterInfo targetInfo)
{
// in singleplayer we can do whatever we want
@@ -901,10 +1076,16 @@ namespace Barotrauma
// always allow managing talents for own character
if (IsOwnCharacter(targetInfo)) { return true; }
// disallow managing talents while spectating
if (IsSpectatingInMultiplayer()) { return false; }
// don't allow controlling non-bot characters
if (targetInfo.Character is not { IsBot: true }) { return false; }
// lastly check if we have the permission to do this
// only allow managing talents for bots on the same team
if (!IsOnSameTeam(targetInfo)) { return false; }
// lastly, check if we have the permission to do this
return GameMain.Client is { } client && client.HasPermission(ClientPermissions.ManageBotTalents);
}
}