Merge remote-tracking branch 'upstream/dev' into develop

This commit is contained in:
EvilFactory
2022-12-09 17:33:44 -03:00
416 changed files with 12674 additions and 5862 deletions
@@ -775,7 +775,7 @@ namespace Barotrauma
if (Character.Controlled != null && ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent radio))
{
radio.Channel = channel;
GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()]));
GameMain.Client?.CreateEntityEvent(radio.Item, new Item.ChangePropertyEventData(radio.SerializableProperties["channel".ToIdentifier()], radio));
if (setText)
{
@@ -357,6 +357,17 @@ namespace Barotrauma
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);
}
catch (UnauthorizedAccessException)
{
continue;
}
var itemFrame = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), fileList.Content.RectTransform), txt)
{
UserData = ItemIsDirectory.Yes
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using Barotrauma.IO;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.CharacterEditor;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
@@ -50,6 +49,14 @@ namespace Barotrauma
static class GUI
{
// Controls where a line is drawn for given coords.
public enum OutlinePosition
{
Default = 0, // Thickness is inside of top left and outside of bottom right coord
Inside = 1, // Thickness is subtracted from the inside
Centered = 2, // Thickness is centered on given coords
Outside = 3, // Tickness is added to the outside
}
public static GUICanvas Canvas => GUICanvas.Instance;
public static CursorState MouseCursor = CursorState.Default;
@@ -248,7 +255,17 @@ namespace Barotrauma
ScreenChanged = false;
}
updateList.ForEach(c => c.DrawAuto(spriteBatch));
foreach (GUIComponent c in updateList)
{
c.DrawAuto(spriteBatch);
}
// always draw IME preview on top of everything else
foreach (GUIComponent c in updateList)
{
if (c is not GUITextBox box) { continue; }
box.DrawIMEPreview(spriteBatch);
}
if (ScreenOverlayColor.A > 0.0f)
{
@@ -310,7 +327,7 @@ namespace Barotrauma
DrawString(spriteBatch, new Vector2(10, y),
"FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond),
Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
if (GameMain.GameSession != null && Timing.TotalTime > GameMain.GameSession.RoundStartTime + 1.0)
if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 1.0)
{
y += yStep;
DrawString(spriteBatch, new Vector2(10, y),
@@ -1350,7 +1367,7 @@ namespace Barotrauma
}
}
#region Element drawing
#region Element drawing
private static readonly List<float> usedIndicatorAngles = new List<float>();
@@ -1605,6 +1622,54 @@ namespace Barotrauma
}
}
public static void DrawRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 origin, float rotation, Color clr, float depth = 0.0f, float thickness = 1, OutlinePosition outlinePos = OutlinePosition.Centered)
{
Vector2 topLeft = new Vector2(-origin.X, -origin.Y);
Vector2 topRight = new Vector2(-origin.X + size.X, -origin.Y);
Vector2 bottomLeft = new Vector2(-origin.X, -origin.Y + size.Y);
Vector2 actualSize = size;
switch(outlinePos)
{
case OutlinePosition.Default:
actualSize += new Vector2(thickness);
break;
case OutlinePosition.Centered:
topLeft -= new Vector2(thickness * 0.5f);
topRight -= new Vector2(thickness * 0.5f);
bottomLeft -= new Vector2(thickness * 0.5f);
actualSize += new Vector2(thickness);
break;
case OutlinePosition.Inside:
topRight -= new Vector2(thickness, 0.0f);
bottomLeft -= new Vector2(0.0f, thickness);
break;
case OutlinePosition.Outside:
topLeft -= new Vector2(thickness);
topRight -= new Vector2(0.0f, thickness);
bottomLeft -= new Vector2(thickness, 0.0f);
actualSize += new Vector2(thickness * 2.0f);
break;
}
Matrix rotate = Matrix.CreateRotationZ(rotation);
topLeft = Vector2.Transform(topLeft, rotate) + position;
topRight = Vector2.Transform(topRight, rotate) + position;
bottomLeft = Vector2.Transform(bottomLeft, rotate) + position;
Rectangle srcRect = new Rectangle(0, 0, 1, 1);
sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
sb.Draw(solidWhiteTexture, topRight, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
sb.Draw(solidWhiteTexture, bottomLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
}
public static void DrawFilledRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 pivot, float rotation, Color clr, float depth = 0.0f)
{
Rectangle srcRect = new Rectangle(0, 0, 1, 1);
sb.Draw(solidWhiteTexture, position, srcRect, clr, rotation, (pivot/size), size, SpriteEffects.None, depth);
}
public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f)
{
DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth);
@@ -1788,9 +1853,9 @@ 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
#endregion
#region Element creation
#region Element creation
public static Texture2D CreateCircle(int radius, bool filled = false)
{
@@ -2162,9 +2227,9 @@ namespace Barotrauma
return msgBox;
}
#endregion
#endregion
#region Element positioning
#region Element positioning
private static List<T> CreateElements<T>(int count, RectTransform parent, Func<RectTransform, T> constructor,
Vector2? relativeSize = null, Point? absoluteSize = null,
Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
@@ -2363,9 +2428,9 @@ namespace Barotrauma
}
}
#endregion
#endregion
#region Misc
#region Misc
public static void TogglePauseMenu()
{
if (Screen.Selected == GameMain.MainMenuScreen) { return; }
@@ -2603,6 +2668,6 @@ namespace Barotrauma
if (!isSavingIndicatorEnabled) { return; }
timeUntilSavingIndicatorDisabled = delay;
}
#endregion
#endregion
}
}
@@ -9,6 +9,7 @@ using Barotrauma.IO;
using RestSharp;
using System.Net;
using System.Collections.Immutable;
using Barotrauma.Tutorials;
namespace Barotrauma
{
@@ -736,7 +737,7 @@ namespace Barotrauma
public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos)
{
if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; }
if (ObjectiveManager.ContentRunning) { return; }
int width = (int)(400 * GUI.Scale);
int height = (int)(18 * GUI.Scale);
@@ -757,9 +758,9 @@ namespace Barotrauma
toolTipBlock.DrawManually(spriteBatch);
}
public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement)
public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Rectangle targetElement, Anchor anchor = Anchor.BottomCenter, Pivot pivot = Pivot.TopLeft)
{
if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial.ContentRunning) { return; }
if (ObjectiveManager.ContentRunning) { return; }
int width = (int)(400 * GUI.Scale);
int height = (int)(18 * GUI.Scale);
@@ -774,7 +775,10 @@ namespace Barotrauma
toolTipBlock.UserData = toolTip;
}
toolTipBlock.RectTransform.AbsoluteOffset = new Point(targetElement.Center.X, targetElement.Bottom);
toolTipBlock.RectTransform.AbsoluteOffset =
RectTransform.CalculateAnchorPoint(anchor, targetElement) +
RectTransform.CalculatePivotOffset(pivot, toolTipBlock.RectTransform.NonScaledSize);
if (toolTipBlock.Rect.Right > GameMain.GraphicsWidth - 10)
{
toolTipBlock.RectTransform.AbsoluteOffset -= new Point(toolTipBlock.Rect.Width, 0);
@@ -111,6 +111,7 @@ namespace Barotrauma
}
public void ReceiveTextInput(string text) { }
public void ReceiveCommandInput(char command) { }
public void ReceiveEditingInput(string text, int start, int length) { }
public void ReceiveSpecialInput(Keys key)
{
@@ -121,9 +122,7 @@ namespace Barotrauma
listBox.ReceiveSpecialInput(key);
GUI.KeyboardDispatcher.Subscriber = this;
break;
case Keys.Enter:
case Keys.Space:
case Keys.Escape:
default:
GUI.KeyboardDispatcher.Subscriber = null;
break;
}
@@ -150,7 +150,6 @@ namespace Barotrauma
}
}
// TODO: fix implicit hiding
public override bool Selected
{
get { return isSelected; }
@@ -1348,6 +1347,7 @@ namespace Barotrauma
}
public void ReceiveTextInput(string text) { }
public void ReceiveCommandInput(char command) { }
public void ReceiveEditingInput(string text, int start, int length) { }
public void ReceiveSpecialInput(Keys key)
{
@@ -1365,9 +1365,7 @@ namespace Barotrauma
case Keys.Right:
if (isHorizontal && AllowArrowKeyScroll) { SelectNext(playSelectSound: PlaySelectSound.Yes); }
break;
case Keys.Enter:
case Keys.Space:
case Keys.Escape:
default:
GUI.KeyboardDispatcher.Subscriber = null;
break;
}
@@ -24,7 +24,8 @@ namespace Barotrauma
InGame,
Vote,
Hint,
Tutorial
Tutorial,
Warning // Keep this last so that it's always drawn in front
}
private bool IsAnimated => type == Type.InGame || type == Type.Hint || type == Type.Tutorial;
@@ -84,8 +85,8 @@ namespace Barotrauma
public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault();
public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null)
: this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize)
public GUIMessageBox(LocalizedString headerText, LocalizedString text, Vector2? relativeSize = null, Point? minSize = null, Type type = Type.Default)
: this(headerText, text, new LocalizedString[] { "OK" }, relativeSize, minSize, type: type)
{
this.Buttons[0].OnClicked = Close;
}
@@ -147,7 +148,7 @@ namespace Barotrauma
Tag = tag.ToIdentifier();
#warning TODO: These should be broken into separate methods at least
if (type == Type.Default || type == Type.Vote)
if (type == Type.Default || type == Type.Vote || type == Type.Warning)
{
Content = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), InnerFrame.RectTransform, Anchor.Center)) { AbsoluteSpacing = 5 };
@@ -179,6 +179,11 @@ namespace Barotrauma
private set;
}
/// <summary>
/// If enabled, the value wraps around to Max when you go below Min, and vice versa
/// </summary>
public bool WrapAround;
public float valueStep;
private float pressedTimer;
@@ -403,13 +408,19 @@ namespace Barotrauma
{
if (MinValueFloat != null)
{
floatValue = Math.Max(floatValue, MinValueFloat.Value);
MinusButton.Enabled = floatValue > MinValueFloat;
floatValue =
WrapAround && MinValueFloat.HasValue && floatValue < MinValueFloat.Value ?
MaxValueFloat.Value :
Math.Max(floatValue, MinValueFloat.Value);
MinusButton.Enabled = WrapAround || floatValue > MinValueFloat;
}
if (MaxValueFloat != null)
{
floatValue = Math.Min(floatValue, MaxValueFloat.Value);
PlusButton.Enabled = floatValue < MaxValueFloat;
floatValue =
WrapAround && MaxValueFloat.HasValue && floatValue > MaxValueFloat.Value ?
MinValueFloat.Value :
Math.Min(floatValue, MaxValueFloat.Value);
PlusButton.Enabled = WrapAround || floatValue < MaxValueFloat;
}
}
@@ -417,16 +428,16 @@ namespace Barotrauma
{
if (MinValueInt != null && intValue < MinValueInt.Value)
{
intValue = Math.Max(intValue, MinValueInt.Value);
intValue = WrapAround && MaxValueInt.HasValue ? MaxValueInt.Value : Math.Max(intValue, MinValueInt.Value);
UpdateText();
}
if (MaxValueInt != null && intValue > MaxValueInt.Value)
{
intValue = Math.Min(intValue, MaxValueInt.Value);
intValue = WrapAround && MinValueInt.HasValue ? MinValueInt.Value : Math.Min(intValue, MaxValueInt.Value);
UpdateText();
}
PlusButton.Enabled = MaxValueInt == null || intValue < MaxValueInt;
MinusButton.Enabled = MinValueInt == null || intValue > MinValueInt;
PlusButton.Enabled = WrapAround || MaxValueInt == null || intValue < MaxValueInt;
MinusButton.Enabled = WrapAround || MinValueInt == null || intValue > MinValueInt;
}
private void UpdateText()
@@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -45,16 +46,17 @@ namespace Barotrauma
}
}
private ScalableFont cjkFont;
private ImmutableDictionary<TextManager.SpeciallyHandledCharCategory, ScalableFont> specialHandlingFonts;
public ScalableFont CjkFont
public ScalableFont GetFontForCategory(TextManager.SpeciallyHandledCharCategory category)
{
get
{
if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); }
if (font.IsCJK) { return font; }
return cjkFont;
}
if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); }
if (font.SpeciallyHandledCharCategory.HasFlag(category)) { return font; }
return specialHandlingFonts.TryGetValue(category, out var resultFont)
? resultFont
: specialHandlingFonts.TryGetValue(TextManager.SpeciallyHandledCharCategory.CJK, out resultFont)
? resultFont
: font;
}
public LanguageIdentifier Language { get; private set; }
@@ -70,40 +72,68 @@ namespace Barotrauma
string fontPath = GetFontFilePath(element);
uint size = GetFontSize(element);
bool dynamicLoading = GetFontDynamicLoading(element);
bool isCJK = GetIsCJK(element);
var shcc = GetShcc(element);
font?.Dispose();
cjkFont?.Dispose();
font = new ScalableFont(fontPath, size, GameMain.Instance.GraphicsDevice, dynamicLoading, isCJK)
specialHandlingFonts?.Values.ForEach(f => f.Dispose());
font = new ScalableFont(
fontPath,
size,
GameMain.Instance.GraphicsDevice,
dynamicLoading,
shcc)
{
ForceUpperCase = element.GetAttributeBool("forceuppercase", false)
};
if (!isCJK)
var fallbackFonts = new Dictionary<TextManager.SpeciallyHandledCharCategory, ScalableFont>();
foreach (var flag in TextManager.SpeciallyHandledCharCategories)
{
cjkFont = ExtractCjkFont(element)
?? new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf",
font.Size, GameMain.Instance.GraphicsDevice, dynamicLoading: true, isCJK: true);
cjkFont.ForceUpperCase = font.ForceUpperCase;
if (shcc.HasFlag(flag)) { continue; }
var extractedFont = ExtractFont(flag, element);
if (extractedFont is null) { continue; }
fallbackFonts.Add(flag, extractedFont);
}
fallbackFonts.Values.ForEach(ff => ff.ForceUpperCase = font.ForceUpperCase);
specialHandlingFonts = fallbackFonts.ToImmutableDictionary();
Language = GameSettings.CurrentConfig.Language;
}
public override void Dispose()
{
font?.Dispose(); font = null;
cjkFont?.Dispose(); cjkFont = null;
font?.Dispose();
font = null;
specialHandlingFonts?.Values.ForEach(f => f.Dispose());
specialHandlingFonts = null;
}
private ScalableFont ExtractCjkFont(ContentXElement element)
private ScalableFont ExtractFont(TextManager.SpeciallyHandledCharCategory flag, ContentXElement element)
{
foreach (var subElement in element.Elements().Reverse())
{
if (subElement.NameAsIdentifier() != "override") { continue; }
if (subElement.GetAttributeBool("iscjk", false))
if (ScalableFont.ExtractShccFromXElement(subElement).HasFlag(flag))
{
return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice);
}
}
return null;
ScalableFont hardcodedFallback(string path)
=> new ScalableFont(
path,
font.Size,
GameMain.Instance.GraphicsDevice,
dynamicLoading: true,
speciallyHandledCharCategory: flag);
return flag switch
{
TextManager.SpeciallyHandledCharCategory.CJK
=> hardcodedFallback("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf"),
TextManager.SpeciallyHandledCharCategory.Cyrillic
=> hardcodedFallback("Content/Fonts/Oswald-Bold.ttf"),
_ => null
};
}
private string GetFontFilePath(ContentXElement element)
@@ -154,21 +184,21 @@ namespace Barotrauma
return element.GetAttributeBool("dynamicloading", false);
}
private bool GetIsCJK(XElement element)
private TextManager.SpeciallyHandledCharCategory GetShcc(XElement element)
{
foreach (var subElement in element.Elements())
{
if (IsValidOverride(subElement))
{
return subElement.GetAttributeBool("iscjk", false);
return ScalableFont.ExtractShccFromXElement(subElement);
}
}
return element.GetAttributeBool("iscjk", false);
return ScalableFont.ExtractShccFromXElement(element);
}
private bool IsValidOverride(XElement element)
{
if (!element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { return false; }
if (!element.IsOverride()) { return false; }
var languages = element.GetAttributeIdentifierArray("language", Array.Empty<Identifier>());
return languages.Any(l => l.ToLanguageIdentifier() == GameSettings.CurrentConfig.Language);
}
@@ -191,26 +221,26 @@ namespace Barotrauma
private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value);
public ScalableFont GetFontForStr(string str) =>
TextManager.IsCJK(str) ? Prefabs.ActivePrefab.CjkFont : Prefabs.ActivePrefab.Font;
Prefabs.ActivePrefab.GetFontForCategory(TextManager.GetSpeciallyHandledCategories(str));
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth)
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects spriteEffects, float layerDepth)
{
DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth);
DrawString(sb, text.Value, position, color, rotation, origin, scale, spriteEffects, layerDepth);
}
public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth)
public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects spriteEffects, float layerDepth)
{
GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth);
GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth);
}
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft)
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, Alignment alignment = Alignment.TopLeft)
{
DrawString(sb, text.Value, position, color, rotation, origin, scale, se, layerDepth, alignment);
DrawString(sb, text.Value, position, color, rotation, origin, scale, spriteEffects, layerDepth, alignment);
}
public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
public void DrawString(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
{
GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, se, layerDepth, alignment, forceUpperCase);
GetFontForStr(text).DrawString(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, alignment, forceUpperCase);
}
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit, bool italics = false)
@@ -223,9 +253,9 @@ namespace Barotrauma
GetFontForStr(text).DrawString(sb, text, position, color, forceUpperCase, italics);
}
public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects se, float layerDepth, in ImmutableArray<RichTextData>? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
public void DrawStringWithColors(SpriteBatch sb, string text, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects spriteEffects, float layerDepth, in ImmutableArray<RichTextData>? richTextData, int rtdOffset = 0, Alignment alignment = Alignment.TopLeft, ForceUpperCase forceUpperCase = Barotrauma.ForceUpperCase.Inherit)
{
GetFontForStr(text).DrawStringWithColors(sb, text, position, color, rotation, origin, scale, se, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase);
GetFontForStr(text).DrawStringWithColors(sb, text, position, color, rotation, origin, scale, spriteEffects, layerDepth, richTextData, rtdOffset, alignment, forceUpperCase);
}
public Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing = false)
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Barotrauma
@@ -285,8 +286,8 @@ namespace Barotrauma
/// This is the new constructor.
/// If the rectT height is set 0, the height is calculated from the text.
/// </summary>
public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null,
Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null)
public GUITextBlock(RectTransform rectT, RichString text, Color? textColor = null, GUIFont font = null,
Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null)
: base(style, rectT)
{
if (color.HasValue)
@@ -551,6 +552,8 @@ namespace Barotrauma
if (TextGetter != null) { Text = TextGetter(); }
string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue);
Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
if (overflowClipActive)
{
@@ -561,7 +564,7 @@ namespace Barotrauma
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
}
if (!text.IsNullOrEmpty())
if (!string.IsNullOrEmpty(textToShow))
{
Vector2 pos = rect.Location.ToVector2() + textPos + TextOffset;
if (RoundToNearestPixel)
@@ -570,7 +573,8 @@ namespace Barotrauma
pos.Y = (int)pos.Y;
}
Color currentTextColor = State == ComponentState.Hover || State == ComponentState.HoverSelected ? HoverTextColor : TextColor;
Color currentTextColor = State is ComponentState.Hover or ComponentState.HoverSelected ? HoverTextColor : TextColor;
if (!enabled)
{
currentTextColor = disabledTextColor;
@@ -582,8 +586,14 @@ namespace Barotrauma
if (!HasColorHighlight)
{
string textToShow = Censor ? censoredText : (Wrap ? wrappedText.Value : text.SanitizedValue);
Color colorToShow = currentTextColor * (currentTextColor.A / 255.0f);
if (TextManager.DebugDraw)
{
if (!text.NestedStr.Loaded || text.NestedStr.Language == LanguageIdentifier.None)
{
colorToShow = Color.Magenta;
}
}
if (Shadow)
{
@@ -597,10 +607,10 @@ namespace Barotrauma
{
if (OverrideRichTextDataAlpha)
{
RichTextData.Value.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f);
RichTextData?.ForEach(rt => rt.Alpha = currentTextColor.A / 255.0f);
}
Font.DrawStringWithColors(spriteBatch, Censor ? censoredText : (Wrap ? wrappedText : text.SanitizedString).Value, pos,
currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData.Value, alignment: textAlignment, forceUpperCase: ForceUpperCase);
Font.DrawStringWithColors(spriteBatch, textToShow, pos,
currentTextColor * (currentTextColor.A / 255.0f), 0.0f, origin, TextScale, SpriteEffects.None, textDepth, RichTextData, alignment: textAlignment, forceUpperCase: ForceUpperCase);
}
Strikethrough?.Draw(spriteBatch, (int)Math.Ceiling(TextSize.X / 2f), pos.X,
@@ -11,7 +11,7 @@ namespace Barotrauma
public delegate void TextBoxEvent(GUITextBox sender, Keys key);
public class GUITextBox : GUIComponent, IKeyboardSubscriber
public partial class GUITextBox : GUIComponent, IKeyboardSubscriber
{
public event TextBoxEvent OnSelected;
public event TextBoxEvent OnDeselected;
@@ -77,12 +77,12 @@ namespace Barotrauma
private int selectionEndIndex;
private bool IsLeftToRight => selectionStartIndex <= selectionEndIndex;
private GUICustomComponent caretAndSelectionRenderer;
private readonly GUICustomComponent caretAndSelectionRenderer;
private bool mouseHeldInside;
private readonly Memento<string> memento = new Memento<string>();
// Skip one update cycle, fixes Enter key instantly deselecting the chatbox
private bool skipUpdate;
@@ -199,6 +199,7 @@ namespace Barotrauma
base.Font = value;
if (textBlock == null) { return; }
textBlock.Font = value;
imePreviewTextHandler.Font = Font;
}
}
@@ -263,6 +264,10 @@ namespace Barotrauma
public override bool PlaySoundOnSelect { get; set; } = true;
private readonly IMEPreviewTextHandler imePreviewTextHandler;
public bool IsIMEActive => imePreviewTextHandler is { HasText: true };
public GUITextBox(RectTransform rectT, string text = "", Color? textColor = null, GUIFont font = null,
Alignment textAlignment = Alignment.Left, bool wrap = false, string style = "", Color? color = null, bool createClearButton = false, bool createPenIcon = true)
: base(style, rectT)
@@ -274,6 +279,7 @@ namespace Barotrauma
frame = new GUIFrame(new RectTransform(Vector2.One, rectT, Anchor.Center), style, color);
GUIStyle.Apply(frame, style == "" ? "GUITextBox" : style);
textBlock = new GUITextBlock(new RectTransform(Vector2.One, frame.RectTransform, Anchor.CenterLeft), text ?? "", textColor, font, textAlignment, wrap);
imePreviewTextHandler = new IMEPreviewTextHandler(textBlock.Font);
GUIStyle.Apply(textBlock, "", this);
if (font != null) { textBlock.Font = font; }
CaretEnabled = true;
@@ -305,18 +311,17 @@ namespace Barotrauma
textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - clearButtonWidth - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue);
}
Font = textBlock.Font;
Enabled = true;
rectT.SizeChanged += () =>
rectT.SizeChanged += () =>
{
if (icon != null) { textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); }
caretPosDirty = true;
caretPosDirty = true;
};
rectT.ScaleChanged += () =>
{
if (icon != null) { textBlock.RectTransform.MaxSize = new Point(frame.Rect.Width - icon.Rect.Height - icon.RectTransform.AbsoluteOffset.X * 2, int.MaxValue); }
caretPosDirty = true;
caretPosDirty = true;
};
}
@@ -391,14 +396,16 @@ namespace Barotrauma
{
GUI.KeyboardDispatcher.Subscriber = null;
}
OnDeselected?.Invoke(this, Keys.None);
imePreviewTextHandler.Reset();
}
public override void Flash(Color? color = null, float flashDuration = 1.5f, bool useRectangleFlash = false, bool useCircularFlash = false, Vector2? flashRectOffset = null)
{
frame.Flash(color, flashDuration, useRectangleFlash, useCircularFlash, flashRectOffset);
}
protected override void Update(float deltaTime)
{
if (!Visible) return;
@@ -579,6 +586,7 @@ namespace Barotrauma
{
CaretIndex = Math.Min(Text.Length, CaretIndex + input.Length);
OnTextChanged?.Invoke(this, Text);
imePreviewTextHandler?.Reset();
}
}
@@ -607,10 +615,12 @@ namespace Barotrauma
public void ReceiveCommandInput(char command)
{
if (Text == null) Text = "";
if (IsIMEActive) { return; }
if (Text == null) { Text = ""; }
// Prevent alt gr from triggering any of these as that combination is often needed for special characters
if (PlayerInput.IsAltDown()) return;
if (PlayerInput.IsAltDown()) { return; }
switch (command)
{
@@ -684,8 +694,21 @@ namespace Barotrauma
}
}
public void ReceiveEditingInput(string text, int start, int length)
{
if (string.IsNullOrEmpty(text))
{
imePreviewTextHandler.Reset();
return;
}
imePreviewTextHandler.UpdateText(text, start, length);
}
public void ReceiveSpecialInput(Keys key)
{
if (IsIMEActive) { return; }
switch (key)
{
case Keys.Left:
@@ -874,6 +897,11 @@ namespace Barotrauma
}
}
public void DrawIMEPreview(SpriteBatch spriteBatch)
{
imePreviewTextHandler.DrawIMEPreview(spriteBatch, CaretScreenPos, textBlock);
}
private void CalculateSelection()
{
string textDrawn = Censor ? textBlock.CensoredText : WrappedText;
@@ -65,7 +65,7 @@ namespace Barotrauma
get; private set;
}
public static Rectangle AfflictionAreaLeft
public static Rectangle HealthBarAfflictionArea
{
get; private set;
}
@@ -143,7 +143,7 @@ namespace Barotrauma
}
int healthBarHeight = (int)(50f * GUI.Scale);
HealthBarArea = new Rectangle(BottomRightInfoArea.Right - healthBarWidth + (int)Math.Floor(1 / GUI.Scale), BottomRightInfoArea.Y - healthBarHeight + GUI.IntScale(10), healthBarWidth, healthBarHeight);
AfflictionAreaLeft = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight);
HealthBarAfflictionArea = new Rectangle(HealthBarArea.X, HealthBarArea.Y - Padding - afflictionAreaHeight, HealthBarArea.Width, afflictionAreaHeight);
int messageAreaWidth = GameMain.GraphicsWidth / 3;
@@ -173,7 +173,7 @@ namespace Barotrauma
int objectiveListAreaX = HealthWindowAreaLeft.Right + Padding;
int objectiveListAreaY = ButtonAreaTop.Bottom + Padding;
TutorialObjectiveListArea = new Rectangle(objectiveListAreaX, objectiveListAreaY, (GameMain.GraphicsWidth - Padding) - objectiveListAreaX, (AfflictionAreaLeft.Top - Padding) - objectiveListAreaY);
TutorialObjectiveListArea = new Rectangle(objectiveListAreaX, objectiveListAreaY, (GameMain.GraphicsWidth - Padding) - objectiveListAreaX, (HealthBarAfflictionArea.Top - Padding) - objectiveListAreaY);
int votingAreaWidth = (int)(400 * GUI.Scale);
int votingAreaX = GameMain.GraphicsWidth - Padding - votingAreaWidth;
@@ -193,7 +193,7 @@ namespace Barotrauma
DrawRectangle(CrewArea, Color.Blue * 0.5f);
DrawRectangle(ChatBoxArea, Color.Cyan * 0.5f);
DrawRectangle(HealthBarArea, Color.Red * 0.5f);
DrawRectangle(AfflictionAreaLeft, Color.Red * 0.5f);
DrawRectangle(HealthBarAfflictionArea, Color.Red * 0.5f);
DrawRectangle(InventoryAreaLower, Color.Yellow * 0.5f);
DrawRectangle(HealthWindowAreaLeft, Color.Red * 0.5f);
DrawRectangle(BottomRightInfoArea, Color.Green * 0.5f);
@@ -0,0 +1,90 @@
#nullable enable
using System.Collections.Immutable;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
public sealed class IMEPreviewTextHandler
{
public bool HasText => !string.IsNullOrEmpty(previewText);
// This has to be settable because for some reason we update the font of GUITextBox in some places
public GUIFont Font { get; set; }
private string previewText = string.Empty;
private Vector2 textSize;
private bool isSectioned;
private ImmutableArray<RichTextData>? richTextData;
public IMEPreviewTextHandler(GUIFont font)
{
Font = font;
}
public void Reset()
{
textSize = Vector2.Zero;
previewText = string.Empty;
richTextData = null;
isSectioned = false;
}
public void UpdateText(string text, int start, int length)
{
isSectioned = start >= 0 && length > 0;
richTextData = null;
if (string.IsNullOrEmpty(text))
{
Reset();
return;
}
previewText = text;
textSize = Font.MeasureString(text);
if (!isSectioned) { return; }
string coloredText = ToolBox.ColorSectionOfString(text, start, length, GUIStyle.Orange);
RichString richString = RichString.Rich(coloredText);
previewText = richString.SanitizedValue;
richTextData = richString.RichTextData;
}
public void DrawIMEPreview(SpriteBatch spriteBatch, Vector2 position, GUITextBlock textBlock)
{
if (!HasText) { return; }
int inflate = GUI.IntScale(3);
RectangleF rect = new RectangleF(position, textSize);
rect.Inflate(inflate, inflate);
RectangleF borderRect = rect;
borderRect.Inflate(1, 1);
GUI.DrawFilledRectangle(spriteBatch, borderRect, Color.White, depth: 0.02f);
GUI.DrawFilledRectangle(spriteBatch, rect, Color.Black, depth: 0.01f);
Font.DrawStringWithColors(spriteBatch,
text: previewText,
position: position,
color: isSectioned ? GUIStyle.TextColorNormal : GUIStyle.Orange,
rotation: 0.0f,
origin: Vector2.Zero,
scale: 1f,
spriteEffects: SpriteEffects.None,
layerDepth: 0,
richTextData: richTextData,
alignment: textBlock.TextAlignment,
forceUpperCase: textBlock.ForceUpperCase);
}
}
}
@@ -12,7 +12,7 @@ using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement;
namespace Barotrauma
{
[SuppressMessage("ReSharper", "UnusedVariable")]
internal class MedicalClinicUI
internal sealed class MedicalClinicUI
{
private enum ElementState
{
@@ -127,12 +127,14 @@ namespace Barotrauma
{
public readonly GUIComponent Panel;
public readonly GUIListBox HealList;
public readonly GUIComponent TreatAllButton;
public readonly List<CrewElement> HealElements;
public CrewHealList(GUIListBox healList, GUIComponent panel)
public CrewHealList(GUIListBox healList, GUIComponent panel, GUIComponent treatAllButton)
{
Panel = panel;
HealList = healList;
TreatAllButton = treatAllButton;
HealElements = new List<CrewElement>();
}
}
@@ -179,7 +181,7 @@ namespace Barotrauma
private PopupAfflictionList? selectedCrewAfflictionList;
private bool isWaitingForServer;
private const float refreshTimerMax = 3f;
private float refreshTimer = 0;
private float refreshTimer;
private PlayerBalanceElement? playerBalanceElement;
@@ -196,7 +198,7 @@ namespace Barotrauma
{
new GUIButton(new RectTransform(new Vector2(0.2f, 0.1f), parent.RectTransform, Anchor.TopCenter), "Recreate UI - NOT PRESENT IN RELEASE!")
{
OnClicked = (_, __) =>
OnClicked = (_, _) =>
{
parent.ClearChildren();
CreateUI();
@@ -254,7 +256,7 @@ namespace Barotrauma
continue;
}
CreatePendingHealElement(healList.HealList.Content, crewMember, healList, Array.Empty<MedicalClinic.NetAffliction>());
CreatePendingHealElement(healList.HealList.Content, crewMember, healList, ImmutableArray<MedicalClinic.NetAffliction>.Empty);
}
// check if there are elements that the crew doesn't have
@@ -309,7 +311,7 @@ namespace Barotrauma
private void UpdateCrewPanel()
{
if (!(crewHealList is { } healList)) { return; }
if (crewHealList is not { } healList) { return; }
ImmutableArray<CharacterInfo> crew = MedicalClinic.GetCrewCharacters();
@@ -334,12 +336,21 @@ namespace Barotrauma
healList.HealList.Content.RemoveChild(element.UIElement);
}
IEnumerable<CrewElement> orderedList = healList.HealElements.OrderBy(element => element.Target.Character?.HealthPercentage ?? 100);
IEnumerable<CrewElement> orderedList = healList.HealElements.OrderBy(static element => element.Target.Character?.HealthPercentage ?? 100);
foreach (CrewElement element in orderedList)
{
element.UIElement.SetAsLastChild();
}
healList.TreatAllButton.Enabled = false;
foreach (CrewElement element in healList.HealElements)
{
if (element.Afflictions.Count is 0) { continue; }
healList.TreatAllButton.Enabled = true;
break;
}
}
private static void UpdateAfflictionList(CrewElement healElement)
@@ -350,7 +361,7 @@ namespace Barotrauma
// sum up all the afflictions and their strengths
Dictionary<AfflictionPrefab, float> afflictionAndStrength = new Dictionary<AfflictionPrefab, float>();
foreach (Affliction affliction in health.GetAllAfflictions().Where(a => MedicalClinic.IsHealable(a)))
foreach (Affliction affliction in health.GetAllAfflictions().Where(MedicalClinic.IsHealable))
{
if (afflictionAndStrength.TryGetValue(affliction.Prefab, out float strength))
{
@@ -446,8 +457,8 @@ namespace Barotrauma
};
GUILayoutGroup clinicLabelLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), clinicContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
GUIImage clinicIcon = new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true);
GUITextBlock clinicLabel = new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont);
new GUIImage(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewManagementHeaderIcon", scaleToFit: true);
new GUITextBlock(new RectTransform(Vector2.One, clinicLabelLayout.RectTransform), TextManager.Get("medicalclinic.medicalclinic"), font: GUIStyle.LargeFont);
GUIFrame clinicBackground = new GUIFrame(new RectTransform(Vector2.One, clinicContent.RectTransform));
@@ -480,22 +491,24 @@ namespace Barotrauma
Stretch = true
};
// GUILayoutGroup sortLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.05f), clinicContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
// new GUITextBlock(new RectTransform(new Vector2(0.2f, 1f), sortLayout.RectTransform), TextManager.Get("campaignstore.sortby"), font: GUI.SubHeadingFont);
// GUIDropDown sortDropdown = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1f), sortLayout.RectTransform));
//
// foreach (SortMode mode in Enum.GetValues(typeof(SortMode)).Cast<SortMode>())
// {
// sortDropdown.AddItem(TextManager.Get($"medicalclinic.sortmode.{mode}"), mode);
// }
//
// sortDropdown.SelectItem(SortMode.Severity);
GUIListBox crewList = new GUIListBox(new RectTransform(Vector2.One, clinicContainer.RectTransform));
crewHealList = new CrewHealList(crewList, parent);
GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone"))
{
OnClicked = (_, _) =>
{
isWaitingForServer = true;
medicalClinic.TreatAllButtonAction(OnReceived);
return true;
}
};
crewHealList = new CrewHealList(crewList, parent, treatAllButton);
void OnReceived(MedicalClinic.CallbackOnlyRequest obj)
{
isWaitingForServer = false;
}
}
private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel)
@@ -525,9 +538,9 @@ namespace Barotrauma
TextColor = GUIStyle.Red
};
MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember { CharacterInfo = info, Afflictions = Array.Empty<MedicalClinic.NetAffliction>() };
MedicalClinic.NetCrewMember member = new MedicalClinic.NetCrewMember(info);
crewBackground.OnClicked = (_, __) =>
crewBackground.OnClicked = (_, _) =>
{
SelectCharacter(member, new Vector2(panel.Rect.Right, crewBackground.Rect.Top));
return true;
@@ -618,7 +631,7 @@ namespace Barotrauma
pendingHealList = list;
}
private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, MedicalClinic.NetAffliction[] afflictions)
private void CreatePendingHealElement(GUIComponent parent, MedicalClinic.NetCrewMember crewMember, PendingHealList healList, ImmutableArray<MedicalClinic.NetAffliction> afflictions)
{
CharacterInfo? healInfo = crewMember.FindCharacterInfo(MedicalClinic.GetCrewCharacters());
if (healInfo is null) { return; }
@@ -803,7 +816,7 @@ namespace Barotrauma
}
allComponents.Add(treatAllButton);
treatAllButton.OnClicked = (_, __) =>
treatAllButton.OnClicked = (_, _) =>
{
ImmutableArray<MedicalClinic.NetAffliction> afflictions = request.Afflictions.Where(a => !medicalClinic.IsAfflictionPending(crewMember, a)).ToImmutableArray();
if (!afflictions.Any()) { return true; }
@@ -845,9 +858,9 @@ namespace Barotrauma
GUITextBlock prefabBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), topTextLayout.RectTransform), prefab.Name, font: GUIStyle.SubHeadingFont);
Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, (int)affliction.AfflictionSeverity / 2f);
Color textColor = Color.Lerp(GUIStyle.Orange, GUIStyle.Red, affliction.Strength / affliction.Prefab.MaxStrength);
LocalizedString vitalityText = TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.Strength).ToString());
LocalizedString vitalityText = affliction.VitalityDecrease == 0 ? string.Empty : TextManager.GetWithVariable("medicalclinic.vitalitydifference", "[amount]", (-affliction.VitalityDecrease).ToString());
GUITextBlock vitalityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), vitalityText, textAlignment: Alignment.Center)
{
TextColor = textColor,
@@ -856,7 +869,7 @@ namespace Barotrauma
AutoScaleHorizontal = true
};
LocalizedString severityText = TextManager.Get($"AfflictionStrength{affliction.AfflictionSeverity}");
LocalizedString severityText = Affliction.GetStrengthText(affliction.Strength, affliction.Prefab.MaxStrength);
GUITextBlock severityBlock = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), topTextLayout.RectTransform), severityText, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont)
{
TextColor = textColor,
@@ -873,9 +886,13 @@ namespace Barotrauma
{
RelativeSpacing = 0.05f
};
GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform), prefab.Description, font: GUIStyle.SmallFont, wrap: true)
LocalizedString description = affliction.Prefab.GetDescription(affliction.Strength, AfflictionPrefab.Description.TargetType.OtherCharacter);
GUITextBlock descriptionBlock = new GUITextBlock(new RectTransform(new Vector2(1f, 0.6f), bottomTextLayout.RectTransform),
description,
font: GUIStyle.SmallFont,
wrap: true)
{
ToolTip = prefab.Description
ToolTip = description
};
bool truncated = false;
while (descriptionBlock.TextSize.Y > descriptionBlock.Rect.Height && descriptionBlock.WrappedText.Contains('\n'))
@@ -919,10 +936,9 @@ namespace Barotrauma
}
else
{
MedicalClinic.NetCrewMember newMember = new MedicalClinic.NetCrewMember
MedicalClinic.NetCrewMember newMember = crewMember with
{
CharacterInfoID = crewMember.CharacterInfoID,
Afflictions = Array.Empty<MedicalClinic.NetAffliction>()
Afflictions = ImmutableArray<MedicalClinic.NetAffliction>.Empty
};
existingMember = newMember;
@@ -936,7 +952,7 @@ namespace Barotrauma
}
}
existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToArray();
existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray();
ToggleElements(ElementState.Disabled, elementsToDisable);
medicalClinic.AddPendingButtonAction(existingMember, request =>
{
@@ -134,7 +134,7 @@ namespace Barotrauma
set => hadSellSubPermissions = value;
}
private bool HasPermissionToUseTab(StoreTab tab)
private static bool HasPermissionToUseTab(StoreTab tab)
{
return tab switch
{
@@ -278,6 +278,7 @@ namespace Barotrauma
RefreshBuying(updateOwned: false);
RefreshSelling(updateOwned: false);
RefreshSellingFromSub(updateOwned: false);
SetConfirmButtonBehavior();
needsRefresh = false;
}
@@ -475,6 +476,7 @@ namespace Barotrauma
};
List<MapEntityCategory> itemCategories = Enum.GetValues(typeof(MapEntityCategory)).Cast<MapEntityCategory>().ToList();
itemCategories.Remove(MapEntityCategory.None);
//don't show categories with no buyable items
itemCategories.RemoveAll(c => !ItemPrefab.Prefabs.Any(ep => ep.Category.HasFlag(c) && ep.CanBeBought));
itemCategoryButtons.Clear();
@@ -507,6 +509,7 @@ namespace Barotrauma
{
btn.RectTransform.SizeChanged += () =>
{
if (btn.Frame.sprites == null) { return; }
var sprite = btn.Frame.sprites[GUIComponent.ComponentState.None].First();
btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width)));
};
@@ -617,9 +620,9 @@ namespace Barotrauma
Stretch = true
};
var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null);
shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false };
shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false };
shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false };
shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true)
{
@@ -745,7 +748,7 @@ namespace Barotrauma
} ?? Enumerable.Empty<PurchasedItem>();
foreach (var button in itemCategoryButtons)
{
if (!(button.UserData is MapEntityCategory category))
if (button.UserData is not MapEntityCategory category)
{
continue;
}
@@ -859,18 +862,17 @@ namespace Barotrauma
float prevBuyListScroll = storeBuyList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll;
int dailySpecialCount = ActiveStore.DailySpecials.Count;
if ((storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount)
int dailySpecialCount = ActiveStore?.DailySpecials.Count(s => s.CanCharacterBuy()) ?? 0;
if ((ActiveStore == null && storeDailySpecialsGroup != null) || (storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount)
{
if (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount)
storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent);
if (ActiveStore != null && (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount))
{
storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent);
storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, dailySpecialCount);
storeDailySpecialsGroup.Parent.SetAsFirstChild();
}
else
{
storeBuyList.RemoveChild(storeDailySpecialsGroup.Parent);
storeDailySpecialsGroup = null;
}
storeBuyList.RecalculateChildren();
@@ -879,20 +881,22 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(StoreTab.Buy);
var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in ActiveStore.Stock)
if (ActiveStore != null)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials)
{
if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; }
CreateOrUpdateItemFrame(itemPrefab, 0);
foreach (PurchasedItem item in ActiveStore.Stock)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials)
{
if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; }
CreateOrUpdateItemFrame(itemPrefab, 0);
}
}
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity)
{
if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo))
if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy())
{
bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab);
var itemFrame = isDailySpecial ?
@@ -945,11 +949,11 @@ namespace Barotrauma
float prevSellListScroll = storeSellList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll;
int requestedGoodsCount = ActiveStore.RequestedGoods.Count;
if ((storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount)
int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
if ((ActiveStore == null && storeRequestedGoodGroup != null) || (storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount)
{
storeSellList.RemoveChild(storeRequestedGoodGroup?.Parent);
if (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount)
if (ActiveStore != null && (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount))
{
storeRequestedGoodGroup = CreateDealsGroup(storeSellList, requestedGoodsCount);
storeRequestedGoodGroup.Parent.SetAsFirstChild();
@@ -964,14 +968,17 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(StoreTab.Sell);
var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in itemsToSell)
if (ActiveStore != null)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
foreach (PurchasedItem item in itemsToSell)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
}
}
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
@@ -1029,11 +1036,11 @@ namespace Barotrauma
float prevSellListScroll = storeSellFromSubList.BarScroll;
float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll;
int requestedGoodsCount = ActiveStore.RequestedGoods.Count;
if ((storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount)
int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
if ((ActiveStore == null && storeRequestedSubGoodGroup != null) || (storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount)
{
storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup?.Parent);
if (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount)
if (ActiveStore != null && (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount))
{
storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, requestedGoodsCount);
storeRequestedSubGoodGroup.Parent.SetAsFirstChild();
@@ -1048,14 +1055,17 @@ namespace Barotrauma
bool hasPermissions = HasSellSubPermissions;
var existingItemFrames = new HashSet<GUIComponent>();
foreach (PurchasedItem item in itemsToSellFromSub)
if (ActiveStore != null)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
foreach (PurchasedItem item in itemsToSellFromSub)
{
CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
}
foreach (var requestedGood in ActiveStore.RequestedGoods)
{
if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
CreateOrUpdateItemFrame(requestedGood, 0);
}
}
void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
@@ -1110,7 +1120,7 @@ namespace Barotrauma
private void SetPriceGetters(GUIComponent itemFrame, bool buying)
{
if (itemFrame == null || !(itemFrame.UserData is PurchasedItem pi)) { return; }
if (itemFrame == null || itemFrame.UserData is not PurchasedItem pi) { return; }
if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock)
{
@@ -1142,6 +1152,7 @@ namespace Barotrauma
public void RefreshItemsToSell()
{
itemsToSell.Clear();
if (ActiveStore == null) { return; }
var playerItems = CargoManager.GetSellableItems(Character.Controlled);
foreach (Item playerItem in playerItems)
{
@@ -1172,6 +1183,7 @@ namespace Barotrauma
public void RefreshItemsToSellFromSub()
{
itemsToSellFromSub.Clear();
if (ActiveStore == null) { return; }
var subItems = CargoManager.GetSellableItemsFromSub();
foreach (Item subItem in subItems)
{
@@ -1205,52 +1217,55 @@ namespace Barotrauma
bool hasPermissions = HasTabPermissions(tab);
HashSet<GUIComponent> existingItemFrames = new HashSet<GUIComponent>();
int totalPrice = 0;
foreach (PurchasedItem item in items)
if (ActiveStore != null)
{
if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; }
GUINumberInput numInput = null;
if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame))
foreach (PurchasedItem item in items)
{
itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions);
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
}
else
{
itemFrame.UserData = item;
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
if (!(item.ItemPrefab.GetPriceInfo(ActiveStore) is { } priceInfo)) { continue; }
GUINumberInput numInput = null;
if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame))
{
itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions);
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
}
else
{
itemFrame.UserData = item;
numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
if (numInput != null)
{
numInput.UserData = item;
numInput.Enabled = hasPermissions;
numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab);
}
SetOwnedText(itemFrame);
SetItemFrameStatus(itemFrame, hasPermissions);
}
existingItemFrames.Add(itemFrame);
suppressBuySell = true;
if (numInput != null)
{
numInput.UserData = item;
numInput.Enabled = hasPermissions;
numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab);
if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); }
numInput.IntValue = item.Quantity;
}
SetOwnedText(itemFrame);
SetItemFrameStatus(itemFrame, hasPermissions);
}
existingItemFrames.Add(itemFrame);
suppressBuySell = false;
suppressBuySell = true;
if (numInput != null)
{
if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); }
numInput.IntValue = item.Quantity;
}
suppressBuySell = false;
try
{
int price = tab switch
try
{
StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
_ => throw new NotImplementedException()
};
totalPrice += item.Quantity * price;
}
catch (NotImplementedException e)
{
DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}");
int price = tab switch
{
StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
_ => throw new NotImplementedException()
};
totalPrice += item.Quantity * price;
}
catch (NotImplementedException e)
{
DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}");
}
}
}
@@ -1287,7 +1302,7 @@ namespace Barotrauma
private void SortItems(GUIListBox list, SortingMethod sortingMethod)
{
if (CurrentLocation == null) { return; }
if (CurrentLocation == null || ActiveStore == null) { return; }
if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc)
{
@@ -1662,13 +1677,15 @@ namespace Barotrauma
{
OwnedItems.Clear();
if (ActiveStore == null) { return; }
// Add items on the sub(s)
if (Submarine.MainSub?.GetItems(true) is List<Item> subItems)
{
foreach (var subItem in subItems)
{
if (!subItem.Components.All(c => !(c is Holdable h) || !h.Attachable || !h.Attached)) { continue; }
if (!subItem.Components.All(c => !(c is Wire w) || w.Connections.All(c => c == null))) { continue; }
if (!subItem.Components.All(c => c is not Holdable h || !h.Attachable || !h.Attached)) { continue; }
if (!subItem.Components.All(c => c is not Wire w || w.Connections.All(c => c == null))) { continue; }
if (!ItemAndAllContainersInteractable(subItem)) { continue; }
AddOwnedItem(subItem);
}
@@ -1701,7 +1718,7 @@ namespace Barotrauma
void AddOwnedItem(Item item)
{
if (!(item?.Prefab.GetPriceInfo(ActiveStore) is PriceInfo priceInfo)) { return; }
if (item?.Prefab.GetPriceInfo(ActiveStore) is not PriceInfo priceInfo) { return; }
bool isNonEmpty = !priceInfo.DisplayNonEmpty || item.ConditionPercentage > 5.0f;
if (OwnedItems.TryGetValue(item.Prefab, out ItemQuantity itemQuantity))
{
@@ -1729,7 +1746,7 @@ namespace Barotrauma
private void SetItemFrameStatus(GUIComponent itemFrame, bool enabled)
{
if (!(itemFrame?.UserData is PurchasedItem pi)) { return; }
if (itemFrame?.UserData is not PurchasedItem pi) { return; }
bool refreshFrameStatus = !pi.IsStoreComponentEnabled.HasValue || pi.IsStoreComponentEnabled.Value != enabled;
if (!refreshFrameStatus) { return; }
if (itemFrame.FindChild("icon", recursive: true) is GUIImage icon)
@@ -1841,11 +1858,7 @@ namespace Barotrauma
LocalizedString toolTip = string.Empty;
if (purchasedItem.ItemPrefab != null)
{
toolTip = purchasedItem.ItemPrefab.Name;
if (!purchasedItem.ItemPrefab.Description.IsNullOrEmpty())
{
toolTip += $"\n{purchasedItem.ItemPrefab.Description}";
}
toolTip = purchasedItem.ItemPrefab.GetTooltip();
if (itemQuantity != null)
{
if (itemQuantity.AllNonEmpty)
@@ -1859,7 +1872,7 @@ namespace Barotrauma
}
}
}
itemComponent.ToolTip = toolTip;
itemComponent.ToolTip = RichString.Rich(toolTip);
}
if (ownedLabel != null)
{
@@ -1995,11 +2008,23 @@ namespace Barotrauma
int totalPrice = 0;
foreach (var item in itemsToPurchase)
{
if (item?.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo))
if (item is null) { continue; }
if (item.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo))
{
itemsToRemove.Add(item);
continue;
}
if (item.ItemPrefab.DefaultPrice.RequiresUnlock)
{
if (!CargoManager.HasUnlockedStoreItem(item.ItemPrefab))
{
itemsToRemove.Add(item);
continue;
}
}
totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo);
}
itemsToRemove.ForEach(i => itemsToPurchase.Remove(i));
@@ -2054,7 +2079,12 @@ namespace Barotrauma
private void SetShoppingCrateTotalText()
{
if (IsBuying)
if (ActiveStore == null)
{
shoppingCrateTotal.Text = TextManager.FormatCurrency(0);
shoppingCrateTotal.TextColor = Color.White;
}
else if (IsBuying)
{
shoppingCrateTotal.Text = TextManager.FormatCurrency(buyTotal);
shoppingCrateTotal.TextColor = Balance < buyTotal ? Color.Red : Color.White;
@@ -2074,7 +2104,11 @@ namespace Barotrauma
private void SetConfirmButtonBehavior()
{
if (IsBuying)
if (ActiveStore == null)
{
confirmButton.OnClicked = null;
}
else if (IsBuying)
{
confirmButton.ClickSound = GUISoundType.ConfirmTransaction;
confirmButton.Text = TextManager.Get("CampaignStore.Purchase");
@@ -2102,6 +2136,7 @@ namespace Barotrauma
private void SetConfirmButtonStatus()
{
confirmButton.Enabled =
ActiveStore != null &&
HasActiveTabPermissions() &&
ActiveShoppingCrateList.Content.RectTransform.Children.Any() &&
activeTab switch
@@ -2111,6 +2146,7 @@ namespace Barotrauma
StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance,
_ => false
};
confirmButton.Visible = ActiveStore != null;
}
private void SetClearAllButtonStatus()
@@ -2169,7 +2205,7 @@ namespace Barotrauma
{
needsRefresh = itemsToSellFromSub.Count != prevSubItems.Count ||
itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) ||
itemsToSellFromSub.Any(i => !(prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is PurchasedItem prev) || i.Quantity != prev.Quantity) ||
itemsToSellFromSub.Any(i => prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is not PurchasedItem prev || i.Quantity != prev.Quantity) ||
prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab));
}
}
@@ -2184,29 +2220,32 @@ namespace Barotrauma
prevBalance = currBalance;
}
}
if (needsItemsToSellRefresh)
if (ActiveStore != null)
{
RefreshItemsToSell();
}
if (needsItemsToSellFromSubRefresh)
{
RefreshItemsToSellFromSub();
}
if (needsRefresh)
{
Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy))
{
RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell))
{
RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub))
{
RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f);
if (needsItemsToSellRefresh)
{
RefreshItemsToSell();
}
if (needsItemsToSellFromSubRefresh)
{
RefreshItemsToSellFromSub();
}
if (needsRefresh)
{
Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy))
{
RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell))
{
RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f);
}
if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub))
{
RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f);
}
}
updateStopwatch.Stop();
@@ -472,7 +472,7 @@ namespace Barotrauma
if (transferService)
{
subsToShow.AddRange(GameMain.GameSession.OwnedSubmarines);
subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass));
subsToShow.Sort(ComparePrice);
string currentSubName = CurrentOrPendingSubmarine().Name;
int currentIndex = subsToShow.FindIndex(s => s.Name == currentSubName);
if (currentIndex != -1)
@@ -484,7 +484,11 @@ namespace Barotrauma
{
subsToShow.AddRange((GameMain.Client is null ? SubmarineInfo.SavedSubmarines : MultiPlayerCampaign.GetCampaignSubs())
.Where(s => s.IsCampaignCompatible && !GameMain.GameSession.OwnedSubmarines.Any(os => os.Name == s.Name)));
subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass));
if (GameMain.GameSession.Campaign?.Map?.CurrentLocation is Location currentLocation)
{
subsToShow.RemoveAll(sub => !currentLocation.IsSubmarineAvailable(sub));
}
subsToShow.Sort(ComparePrice);
}
if (transferService)
@@ -492,10 +496,14 @@ namespace Barotrauma
SetConfirmButtonState(selectedSubmarine != null && selectedSubmarine.Name != CurrentOrPendingSubmarine().Name);
}
subsToShow.Sort((x, y) => x.SubmarineClass.CompareTo(y.SubmarineClass));
pageCount = Math.Max(1, (int)Math.Ceiling(subsToShow.Count / (float)submarinesPerPage));
UpdatePaging();
ContentRefreshRequired = false;
static int ComparePrice(SubmarineInfo x, SubmarineInfo y)
{
return x.Price.CompareTo(y.Price) * 100 + x.Name.CompareTo(y.Name);
}
}
private SubmarineInfo GetSubToDisplay(int index)
@@ -673,7 +681,7 @@ namespace Barotrauma
{
if (GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null)
{
return Submarine.MainSub.Info;
return Submarine.MainSub?.Info;
}
else
{
@@ -34,7 +34,7 @@ namespace Barotrauma
private List<CharacterTeamType> teamIDs;
private const string inLobbyString = "\u2022 \u2022 \u2022";
private GUIFrame pendingChangesFrame = null;
public static GUIFrame PendingChangesFrame = null;
public static Color OwnCharacterBGColor = Color.Gold * 0.7f;
private bool isTransferMenuOpen;
@@ -44,6 +44,7 @@ namespace Barotrauma
private float transferMenuOpenState;
private bool transferMenuStateCompleted;
private readonly HashSet<Identifier> registeredEvents = new HashSet<Identifier>();
private readonly TalentMenu talentMenu = new TalentMenu();
private class LinkedGUI
{
@@ -206,15 +207,8 @@ namespace Barotrauma
transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height);
}
GameSession.UpdateTalentNotificationIndicator(talentPointNotification);
if (Character.Controlled?.Info is { } characterInfo && talentResetButton != null && talentApplyButton != null)
{
int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
talentResetButton.Enabled = talentApplyButton.Enabled = talentCount > 0;
if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
{
talentApplyButton.Flash(GUIStyle.Orange);
}
}
talentMenu?.Update();
if (SelectedTab != InfoFrameTab.Crew) { return; }
if (linkedGUIList == null) { return; }
@@ -325,11 +319,11 @@ namespace Barotrauma
AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8))
}, style: null);
pendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null);
PendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null);
if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false)
{
NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame);
NetLobbyScreen.CreateChangesPendingFrame(PendingChangesFrame);
}
SetBalanceText(balanceText, campaignMode.Bank.Balance);
@@ -403,7 +397,7 @@ namespace Barotrauma
CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub);
break;
case InfoFrameTab.Talents:
CreateCharacterInfo(infoFrameHolder);
talentMenu.CreateGUI(infoFrameHolder, Character.Controlled ?? GameMain.Client?.Character);
break;
}
}
@@ -957,16 +951,26 @@ namespace Barotrauma
if (character != null)
{
if (GameMain.Client == null)
if (GameMain.Client is null)
{
GUIComponent preview = character.Info.CreateInfoFrame(background, false, null);
}
else
{
GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character)));
GameMain.Client.SelectCrewCharacter(character, preview);
if (!character.IsBot && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { CreateWalletFrame(background, character, mpCampaign); }
}
if (background.FindChild(TalentMenu.ManageBotTalentsButtonUserData, recursive: true) is GUIButton { Enabled: true } talentButton)
{
talentButton.OnClicked = (button, o) =>
{
talentMenu.CreateGUI(infoFrameHolder, character);
return true;
};
}
}
else if (client != null)
{
@@ -1487,7 +1491,11 @@ namespace Barotrauma
GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = (int)(0.0245f * missionFrame.Rect.Height);
GUIFrame missionFrameContent = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, missionFrame.Rect.Height - padding * 2), infoFrame.RectTransform, Anchor.Center), style: null);
Location location = GameMain.GameSession.EndLocation ?? GameMain.GameSession.StartLocation;
Location location = GameMain.GameSession.StartLocation;
if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
{
location ??= GameMain.GameSession.EndLocation;
}
GUILayoutGroup locationInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), missionFrameContent.RectTransform))
{
@@ -1774,373 +1782,10 @@ namespace Barotrauma
sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font, includeTitle: false, includeClass: false, includeDescription: true);
}
}
private Color unselectedColor = new Color(240, 255, 255, 225);
private Color unselectableColor = new Color(100, 100, 100, 225);
private Color pressedColor = new Color(60, 60, 60, 225);
private readonly List<(GUIButton button, GUIComponent icon)> talentButtons = new List<(GUIButton button, GUIComponent icon)>();
private readonly List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)> talentCornerIcons = new List<(Identifier talentTree, int index, GUIImage icon, GUIFrame background, GUIFrame backgroundGlow)>();
private List<Identifier> selectedTalents = new List<Identifier>();
private GUITextBlock experienceText;
private GUIProgressBar experienceBar;
private GUITextBlock talentPointText;
private GUIListBox skillListBox;
private GUIButton talentApplyButton,
talentResetButton;
private GUIImage talentPointNotification;
private readonly ImmutableDictionary<TalentTree.TalentTreeStageState, GUIComponentStyle> talentStageStyles = new Dictionary<TalentTree.TalentTreeStageState, GUIComponentStyle>
{
{ TalentTree.TalentTreeStageState.Invalid, GUIStyle.GetComponentStyle("TalentTreeLocked") },
{ TalentTree.TalentTreeStageState.Locked, GUIStyle.GetComponentStyle("TalentTreeLocked") },
{ TalentTree.TalentTreeStageState.Unlocked, GUIStyle.GetComponentStyle("TalentTreePurchased") },
{ TalentTree.TalentTreeStageState.Available, GUIStyle.GetComponentStyle("TalentTreeUnlocked") },
{ TalentTree.TalentTreeStageState.Highlighted, GUIStyle.GetComponentStyle("TalentTreeAvailable") },
}.ToImmutableDictionary();
private readonly ImmutableDictionary<TalentTree.TalentTreeStageState, Color> talentStageBackgroundColors = new Dictionary<TalentTree.TalentTreeStageState, Color>
{
{ TalentTree.TalentTreeStageState.Invalid, new Color(48,48,48,255) },
{ TalentTree.TalentTreeStageState.Locked, new Color(48,48,48,255) },
{ TalentTree.TalentTreeStageState.Unlocked, new Color(24,37,31,255) },
{ TalentTree.TalentTreeStageState.Available, new Color(50,47,33,255) },
{ TalentTree.TalentTreeStageState.Highlighted, new Color(50,47,33,255) },
}.ToImmutableDictionary();
private void CreateCharacterInfo(GUIFrame infoFrame)
{
infoFrame.ClearChildren();
talentButtons.Clear();
talentCornerIcons.Clear();
GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = GUI.IntScale(15);
GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), infoFrame.RectTransform, Anchor.Center), style: null);
GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null);
GUIFrame characterSettingsFrame = null;
GUILayoutGroup characterLayout = null;
if (!(GameMain.NetworkMember is null))
{
characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, frame.RectTransform), style: null) { Visible = false };
characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform));
GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null);
GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null);
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
}
Character controlledCharacter = Character.Controlled;
CharacterInfo info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo;
if (info == null) { return; }
Job job = info.Job;
GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter)
{
AbsoluteSpacing = GUI.IntScale(10),
Stretch = true
};
GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), contentLayout.RectTransform, Anchor.Center), isHorizontal: true);
new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) =>
{
float posY = component.Rect.Center.Y - component.Rect.Width / 2;
info.DrawPortrait(batch, new Vector2(component.Rect.X, posY), Vector2.Zero, component.Rect.Width, false, false);
});
GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform))
{
AbsoluteSpacing = GUI.IntScale(5),
CanBeFocused = true
};
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
if (!info.OmitJobInMenus)
{
nameBlock.TextColor = job.Prefab.UIColor;
GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor };
}
if (info.PersonalityTrait != null)
{
LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName);
Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString);
GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont);
traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint();
}
IEnumerable<TalentPrefab> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e));
if (talentsOutsideTree.Count() > 0)
{
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), nameLayout.RectTransform), style: null);
GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter);
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont);
talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y);
var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.8f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true)
{
AutoHideScrollBar = false,
ResizeContentToMakeSpaceForScrollBar = false
};
extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter);
extraTalentList.RectTransform.MinSize = new Point(0, GUI.IntScale(65));
extraTalentLayout.Recalculate();
extraTalentList.ForceLayoutRecalculation();
foreach (var extraTalent in talentsOutsideTree)
{
var img = new GUIImage(new RectTransform(new Point(extraTalentList.Content.Rect.Height), extraTalentList.Content.RectTransform), sprite: extraTalent.Icon, scaleToFit: true)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + extraTalent.Description),
Color = GUIStyle.Green
};
img.RectTransform.SizeChanged += () =>
{
img.RectTransform.MaxSize = new Point(img.Rect.Height);
};
}
}
GUILayoutGroup skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight)
{
AbsoluteSpacing = GUI.IntScale(5),
Stretch = true
};
GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont);
skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null);
CreateSkillList(controlledCharacter, info, skillListBox);
new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine");
GUIListBox talentTreeListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.6f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null);
if (controlledCharacter == null)
{
talentTreeListBox.Enabled = false;
}
else
{
if (!TalentTree.JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; }
selectedTalents = info.GetUnlockedTalentsInTree().ToList();
List<GUITextBlock> subTreeNames = new List<GUITextBlock>();
foreach (var subTree in talentTree.TalentSubTrees)
{
GUIFrame subTreeFrame = new GUIFrame(new RectTransform(new Vector2(0.333f, 1f), talentTreeListBox.Content.RectTransform, anchor: Anchor.TopLeft), style: null);
GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 1f), subTreeFrame.RectTransform, Anchor.Center), false, childAnchor: Anchor.TopCenter);
GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.111f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null);
int elementPadding = GUI.IntScale(8);
Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize;
GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader");
subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center));
for (int i = 0; i < 4; i++)
{
GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.222f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter), style: null);
Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize;
GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center), style: "TalentBackground")
{
Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked]
};
GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false };
GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null)
{
CanBeFocused = false,
Color = talentStageBackgroundColors[TalentTree.TalentTreeStageState.Locked]
};
Point iconSize = cornerIcon.RectTransform.NonScaledSize;
cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2);
if (subTree.TalentOptionStages.Length <= i) { continue; }
TalentOption talentOption = subTree.TalentOptionStages[i];
GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 0.7f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
foreach (Identifier talentId in talentOption.TalentIdentifiers.OrderBy(t => t))
{
if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab talent)) { continue; }
GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentOptionLayoutGroup.RectTransform), style: null)
{
CanBeFocused = false
};
GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null);
GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + talent.Description),
UserData = talent.Identifier,
PressedColor = pressedColor,
Enabled = controlledCharacter != null,
OnClicked = (button, userData) =>
{
// deselect other buttons in tier by removing their selected talents from pool
foreach (GUIButton guiButton in talentOptionLayoutGroup.GetAllChildren<GUIButton>())
{
if (guiButton.UserData is Identifier otherTalentIdentifier && guiButton != button)
{
if (!controlledCharacter.HasTalent(otherTalentIdentifier))
{
selectedTalents.Remove(otherTalentIdentifier);
}
}
}
Identifier talentIdentifier = (Identifier)userData;
if (TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents))
{
if (!selectedTalents.Contains(talentIdentifier))
{
selectedTalents.Add(talentIdentifier);
}
}
else if (!controlledCharacter.HasTalent(talentIdentifier))
{
selectedTalents.Remove(talentIdentifier);
}
UpdateTalentInfo();
return true;
},
};
talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
GUIComponent iconImage;
if (talent.Icon is null)
{
iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null)
{
OutlineColor = GUIStyle.Red,
TextColor = GUIStyle.Red,
PressedColor = unselectableColor,
DisabledColor = unselectableColor,
CanBeFocused = false,
};
}
else
{
iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true)
{
PressedColor = unselectableColor,
DisabledColor = unselectableColor * 0.5f,
CanBeFocused = false,
};
}
iconImage.Enabled = talentButton.Enabled;
talentButtons.Add((talentButton, iconImage));
}
talentCornerIcons.Add((subTree.Identifier, i, cornerIcon, talentBackground, talentBackgroundHighlight));
}
}
GUITextBlock.AutoScaleAndNormalize(subTreeNames);
GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), contentLayout.RectTransform, Anchor.TopCenter), isHorizontal: true)
{
RelativeSpacing = 0.01f,
Stretch = true
};
GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform));
GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null);
experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft),
barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green)
{
IsHorizontal = true,
};
experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
{
Shadow = true,
ToolTip = TextManager.Get("experiencetooltip")
};
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight) { AutoScaleVertical = true };
talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale")
{
OnClicked = ResetTalentSelection
};
talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale")
{
OnClicked = ApplyTalentSelection,
};
GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
}
if (!(GameMain.NetworkMember is null))
{
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")
{
IgnoreLayoutGroups = false
};
newCharacterBox.TextBlock.AutoScaleHorizontal = true;
newCharacterBox.OnClicked = (button, o) =>
{
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
{
newCharacterBox.Text = TextManager.Get("settings");
if (pendingChangesFrame != null)
{
NetLobbyScreen.CreateChangesPendingFrame(pendingChangesFrame);
}
OpenMenu();
});
return true;
}
OpenMenu();
return true;
void OpenMenu()
{
characterSettingsFrame!.Visible = true;
content.Visible = false;
}
};
if (!(characterLayout is null))
{
GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter);
new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages?
{
OnClicked = (button, o) =>
{
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
characterSettingsFrame!.Visible = false;
content.Visible = true;
return true;
}
};
}
}
UpdateTalentInfo();
}
private void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
public static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
{
parent.Content.ClearChildren();
List<GUITextBlock> skillNames = new List<GUITextBlock>();
@@ -2154,10 +1799,10 @@ namespace Barotrauma
new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.TopRight);
float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level;
float modifiedSkillLevel = MathF.Floor(character?.GetSkillLevel(skill.Identifier) ?? skill.Level);
if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level)))
{
int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level);
int skillChange = (int)MathF.Floor(modifiedSkillLevel - MathF.Floor(skill.Level));
string stringColor = skillChange switch
{
> 0 => XMLExtensions.ToStringHex(GUIStyle.Green),
@@ -2168,123 +1813,17 @@ namespace Barotrauma
RichString changeText = RichString.Rich($"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)");
new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText) { Padding = Vector4.Zero };
}
//skillContainer.Recalculate();
skillContainer.Recalculate();
}
parent.RecalculateChildren();
GUITextBlock.AutoScaleAndNormalize(skillNames);
}
private void UpdateTalentInfo()
{
Character controlledCharacter = Character.Controlled;
if (controlledCharacter?.Info == null) { return; }
if (SelectedTab != InfoFrameTab.Talents) { return; }
bool unlockedAllTalents = controlledCharacter.HasUnlockedAllTalents();
if (unlockedAllTalents)
{
experienceText.Text = string.Empty;
experienceBar.BarSize = 1f;
}
else
{
experienceText.Text = $"{controlledCharacter.Info.ExperiencePoints - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()} / {controlledCharacter.Info.GetExperienceRequiredToLevelUp() - controlledCharacter.Info.GetExperienceRequiredForCurrentLevel()}";
experienceBar.BarSize = controlledCharacter.Info.GetProgressTowardsNextLevel();
}
selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents);
string pointsLeft = controlledCharacter.Info.GetAvailableTalentPoints().ToString();
int talentCount = selectedTalents.Count - controlledCharacter.Info.GetUnlockedTalentsInTree().Count();
if (unlockedAllTalents)
{
talentPointText.SetRichText($"‖color:{XMLExtensions.ToStringHex(Color.Gray)}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖");
}
else if (talentCount > 0)
{
string pointsUsed = $"‖color:{XMLExtensions.ColorToString(GUIStyle.Red)}‖{-talentCount}‖color:end‖";
LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed));
talentPointText.SetRichText(localizedString);
}
else
{
talentPointText.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft));
}
foreach (var (talentTree, index, icon, frame, glow) in talentCornerIcons)
{
TalentTree.TalentTreeStageState state = TalentTree.GetTalentOptionStageState(controlledCharacter, talentTree, index, selectedTalents);
GUIComponentStyle newStyle = talentStageStyles[state];
icon.ApplyStyle(newStyle);
icon.Color = newStyle.Color;
frame.Color = talentStageBackgroundColors[state];
glow.Visible = state == TalentTree.TalentTreeStageState.Highlighted;
}
foreach (var talentButton in talentButtons)
{
Identifier talentIdentifier = (Identifier)talentButton.button.UserData;
bool unselectable = !TalentTree.IsViableTalentForCharacter(controlledCharacter, talentIdentifier, selectedTalents) || controlledCharacter.HasTalent(talentIdentifier);
Color newTalentColor = unselectable ? unselectableColor : unselectedColor;
Color hoverColor = Color.White;
if (controlledCharacter.HasTalent(talentIdentifier))
{
newTalentColor = GUIStyle.Green;
}
else if (selectedTalents.Contains(talentIdentifier))
{
newTalentColor = GUIStyle.Orange;
hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f);
}
talentButton.icon.Color = newTalentColor;
talentButton.icon.HoverColor = hoverColor;
}
CreateSkillList(controlledCharacter, controlledCharacter.Info, skillListBox);
}
private void ApplyTalents(Character controlledCharacter)
{
selectedTalents = TalentTree.CheckTalentSelection(controlledCharacter, selectedTalents);
foreach (Identifier talent in selectedTalents)
{
controlledCharacter.GiveTalent(talent);
if (GameMain.Client != null)
{
GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData());
}
}
selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList();
UpdateTalentInfo();
}
private bool ApplyTalentSelection(GUIButton guiButton, object userData)
{
Character controlledCharacter = Character.Controlled;
ApplyTalents(controlledCharacter);
return true;
}
private bool ResetTalentSelection(GUIButton guiButton, object userData)
{
Character controlledCharacter = Character.Controlled;
if (controlledCharacter?.Info == null) { return false; }
selectedTalents = controlledCharacter.Info.GetUnlockedTalentsInTree().ToList();
UpdateTalentInfo();
return true;
}
public void OnExperienceChanged(Character character)
{
if (character != Character.Controlled) { return; }
UpdateTalentInfo();
talentMenu.UpdateTalentInfo();
}
public void OnClose()
@@ -0,0 +1,817 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using static Barotrauma.TalentTree;
using static Barotrauma.TalentTree.TalentStages;
namespace Barotrauma
{
internal readonly record struct TalentShowCaseButton(ImmutableHashSet<TalentButton> Buttons,
GUIComponent IconComponent);
internal readonly record struct TalentButton(GUIComponent IconComponent,
TalentPrefab Prefab)
{
public Identifier Identifier => Prefab.Identifier;
}
internal readonly record struct TalentCornerIcon(Identifier TalentTree,
int Index,
GUIImage IconComponent,
GUIFrame BackgroundComponent,
GUIFrame GlowComponent);
internal readonly struct TalentTreeStyle
{
public readonly GUIComponentStyle ComponentStyle;
public readonly Color Color;
public TalentTreeStyle(string componentStyle, Color color)
{
ComponentStyle = GUIStyle.GetComponentStyle(componentStyle);
Color = color;
}
}
internal sealed class TalentMenu
{
public const string ManageBotTalentsButtonUserData = "managebottalentsbutton";
private Character? character;
private CharacterInfo? characterInfo;
private static readonly Color unselectedColor = new Color(240, 255, 255, 225),
unselectableColor = new Color(100, 100, 100, 225),
pressedColor = new Color(60, 60, 60, 225),
lockedColor = new Color(48, 48, 48, 255),
unlockedColor = new Color(24, 37, 31, 255),
availableColor = new Color(50, 47, 33, 255);
private static readonly ImmutableDictionary<TalentStages, TalentTreeStyle> talentStageStyles =
new Dictionary<TalentStages, TalentTreeStyle>
{
[Invalid] = new TalentTreeStyle("TalentTreeLocked", lockedColor),
[Locked] = new TalentTreeStyle("TalentTreeLocked", lockedColor),
[Unlocked] = new TalentTreeStyle("TalentTreePurchased", unlockedColor),
[Available] = new TalentTreeStyle("TalentTreeUnlocked", availableColor),
[Highlighted] = new TalentTreeStyle("TalentTreeAvailable", availableColor)
}.ToImmutableDictionary();
private readonly HashSet<TalentButton> talentButtons = new HashSet<TalentButton>();
private readonly HashSet<TalentShowCaseButton> talentShowCaseButtons = new HashSet<TalentShowCaseButton>();
private readonly HashSet<GUIComponent> showCaseTalentFrames = new HashSet<GUIComponent>();
private readonly HashSet<TalentCornerIcon> talentCornerIcons = new HashSet<TalentCornerIcon>();
private HashSet<Identifier> selectedTalents = new HashSet<Identifier>();
private readonly Queue<Identifier> showCaseClosureQueue = new();
private GUIListBox? skillListBox;
private GUITextBlock? talentPointText;
private GUIProgressBar? experienceBar;
private GUITextBlock? experienceText;
private GUILayoutGroup? skillLayout;
private GUIButton? talentApplyButton,
talentResetButton;
public void CreateGUI(GUIFrame parent, Character? targetCharacter)
{
parent.ClearChildren();
talentButtons.Clear();
talentShowCaseButtons.Clear();
talentCornerIcons.Clear();
showCaseTalentFrames.Clear();
character = targetCharacter;
characterInfo = targetCharacter?.Info;
GUIFrame background = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
int padding = GUI.IntScale(15);
GUIFrame frame = new GUIFrame(new RectTransform(new Point(background.Rect.Width - padding, background.Rect.Height - padding), parent.RectTransform, Anchor.Center), style: null);
GUIFrame content = new GUIFrame(new RectTransform(new Vector2(0.98f), frame.RectTransform, Anchor.Center), style: null);
GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, content.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter)
{
AbsoluteSpacing = GUI.IntScale(10),
Stretch = true
};
if (characterInfo is null) { return; }
CreateStatPanel(contentLayout, characterInfo);
new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine");
if (JobTalentTrees.TryGet(characterInfo.Job.Prefab.Identifier, out TalentTree? talentTree))
{
CreateTalentMenu(contentLayout, characterInfo, talentTree!);
}
CreateFooter(contentLayout, characterInfo);
UpdateTalentInfo();
if (GameMain.NetworkMember != null && IsOwnCharacter(characterInfo))
{
CreateMultiplayerCharacterSettings(frame, content);
}
}
private void CreateMultiplayerCharacterSettings(GUIComponent parent, GUIComponent content)
{
if (skillLayout is null) { return; }
GUIFrame characterSettingsFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null) { Visible = false };
GUILayoutGroup characterLayout = new GUILayoutGroup(new RectTransform(Vector2.One, characterSettingsFrame.RectTransform));
GUIFrame containerFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.9f), characterLayout.RectTransform), style: null);
GUIFrame playerFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.7f), containerFrame.RectTransform, Anchor.Center), style: null);
GameMain.NetLobbyScreen.CreatePlayerFrame(playerFrame, alwaysAllowEditing: true, createPendingText: false);
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")
{
IgnoreLayoutGroups = false,
TextBlock =
{
AutoScaleHorizontal = true
}
};
newCharacterBox.OnClicked = (button, o) =>
{
if (!GameMain.NetLobbyScreen.CampaignCharacterDiscarded)
{
GameMain.NetLobbyScreen.TryDiscardCampaignCharacter(() =>
{
newCharacterBox.Text = TextManager.Get("settings");
if (TabMenu.PendingChangesFrame != null)
{
NetLobbyScreen.CreateChangesPendingFrame(TabMenu.PendingChangesFrame);
}
OpenMenu();
});
return true;
}
OpenMenu();
return true;
void OpenMenu()
{
characterSettingsFrame!.Visible = true;
content.Visible = false;
}
};
GUILayoutGroup characterCloseButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), characterLayout.RectTransform), childAnchor: Anchor.BottomCenter);
new GUIButton(new RectTransform(new Vector2(0.4f, 1f), characterCloseButtonLayout.RectTransform), TextManager.Get("ApplySettingsButton")) //TODO: Is this text appropriate for this circumstance for all languages?
{
OnClicked = (button, o) =>
{
GameMain.Client?.SendCharacterInfo(GameMain.Client.PendingName);
characterSettingsFrame.Visible = false;
content.Visible = true;
return true;
}
};
}
private void CreateStatPanel(GUIComponent parent, CharacterInfo info)
{
Job job = info.Job;
GUILayoutGroup topLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), parent.RectTransform, Anchor.Center), isHorizontal: true);
new GUICustomComponent(new RectTransform(new Vector2(0.25f, 1f), topLayout.RectTransform), onDraw: (batch, component) =>
{
info.DrawPortrait(batch, component.Rect.Location.ToVector2(), Vector2.Zero, component.Rect.Width, false, false);
});
GUILayoutGroup nameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1f), topLayout.RectTransform))
{
AbsoluteSpacing = GUI.IntScale(5),
CanBeFocused = true
};
GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), info.Name, font: GUIStyle.SubHeadingFont);
if (!info.OmitJobInMenus)
{
nameBlock.TextColor = job.Prefab.UIColor;
GUITextBlock jobBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), nameLayout.RectTransform), job.Name, font: GUIStyle.SmallFont) { TextColor = job.Prefab.UIColor };
}
if (info.PersonalityTrait != null)
{
LocalizedString traitString = TextManager.AddPunctuation(':', TextManager.Get("PersonalityTrait"), info.PersonalityTrait.DisplayName);
Vector2 traitSize = GUIStyle.SmallFont.MeasureString(traitString);
GUITextBlock traitBlock = new GUITextBlock(new RectTransform(Vector2.One, nameLayout.RectTransform), traitString, font: GUIStyle.SmallFont);
traitBlock.RectTransform.NonScaledSize = traitSize.Pad(traitBlock.Padding).ToPoint();
}
ImmutableHashSet<TalentPrefab?> talentsOutsideTree = info.GetUnlockedTalentsOutsideTree().Select(static e => TalentPrefab.TalentPrefabs.Find(c => c.Identifier == e)).ToImmutableHashSet();
if (talentsOutsideTree.Any())
{
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), nameLayout.RectTransform), style: null);
GUILayoutGroup extraTalentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.55f), nameLayout.RectTransform), childAnchor: Anchor.TopCenter);
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), extraTalentLayout.RectTransform, anchor: Anchor.Center), TextManager.Get("talentmenu.extratalents"), font: GUIStyle.SubHeadingFont)
{
AutoScaleVertical = true
};
talentPointText.RectTransform.MaxSize = new Point(int.MaxValue, (int)talentPointText.TextSize.Y);
var extraTalentList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.7f), extraTalentLayout.RectTransform, anchor: Anchor.Center), isHorizontal: true)
{
AutoHideScrollBar = false,
ResizeContentToMakeSpaceForScrollBar = false
};
extraTalentList.ScrollBar.RectTransform.SetPosition(Anchor.BottomCenter, Pivot.TopCenter);
extraTalentLayout.Recalculate();
extraTalentList.ForceLayoutRecalculation();
foreach (var extraTalent in talentsOutsideTree)
{
if (extraTalent is null) { continue; }
GUIImage talentImg = new GUIImage(new RectTransform(Vector2.One, extraTalentList.Content.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite: extraTalent.Icon, scaleToFit: true)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{extraTalent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(extraTalent.Description.Value)),
Color = GUIStyle.Green
};
}
}
skillLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1f), topLayout.RectTransform), childAnchor: Anchor.TopRight)
{
AbsoluteSpacing = GUI.IntScale(5),
Stretch = true
};
GUITextBlock skillBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillLayout.RectTransform), TextManager.Get("skills"), font: GUIStyle.SubHeadingFont);
skillListBox = new GUIListBox(new RectTransform(new Vector2(1f, 1f - skillBlock.RectTransform.RelativeSize.Y), skillLayout.RectTransform), style: null);
TabMenu.CreateSkillList(info.Character, info, skillListBox);
}
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));
selectedTalents = info.GetUnlockedTalentsInTree().ToHashSet();
var specializationCount = tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization);
List<GUITextBlock> subTreeNames = new List<GUITextBlock>();
foreach (var subTree in tree.TalentSubTrees)
{
GUIListBox talentList;
GUIComponent talentParent;
Vector2 treeSize;
switch (subTree.Type)
{
case TalentTreeType.Primary:
talentList = mainList;
treeSize = new Vector2(1f, 0.5f);
break;
case TalentTreeType.Specialization:
talentList = GetSpecializationList();
treeSize = new Vector2(Math.Max(0.333f, 1.0f / tree.TalentSubTrees.Count(t => t.Type == TalentTreeType.Specialization)), 1f);
break;
default:
throw new ArgumentOutOfRangeException($"Invalid TalentTreeType \"{subTree.Type}\"");
}
talentParent = talentList.Content;
GUILayoutGroup subTreeLayoutGroup = new GUILayoutGroup(new RectTransform(treeSize, talentParent.RectTransform), isHorizontal: false, childAnchor: Anchor.TopCenter)
{
Stretch = true
};
if (subTree.Type != TalentTreeType.Primary)
{
GUIFrame subtreeTitleFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), subTreeLayoutGroup.RectTransform, anchor: Anchor.TopCenter)
{ MinSize = new Point(0, GUI.IntScale(30)) }, style: null);
subtreeTitleFrame.RectTransform.IsFixedSize = true;
int elementPadding = GUI.IntScale(8);
Point headerSize = subtreeTitleFrame.RectTransform.NonScaledSize;
GUIFrame subTreeTitleBackground = new GUIFrame(new RectTransform(new Point(headerSize.X - elementPadding, headerSize.Y), subtreeTitleFrame.RectTransform, anchor: Anchor.Center), style: "SubtreeHeader");
subTreeNames.Add(new GUITextBlock(new RectTransform(Vector2.One, subTreeTitleBackground.RectTransform, anchor: Anchor.TopCenter), subTree.DisplayName, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center));
}
int optionAmount = subTree.TalentOptionStages.Length;
for (int i = 0; i < optionAmount; i++)
{
TalentOption option = subTree.TalentOptionStages[i];
CreateTalentOption(subTreeLayoutGroup, subTree, i, option, info, specializationCount);
}
subTreeLayoutGroup.RectTransform.Resize(new Point(subTreeLayoutGroup.Rect.Width,
subTreeLayoutGroup.Children.Sum(c => c.Rect.Height + subTreeLayoutGroup.AbsoluteSpacing)));
subTreeLayoutGroup.RectTransform.MinSize = new Point(subTreeLayoutGroup.Rect.Width, subTreeLayoutGroup.Rect.Height);
subTreeLayoutGroup.Recalculate();
if (subTree.Type == TalentTreeType.Specialization)
{
talentList.RectTransform.Resize(new Point(talentList.Rect.Width, Math.Max(subTreeLayoutGroup.Rect.Height, talentList.Rect.Height)));
talentList.RectTransform.MinSize = new Point(0, talentList.Rect.Height);
}
}
var specializationList = GetSpecializationList();
//resize (scale up) children if there's less than 3 of them to make them cover the whole width of the menu
specializationList.Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height),
resizeChildren: specializationCount < 3);
//make room for scrollbar if there's more than the default amount of specializations
if (specializationCount > 3)
{
specializationList.RectTransform.MinSize = new Point(specializationList.Rect.Width, specializationList.Content.Rect.Height + (int)(specializationList.ScrollBar.Rect.Height * 0.9f));
}
GUITextBlock.AutoScaleAndNormalize(subTreeNames);
GUIListBox GetSpecializationList()
{
if (mainList.Content.Children.LastOrDefault() is GUIListBox specList)
{
return specList;
}
GUIListBox newSpecializationList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), mainList.Content.RectTransform, Anchor.TopCenter), isHorizontal: true, style: null);
return newSpecializationList;
}
}
private void CreateTalentOption(GUIComponent parent, TalentSubTree subTree, int index, TalentOption talentOption, CharacterInfo info, int specializationCount)
{
int elementPadding = GUI.IntScale(8);
int height = GUI.IntScale((GameMain.GameSession?.Campaign == null ? 65 : 60) * (specializationCount > 3 ? 0.97f : 1.0f));
GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.01f), parent.RectTransform, anchor: Anchor.TopCenter)
{ MinSize = new Point(0, height) }, style: null);
Point talentFrameSize = talentOptionFrame.RectTransform.NonScaledSize;
GUIFrame talentBackground = new GUIFrame(new RectTransform(new Point(talentFrameSize.X - elementPadding, talentFrameSize.Y - elementPadding), talentOptionFrame.RectTransform, anchor: Anchor.Center),
style: "TalentBackground")
{
Color = talentStageStyles[Locked].Color
};
GUIFrame talentBackgroundHighlight = new GUIFrame(new RectTransform(Vector2.One, talentBackground.RectTransform, anchor: Anchor.Center), style: "TalentBackgroundGlow") { Visible = false };
GUIImage cornerIcon = new GUIImage(new RectTransform(new Vector2(0.2f), talentOptionFrame.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { MaxSize = new Point(16) }, style: null)
{
CanBeFocused = false,
Color = talentStageStyles[Locked].Color
};
Point iconSize = cornerIcon.RectTransform.NonScaledSize;
cornerIcon.RectTransform.AbsoluteOffset = new Point(iconSize.X / 2, iconSize.Y / 2);
GUILayoutGroup talentOptionCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 0.9f), talentOptionFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
GUILayoutGroup talentOptionLayoutGroup = new GUILayoutGroup(new RectTransform(Vector2.One, talentOptionCenterGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
HashSet<Identifier> talentOptionIdentifiers = talentOption.TalentIdentifiers.OrderBy(static t => t).ToHashSet();
HashSet<TalentButton> buttonsToAdd = new();
Dictionary<GUILayoutGroup, ImmutableHashSet<Identifier>> showCaseTalentParents = new();
Dictionary<Identifier, GUIComponent> showCaseTalentButtonsToAdd = new();
foreach (var (showCaseTalentIdentifier, talents) in talentOption.ShowCaseTalents)
{
talentOptionIdentifiers.Add(showCaseTalentIdentifier);
Point parentSize = talentBackground.RectTransform.NonScaledSize;
GUIFrame showCaseFrame = new GUIFrame(new RectTransform(new Point((int)(parentSize.X / 3f * (talents.Count - 1)), parentSize.Y)), style: "GUITooltip")
{
UserData = showCaseTalentIdentifier,
IgnoreLayoutGroups = true,
Visible = false
};
GUILayoutGroup showcaseCenterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.7f), showCaseFrame.RectTransform, Anchor.Center), childAnchor: Anchor.CenterLeft);
GUILayoutGroup showcaseLayout = new GUILayoutGroup(new RectTransform(Vector2.One, showcaseCenterGroup.RectTransform), isHorizontal: true) { Stretch = true };
showCaseTalentParents.Add(showcaseLayout, talents);
showCaseTalentFrames.Add(showCaseFrame);
}
foreach (Identifier talentId in talentOptionIdentifiers)
{
if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) { continue; }
bool isShowCaseTalent = talentOption.ShowCaseTalents.ContainsKey(talentId);
GUIComponent talentParent = talentOptionLayoutGroup;
foreach (var (key, value) in showCaseTalentParents)
{
if (value.Contains(talentId))
{
talentParent = key;
break;
}
}
GUIFrame talentFrame = new GUIFrame(new RectTransform(Vector2.One, talentParent.RectTransform), style: null)
{
CanBeFocused = false
};
GUIFrame croppedTalentFrame = new GUIFrame(new RectTransform(Vector2.One, talentFrame.RectTransform, anchor: Anchor.Center, scaleBasis: ScaleBasis.BothHeight), style: null);
GUIButton talentButton = new GUIButton(new RectTransform(Vector2.One, croppedTalentFrame.RectTransform, anchor: Anchor.Center), style: null)
{
ToolTip = RichString.Rich($"‖color:{Color.White.ToStringHex()}‖{talent.DisplayName}‖color:end‖" + "\n\n" + ToolBox.ExtendColorToPercentageSigns(talent.Description.Value)),
UserData = talent.Identifier,
PressedColor = pressedColor,
Enabled = info.Character != null,
OnClicked = (button, userData) =>
{
if (isShowCaseTalent)
{
foreach (GUIComponent component in showCaseTalentFrames)
{
if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == talentId)
{
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);
component.Visible = true;
}
else
{
component.Visible = false;
}
}
return true;
}
if (character is null) { return false; }
Identifier talentIdentifier = (Identifier)userData;
if (talentOption.MaxChosenTalents is 1)
{
// deselect other buttons in tier by removing their selected talents from pool
foreach (Identifier identifier in selectedTalents)
{
if (character.HasTalent(identifier) || identifier == talentId) { continue; }
if (talentOptionIdentifiers.Contains(identifier))
{
selectedTalents.Remove(identifier);
}
}
}
if (character.HasTalent(talentIdentifier))
{
return true;
}
else if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents))
{
if (!selectedTalents.Contains(talentIdentifier))
{
selectedTalents.Add(talentIdentifier);
}
else
{
selectedTalents.Remove(talentIdentifier);
}
}
else
{
selectedTalents.Remove(talentIdentifier);
}
UpdateTalentInfo();
return true;
},
};
talentButton.Color = talentButton.HoverColor = talentButton.PressedColor = talentButton.SelectedColor = talentButton.DisabledColor = Color.Transparent;
GUIComponent iconImage;
if (talent.Icon is null)
{
iconImage = new GUITextBlock(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), text: "???", font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: null)
{
OutlineColor = GUIStyle.Red,
TextColor = GUIStyle.Red,
PressedColor = unselectableColor,
DisabledColor = unselectableColor,
CanBeFocused = false,
};
}
else
{
iconImage = new GUIImage(new RectTransform(Vector2.One, talentButton.RectTransform, anchor: Anchor.Center), sprite: talent.Icon, scaleToFit: true)
{
Color = talent.ColorOverride.TryUnwrap(out Color color) ? color : Color.White,
PressedColor = unselectableColor,
DisabledColor = unselectableColor * 0.5f,
CanBeFocused = false,
};
}
iconImage.Enabled = talentButton.Enabled;
if (isShowCaseTalent)
{
showCaseTalentButtonsToAdd.Add(talentId, iconImage);
continue;
}
buttonsToAdd.Add(new TalentButton(iconImage, talent));
}
foreach (TalentButton button in buttonsToAdd)
{
talentButtons.Add(button);
}
foreach (var (key, value) in showCaseTalentButtonsToAdd)
{
HashSet<TalentButton> buttons = new();
foreach (Identifier identifier in talentOption.ShowCaseTalents[key])
{
if (talentButtons.FirstOrNull(talentButton => talentButton.Identifier == identifier) is not { } button) { continue; }
buttons.Add(button);
}
talentShowCaseButtons.Add(new TalentShowCaseButton(buttons.ToImmutableHashSet(), value));
}
talentCornerIcons.Add(new TalentCornerIcon(subTree.Identifier, index, cornerIcon, talentBackground, talentBackgroundHighlight));
}
private void CreateFooter(GUIComponent parent, CharacterInfo info)
{
GUILayoutGroup bottomLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.07f), parent.RectTransform, Anchor.TopCenter), isHorizontal: true)
{
RelativeSpacing = 0.01f,
Stretch = true
};
GUILayoutGroup experienceLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.59f, 1f), bottomLayout.RectTransform));
GUIFrame experienceBarFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.5f), experienceLayout.RectTransform), style: null);
experienceBar = new GUIProgressBar(new RectTransform(new Vector2(1f, 1f), experienceBarFrame.RectTransform, Anchor.CenterLeft),
barSize: info.GetProgressTowardsNextLevel(), color: GUIStyle.Green)
{
IsHorizontal = true,
};
experienceText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), experienceBarFrame.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.Font, textAlignment: Alignment.CenterRight)
{
Shadow = true,
ToolTip = TextManager.Get("experiencetooltip")
};
talentPointText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), experienceLayout.RectTransform, anchor: Anchor.Center), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight)
{ AutoScaleVertical = true };
talentResetButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("reset"), style: "GUIButtonFreeScale")
{
OnClicked = ResetTalentSelection
};
talentApplyButton = new GUIButton(new RectTransform(new Vector2(0.19f, 1f), bottomLayout.RectTransform), text: TextManager.Get("applysettingsbutton"), style: "GUIButtonFreeScale")
{
OnClicked = ApplyTalentSelection,
};
GUITextBlock.AutoScaleAndNormalize(talentResetButton.TextBlock, talentApplyButton.TextBlock);
}
private bool ResetTalentSelection(GUIButton guiButton, object userData)
{
if (characterInfo is null) { return false; }
selectedTalents = characterInfo.GetUnlockedTalentsInTree().ToHashSet();
UpdateTalentInfo();
return true;
}
private void ApplyTalents(Character controlledCharacter)
{
foreach (Identifier talent in CheckTalentSelection(controlledCharacter, selectedTalents))
{
controlledCharacter.GiveTalent(talent);
if (GameMain.Client != null)
{
GameMain.Client.CreateEntityEvent(controlledCharacter, new Character.UpdateTalentsEventData());
}
}
UpdateTalentInfo();
}
private bool ApplyTalentSelection(GUIButton guiButton, object userData)
{
if (character is null) { return false; }
ApplyTalents(character);
return true;
}
public void UpdateTalentInfo()
{
if (character is null || characterInfo is null) { return; }
bool unlockedAllTalents = character.HasUnlockedAllTalents();
if (experienceBar is null || experienceText is null) { return; }
if (unlockedAllTalents)
{
experienceText.Text = string.Empty;
experienceBar.BarSize = 1f;
}
else
{
experienceText.Text = $"{characterInfo.ExperiencePoints - characterInfo.GetExperienceRequiredForCurrentLevel()} / {characterInfo.GetExperienceRequiredToLevelUp() - characterInfo.GetExperienceRequiredForCurrentLevel()}";
experienceBar.BarSize = characterInfo.GetProgressTowardsNextLevel();
}
selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet();
string pointsLeft = characterInfo.GetAvailableTalentPoints().ToString();
int talentCount = selectedTalents.Count - characterInfo.GetUnlockedTalentsInTree().Count();
if (unlockedAllTalents)
{
talentPointText?.SetRichText($"‖color:{Color.Gray.ToStringHex()}‖{TextManager.Get("talentmenu.alltalentsunlocked")}‖color:end‖");
}
else if (talentCount > 0)
{
string pointsUsed = $"‖color:{XMLExtensions.ToStringHex(GUIStyle.Red)}‖{-talentCount}‖color:end‖";
LocalizedString localizedString = TextManager.GetWithVariables("talentmenu.points.spending", ("[amount]", pointsLeft), ("[used]", pointsUsed));
talentPointText?.SetRichText(localizedString);
}
else
{
talentPointText?.SetRichText(TextManager.GetWithVariable("talentmenu.points", "[amount]", pointsLeft));
}
foreach (TalentCornerIcon cornerIcon in talentCornerIcons)
{
TalentStages state = GetTalentOptionStageState(character, cornerIcon.TalentTree, cornerIcon.Index, selectedTalents);
TalentTreeStyle style = talentStageStyles[state];
GUIComponentStyle newStyle = style.ComponentStyle;
cornerIcon.IconComponent.ApplyStyle(newStyle);
cornerIcon.IconComponent.Color = newStyle.Color;
cornerIcon.BackgroundComponent.Color = style.Color;
cornerIcon.GlowComponent.Visible = state == Highlighted;
}
foreach (TalentButton talentButton in talentButtons)
{
TalentStages stage = GetTalentState(character, talentButton.Identifier, selectedTalents);
ApplyTalentIconColor(stage, talentButton.IconComponent, talentButton.Prefab.ColorOverride);
}
foreach (TalentShowCaseButton showCaseTalentButton in talentShowCaseButtons)
{
TalentStages collectiveTalentStage = GetCollectiveTalentState(character, showCaseTalentButton.Buttons, selectedTalents);
ApplyTalentIconColor(collectiveTalentStage, showCaseTalentButton.IconComponent, Option<Color>.None());
}
if (skillListBox is null) { return; }
TabMenu.CreateSkillList(character, characterInfo, skillListBox);
static TalentStages GetTalentState(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
{
bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier);
TalentStages stage = unselectable ? Locked : Available;
if (unselectable)
{
stage = Locked;
}
if (character.HasTalent(talentIdentifier))
{
stage = Unlocked;
}
else if (selectedTalents.Contains(talentIdentifier))
{
stage = Highlighted;
}
return stage;
}
static void ApplyTalentIconColor(TalentStages stage, GUIComponent component, Option<Color> colorOverride)
{
Color color = stage switch
{
Invalid => unselectableColor,
Locked => unselectableColor,
Unlocked => GetColorOrOverride(GUIStyle.Green, colorOverride),
Highlighted => GetColorOrOverride(GUIStyle.Orange, colorOverride),
Available => GetColorOrOverride(unselectedColor, colorOverride),
_ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null)
};
component.Color = color;
component.HoverColor = Color.Lerp(color, Color.White, 0.7f);
static Color GetColorOrOverride(Color color, Option<Color> colorOverride) => colorOverride.TryUnwrap(out Color overrideColor) ? overrideColor : color;
}
// this could also be reused for setting colors for talentCornerIcons but that's for another time
static TalentStages GetCollectiveTalentState(Character character, IReadOnlyCollection<TalentButton> buttons, IReadOnlyCollection<Identifier> selectedTalents)
{
HashSet<TalentStages> talentStages = new HashSet<TalentStages>();
foreach (TalentButton button in buttons)
{
talentStages.Add(GetTalentState(character, button.Identifier, selectedTalents));
}
TalentStages collectiveStage = talentStages.Any(static stage => stage is Locked)
? Locked
: Available;
foreach (TalentStages stage in talentStages)
{
if (stage is Highlighted)
{
collectiveStage = Highlighted;
break;
}
if (stage is Unlocked)
{
collectiveStage = Unlocked;
break;
}
}
return collectiveStage;
}
}
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;
if (talentApplyButton.Enabled && talentApplyButton.FlashTimer <= 0.0f)
{
talentApplyButton.Flash(GUIStyle.Orange);
}
while (showCaseClosureQueue.TryDequeue(out Identifier identifier))
{
foreach (GUIComponent component in showCaseTalentFrames)
{
if (component.UserData is Identifier showcaseIdentifier && showcaseIdentifier == identifier)
{
component.Visible = false;
}
}
}
bool mouseInteracted = PlayerInput.PrimaryMouseButtonClicked() || PlayerInput.SecondaryMouseButtonClicked() || PlayerInput.ScrollWheelSpeed != 0;
bool keyboardInteracted = PlayerInput.KeyHit(Keys.Escape) || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.InfoTab].IsHit();
foreach (GUIComponent component in showCaseTalentFrames)
{
if (component.UserData is not Identifier identifier) { continue; }
component.AddToGUIUpdateList(order: 1);
if (!component.Visible) { continue; }
if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition)))
{
showCaseClosureQueue.Enqueue(identifier);
}
}
}
private static bool IsOwnCharacter(CharacterInfo? info)
{
if (info is null) { return false; }
CharacterInfo? ownCharacterInfo = Character.Controlled?.Info ?? GameMain.Client?.CharacterInfo;
if (ownCharacterInfo is null) { return false; }
return info == ownCharacterInfo;
}
public static bool CanManageTalents(CharacterInfo targetInfo)
{
// in singleplayer we can do whatever we want
if (GameMain.IsSingleplayer) { return true; }
// always allow managing talents for own character
if (IsOwnCharacter(targetInfo)) { return true; }
// 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
return GameMain.Client is { } client && client.HasPermission(ClientPermissions.ManageBotTalents);
}
}
}
@@ -278,10 +278,13 @@ namespace Barotrauma
* | upgrades | maintenance | <- 1/3rd empty space |
* |---------------------------------------------------------------------------------------------------|
*/
GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.5f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f };
GUILayoutGroup leftLayout = new GUILayoutGroup(rectT(0.4f, 1, topHeaderLayout)) { RelativeSpacing = 0.05f };
GUILayoutGroup locationLayout = new GUILayoutGroup(rectT(1, 0.5f, leftLayout), isHorizontal: true);
GUIImage submarineIcon = new GUIImage(rectT(new Point(locationLayout.Rect.Height, locationLayout.Rect.Height), locationLayout), style: "SubmarineIcon", scaleToFit: true);
new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont);
var header = new GUITextBlock(rectT(1.0f - submarineIcon.RectTransform.RelativeSize.X, 1, locationLayout), TextManager.Get("UpgradeUI.Title"), font: GUIStyle.LargeFont);
header.RectTransform.MaxSize = new Point((int)(header.TextSize.X + header.Padding.X + header.Padding.Z), int.MaxValue);
new GUITextBlock(rectT(1.0f, 1, locationLayout), TextManager.Get("UpgradeUI.AllSubmarinesInfo"), font: GUIStyle.SmallFont, wrap: true);
categoryButtonLayout = new GUILayoutGroup(rectT(0.4f, 0.3f, leftLayout), isHorizontal: true) { Stretch = true };
GUIButton upgradeButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Upgrades"), style: "GUITabButton") { UserData = UpgradeTab.Upgrade, Selected = selectedUpgradeTab == UpgradeTab.Upgrade };
GUIButton repairButton = new GUIButton(rectT(1, 1f, categoryButtonLayout), TextManager.Get("UICategory.Maintenance"), style: "GUITabButton") { UserData = UpgradeTab.Repairs, Selected = selectedUpgradeTab == UpgradeTab.Repairs };
@@ -433,8 +436,8 @@ namespace Barotrauma
Location location = Campaign.Map.CurrentLocation;
int hullRepairCost = Campaign.GetHullRepairCost();
int itemRepairCost = Campaign.GetItemRepairCost();
int hullRepairCost = CampaignMode.GetHullRepairCost();
int itemRepairCost = CampaignMode.GetItemRepairCost();
int shuttleRetrieveCost = CampaignMode.ShuttleReplaceCost;
if (location != null)
{