Build 0.20.0.0

This commit is contained in:
Markus Isberg
2022-10-27 17:54:57 +03:00
parent 05c7b1f869
commit edaf4b09fe
197 changed files with 4344 additions and 1773 deletions

View File

@@ -442,8 +442,7 @@ namespace Barotrauma
{
foreach (Limb limb in Limbs)
{
if (limb == null || limb.IsSevered || limb.ActiveSprite == null) { continue; }
if (limb == null || limb.IsSevered || limb.ActiveSprite == null || !limb.DoesFlip) { continue; }
Vector2 spriteOrigin = limb.ActiveSprite.Origin;
spriteOrigin.X = limb.ActiveSprite.SourceRect.Width - spriteOrigin.X;
limb.ActiveSprite.Origin = spriteOrigin;
@@ -468,7 +467,10 @@ namespace Barotrauma
{
var damageSound = character.GetSound(s => s.Type == CharacterSound.SoundType.Damage);
float range = damageSound != null ? damageSound.Range * 2 : ConvertUnits.ToDisplayUnits(character.AnimController.Collider.GetSize().Length() * 10);
SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range);
if (!limbJoint.Params.BreakSound.IsNullOrEmpty() && !limbJoint.Params.BreakSound.Equals("none", StringComparison.OrdinalIgnoreCase))
{
SoundPlayer.PlayDamageSound(limbJoint.Params.BreakSound, 1.0f, limbJoint.LimbA.body.DrawPosition, range: range);
}
}
}

View File

@@ -109,6 +109,22 @@ namespace Barotrauma
set => grainStrength = Math.Max(0, value);
}
/// <summary>
/// Can be used to set camera shake from status effects
/// </summary>
public float CameraShake
{
get { return Screen.Selected?.Cam?.Shake ?? 0.0f; }
set
{
if (!MathUtils.IsValid(value)) { return; }
if (Screen.Selected?.Cam != null)
{
Screen.Selected.Cam.Shake = value;
}
}
}
private readonly List<ParticleEmitter> bloodEmitters = new List<ParticleEmitter>();
public IEnumerable<ParticleEmitter> BloodEmitters
{

View File

@@ -307,7 +307,7 @@ namespace Barotrauma
{
if (!brokenItem.IsInteractable(character)) { continue; }
float alpha = GetDistanceBasedIconAlpha(brokenItem);
if (alpha <= 0.0f) continue;
if (alpha <= 0.0f) { continue; }
GUI.DrawIndicator(spriteBatch, brokenItem.DrawPosition, cam, 100.0f, GUIStyle.BrokenIcon.Value.Sprite,
Color.Lerp(GUIStyle.Red, GUIStyle.Orange * 0.5f, brokenItem.Condition / brokenItem.MaxCondition) * alpha);
}

View File

@@ -501,7 +501,7 @@ namespace Barotrauma
info?.ClearSavedStatValues(statType);
for (int i = 0; i < savedStatValueCount; i++)
{
string statIdentifier = msg.ReadString();
Identifier statIdentifier = msg.ReadIdentifier();
float statValue = msg.ReadSingle();
bool removeOnDeath = msg.ReadBoolean();
info?.ChangeSavedStatValue(statType, statValue, statIdentifier, removeOnDeath, setValue: true);

View File

@@ -1136,6 +1136,17 @@ namespace Barotrauma
});
AssignRelayToServer("debugdraw", false);
AssignOnExecute("debugdrawlocalization", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
{
state = !TextManager.DebugDraw;
}
TextManager.DebugDraw = state;
NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.White);
});
AssignRelayToServer("debugdraw", false);
AssignOnExecute("togglevoicechatfilters", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
@@ -1695,6 +1706,8 @@ namespace Barotrauma
config.Language = language;
GameSettings.SetCurrentConfig(config);
}
HashSet<string> missingTexts = new HashSet<string>();
//key = text tag, value = list of languages the tag is missing from
Dictionary<Identifier, HashSet<LanguageIdentifier>> missingTags = new Dictionary<Identifier, HashSet<LanguageIdentifier>>();
@@ -1755,20 +1768,38 @@ namespace Barotrauma
foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent))))
{
foreach (var property in itemComponentType.GetProperties())
checkSerializableEntityType(itemComponentType);
}
checkSerializableEntityType(typeof(Item));
checkSerializableEntityType(typeof(Hull));
checkSerializableEntityType(typeof(Structure));
void checkSerializableEntityType(Type t)
{
foreach (var property in t.GetProperties())
{
if (!property.IsDefined(typeof(InGameEditable), false)) { continue; }
if (!property.IsDefined(typeof(Editable), false)) { continue; }
string propertyTag = $"{property.DeclaringType.Name}.{property.Name}";
addIfMissingAll(language,
if (addIfMissingAll(language,
propertyTag.ToIdentifier(),
property.Name.ToIdentifier(),
$"sp.{propertyTag}.name".ToIdentifier());
$"sp.{property.Name}.name".ToIdentifier(),
$"sp.{propertyTag}.name".ToIdentifier()) && language == "English".ToLanguageIdentifier())
{
missingTexts.Add($"<sp.{propertyTag.ToLower()}.name>{property.Name.FormatCamelCaseWithSpaces()}</sp.{propertyTag.ToLower()}.name>");
}
addIfMissingAll(language,
var description = (property.GetCustomAttributes(true).First(a => a is Serialize) as Serialize).Description;
if (addIfMissingAll(language,
$"sp.{propertyTag}.description".ToIdentifier(),
$"{property.Name.ToIdentifier()}.description".ToIdentifier());
$"sp.{property.Name}.description".ToIdentifier(),
$"{property.Name.ToIdentifier()}.description".ToIdentifier()) && language == "English".ToLanguageIdentifier())
{
missingTexts.Add($"<sp.{propertyTag.ToLower()}.description>{description}</sp.{propertyTag.ToLower()}.description>");
}
}
}
@@ -1889,6 +1920,23 @@ namespace Barotrauma
ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
SwapLanguage(TextManager.DefaultLanguage);
if (missingTexts.Any())
{
ShowQuestionPrompt("Dump the property names and descriptions missing from English to a new xml file? Y/N",
(option) =>
{
if (option.ToLowerInvariant() == "y")
{
string path = "newtexts.txt";
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
File.WriteAllLines(path, missingTexts);
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
ToolBox.OpenFileWithShell(Path.GetFullPath(path));
SwapLanguage(TextManager.DefaultLanguage);
}
});
}
void addIfMissing(Identifier tag, LanguageIdentifier language)
{
if (!tags[language].Contains(tag))
@@ -1897,15 +1945,90 @@ namespace Barotrauma
missingTags[tag].Add(language);
}
}
void addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags)
bool addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags)
{
if (!potentialTags.Any(t => tags[language].Contains(t)))
{
var tag = potentialTags.First();
if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet<LanguageIdentifier>(); }
missingTags[tag].Add(language);
return true;
}
return false;
}
}));
commands.Add(new Command("checkduplicateloca", "", (string[] args) =>
{
if (args.Length < 1)
{
ThrowError("Please specify a file path.");
return;
}
XDocument doc1 = XMLExtensions.TryLoadXml(args[0]);
if (doc1?.Root == null)
{
ThrowError($"Could not load the file \"{args[0]}\"");
return;
}
List<(string tag, string text)> texts = new List<(string tag, string text)>();
bool duplicatesFound = false;
foreach (XElement element in doc1.Root.Elements())
{
string tag = element.Name.ToString();
string text = element.ElementInnerText();
if (texts.Any(t => t.tag == tag))
{
ThrowError($"Duplicate tag \"{tag}\".");
duplicatesFound = true;
}
}
if (duplicatesFound)
{
ThrowError($"Aborting, please fix duplicate tags in the file and try again.");
return;
}
foreach (XElement element in doc1.Root.Elements())
{
string tag = element.Name.ToString();
string text = element.ElementInnerText();
if (texts.Any(t => t.text == text))
{
if (tag.StartsWith("sp."))
{
string[] split = tag.Split('.');
if (split.Length > 3)
{
texts.RemoveAll(t => t.text == text);
string newTag = $"sp.{split[2]}.{split[3]}";
texts.Add((newTag, text));
NewMessage($"Duplicate text \"{tag}\", merging to \"{newTag}\".");
}
else
{
NewMessage($"Duplicate text \"{tag}\", using existing one \"{texts.Find(t => t.text == text).tag}\".");
}
}
else
{
texts.Add((tag, text));
ThrowError($"Duplicate text \"{tag}\". Could not determine if the text can be merged with an existing one, please check it manually.");
}
}
else
{
texts.Add((tag, text));
}
}
string filePath = "uniquetexts.xml";
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
File.WriteAllLines(filePath, texts.Select(t => $"<{t.tag}>{t.text}</{t.tag}>"));
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
}));
commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) =>
@@ -2585,99 +2708,6 @@ namespace Barotrauma
}));
#endif
commands.Add(new Command("cleanbuild", "", (string[] args) =>
{
/*GameSettings.CurrentConfig.MusicVolume = 0.5f;
GameSettings.CurrentConfig.SoundVolume = 0.5f;
GameSettings.CurrentConfig.DynamicRangeCompressionEnabled = true;
GameSettings.CurrentConfig.VoipAttenuationEnabled = true;
NewMessage("Music and sound volume set to 0.5", Color.Green);
GameSettings.CurrentConfig.GraphicsWidth = 0;
GameSettings.CurrentConfig.GraphicsHeight = 0;
GameSettings.CurrentConfig.WindowMode = WindowMode.BorderlessWindowed;
NewMessage("Resolution set to 0 x 0 (screen resolution will be used)", Color.Green);
NewMessage("Fullscreen enabled", Color.Green);
GameSettings.CurrentConfig.VerboseLogging = false;
if (GameSettings.CurrentConfig.MasterServerUrl != "http://www.undertowgames.com/baromaster")
{
ThrowError("MasterServerUrl \"" + GameSettings.CurrentConfig.MasterServerUrl + "\"!");
}
GameSettings.SaveCurrentConfig();*/
throw new NotImplementedException();
#warning TODO: reimplement
var saveFiles = Barotrauma.IO.Directory.GetFiles(SaveUtil.SaveFolder);
foreach (string saveFile in saveFiles)
{
Barotrauma.IO.File.Delete(saveFile);
NewMessage("Deleted " + saveFile, Color.Green);
}
if (Barotrauma.IO.Directory.Exists(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp")))
{
Barotrauma.IO.Directory.Delete(Barotrauma.IO.Path.Combine(SaveUtil.SaveFolder, "temp"), true);
NewMessage("Deleted temp save folder", Color.Green);
}
if (Barotrauma.IO.Directory.Exists(ServerLog.SavePath))
{
var logFiles = Barotrauma.IO.Directory.GetFiles(ServerLog.SavePath);
foreach (string logFile in logFiles)
{
Barotrauma.IO.File.Delete(logFile);
NewMessage("Deleted " + logFile, Color.Green);
}
}
if (Barotrauma.IO.File.Exists("filelist.xml"))
{
Barotrauma.IO.File.Delete("filelist.xml");
NewMessage("Deleted filelist", Color.Green);
}
if (Barotrauma.IO.File.Exists("Data/bannedplayers.txt"))
{
Barotrauma.IO.File.Delete("Data/bannedplayers.txt");
NewMessage("Deleted bannedplayers.txt", Color.Green);
}
if (Barotrauma.IO.File.Exists("Submarines/TutorialSub.sub"))
{
Barotrauma.IO.File.Delete("Submarines/TutorialSub.sub");
NewMessage("Deleted TutorialSub from the submarine folder", Color.Green);
}
/*if (Barotrauma.IO.File.Exists(GameServer.SettingsFile))
{
Barotrauma.IO.File.Delete(GameServer.SettingsFile);
NewMessage("Deleted server settings", Color.Green);
}
if (Barotrauma.IO.File.Exists(GameServer.ClientPermissionsFile))
{
Barotrauma.IO.File.Delete(GameServer.ClientPermissionsFile);
NewMessage("Deleted client permission file", Color.Green);
}*/
if (Barotrauma.IO.File.Exists("crashreport.log"))
{
Barotrauma.IO.File.Delete("crashreport.log");
NewMessage("Deleted crashreport.log", Color.Green);
}
if (!Barotrauma.IO.File.Exists("Content/Map/TutorialSub.sub"))
{
ThrowError("TutorialSub.sub not found!");
}
}));
commands.Add(new Command("reloadcorepackage", "", (string[] args) =>
{
if (args.Length < 1)

View File

@@ -769,7 +769,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)
{

View File

@@ -25,7 +25,7 @@ namespace Barotrauma
private PlayerBalanceElement? playerBalanceElement;
private List<CharacterInfo> PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires;
private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(ClientPermissions.ManageHires);
private bool HasPermission => CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires);
private Point resolutionWhenCreated;

View File

@@ -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

View File

@@ -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;
@@ -1605,6 +1612,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);

View File

@@ -584,6 +584,13 @@ namespace Barotrauma
{
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)
{

View File

@@ -139,10 +139,10 @@ namespace Barotrauma
return tab switch
{
StoreTab.Buy => true,
StoreTab.Sell => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellInventoryItems),
StoreTab.SellSub => campaignUI.Campaign.AllowedToManageCampaign(Networking.ClientPermissions.SellSubItems),
StoreTab.Sell => CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.SellInventoryItems),
StoreTab.SellSub => CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.SellSubItems),
_ => false,
};
};
}
private void UpdatePermissions()
@@ -892,7 +892,7 @@ namespace Barotrauma
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 ?
@@ -1995,11 +1995,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));

View File

@@ -673,7 +673,7 @@ namespace Barotrauma
{
if (GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null)
{
return Submarine.MainSub.Info;
return Submarine.MainSub?.Info;
}
else
{

View File

@@ -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,14 +207,10 @@ 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)
if (SelectedTab is InfoFrameTab.Talents)
{
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; }
@@ -251,6 +248,10 @@ namespace Barotrauma
{
infoFrame?.AddToGUIUpdateList();
NetLobbyScreen.JobInfoFrame?.AddToGUIUpdateList();
if (SelectedTab is InfoFrameTab.Talents)
{
talentMenu?.AddToGUIUpdateList();
}
}
public static void OnRoundEnded()
@@ -325,11 +326,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 +404,7 @@ namespace Barotrauma
CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub);
break;
case InfoFrameTab.Talents:
CreateCharacterInfo(infoFrameHolder);
talentMenu.CreateGUI(infoFrameHolder);
break;
}
}
@@ -1774,370 +1775,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 };
}
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>();
@@ -2172,116 +1813,10 @@ namespace Barotrauma
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()

View File

@@ -0,0 +1,680 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using static Barotrauma.TalentTree;
using static Barotrauma.TalentTree.TalentTreeStageState;
namespace Barotrauma
{
internal readonly record struct TalentButton(GUIButton Button,
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
{
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<TalentTreeStageState, TalentTreeStyle> talentStageStyles =
new Dictionary<TalentTreeStageState, 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<GUIComponent> showCaseTalentFrames = new HashSet<GUIComponent>();
private readonly HashSet<TalentCornerIcon> talentCornerIcons = new HashSet<TalentCornerIcon>();
private HashSet<Identifier> selectedTalents = new HashSet<Identifier>();
private GUIListBox? skillListBox;
private GUITextBlock? talentPointText;
private GUIProgressBar? experienceBar;
private GUITextBlock? experienceText;
private GUILayoutGroup? skillLayout;
private GUIButton? talentApplyButton,
talentResetButton;
public void CreateGUI(GUIFrame parent)
{
parent.ClearChildren();
talentButtons.Clear();
talentCornerIcons.Clear();
showCaseTalentFrames.Clear();
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
};
Character? controlledCharacter = Character.Controlled;
CharacterInfo? info = controlledCharacter?.Info ?? GameMain.Client?.CharacterInfo;
if (info is null) { return; }
CreateStatPanel(contentLayout, info);
new GUIFrame(new RectTransform(new Vector2(1f, 1f), contentLayout.RectTransform), style: "HorizontalLine");
if (JobTalentTrees.TryGet(info.Job.Prefab.Identifier, out TalentTree? talentTree))
{
CreateTalentMenu(contentLayout, info, talentTree!);
}
CreateFooter(contentLayout, info);
UpdateTalentInfo();
if (GameMain.NetworkMember != null)
{
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) =>
{
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();
}
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.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)
{
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" + extraTalent.Description),
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();
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(0.333f, 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);
}
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();
GetSpecializationList().RectTransform.Resize(new Point(specializationList.Content.Children.Sum(c => c.Rect.Width), specializationList.Rect.Height));
GUITextBlock.AutoScaleAndNormalize(subTreeNames);
GUIListBox GetSpecializationList()
{
if (mainList.Content.Children.LastOrDefault() is GUIListBox specializationList)
{
return specializationList;
}
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 elementPadding = GUI.IntScale(8);
GUIFrame talentOptionFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.01f), parent.RectTransform, anchor: Anchor.TopCenter)
{ MinSize = new Point(0, GUI.IntScale(65)) }, 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();
bool hasShowcase = talentOption.ShowcaseTalent.TryUnwrap(out Identifier showcaseTalentIdentifier);
GUILayoutGroup showcaseLayout = talentOptionLayoutGroup;
if (hasShowcase)
{
talentOptionIdentifiers.Add(showcaseTalentIdentifier);
Point parentSize = talentBackground.RectTransform.NonScaledSize;
GUIFrame showCaseFrame = new GUIFrame(new RectTransform(new Point((int)(parentSize.X / 3f * (talentOptionIdentifiers.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);
showcaseLayout = new GUILayoutGroup(new RectTransform(Vector2.One, showcaseCenterGroup.RectTransform), isHorizontal: true) { Stretch = true };
showCaseTalentFrames.Add(showCaseFrame);
}
foreach (Identifier talentId in talentOptionIdentifiers)
{
if (!TalentPrefab.TalentPrefabs.TryGet(talentId, out TalentPrefab? talent)) { continue; }
bool isShowCaseTalent = hasShowcase && talentId == showcaseTalentIdentifier;
GUIComponent talentParent;
if (hasShowcase && talentId != showcaseTalentIdentifier)
{
talentParent = showcaseLayout;
}
else
{
talentParent = talentOptionLayoutGroup;
}
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" + talent.Description),
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;
}
Character? controlledCharacter = info.Character;
if (controlledCharacter is null) { return false; }
if (talentOption.MaxChosenTalents is 1)
{
// 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 (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents))
{
if (!selectedTalents.Contains(talentIdentifier))
{
selectedTalents.Add(talentIdentifier);
}
else
{
selectedTalents.Remove(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)
{
Color = talent.ColorOverride.TryUnwrap(out Color color) ? color : Color.White,
PressedColor = unselectableColor,
DisabledColor = unselectableColor * 0.5f,
CanBeFocused = false,
};
}
iconImage.Enabled = talentButton.Enabled;
if (isShowCaseTalent) { continue; }
talentButtons.Add(new TalentButton(talentButton, iconImage, talent));
}
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)
{
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)
{
Character controlledCharacter = Character.Controlled;
if (controlledCharacter is null) { return false; }
ApplyTalents(controlledCharacter);
return true;
}
public void UpdateTalentInfo()
{
if (!(Character.Controlled is { Info: var info } character)) { 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 = $"{info.ExperiencePoints - info.GetExperienceRequiredForCurrentLevel()} / {info.GetExperienceRequiredToLevelUp() - info.GetExperienceRequiredForCurrentLevel()}";
experienceBar.BarSize = info.GetProgressTowardsNextLevel();
}
selectedTalents = CheckTalentSelection(character, selectedTalents).ToHashSet();
string pointsLeft = info.GetAvailableTalentPoints().ToString();
int talentCount = selectedTalents.Count - info.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)
{
TalentTreeStageState 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)
{
Identifier talentIdentifier = talentButton.Identifier;
bool unselectable = !IsViableTalentForCharacter(character, talentIdentifier, selectedTalents) || character.HasTalent(talentIdentifier);
Color newTalentColor = unselectable ? unselectableColor : unselectedColor;
Color hoverColor = Color.White;
bool selected = false;
if (character.HasTalent(talentIdentifier))
{
selected = true;
newTalentColor = GUIStyle.Green;
}
else if (selectedTalents.Contains(talentIdentifier))
{
selected = true;
newTalentColor = GUIStyle.Orange;
hoverColor = Color.Lerp(GUIStyle.Orange, Color.White, 0.7f);
}
bool shouldOverride = !unselectable || selected;
if (shouldOverride && talentButton.Prefab.ColorOverride.TryUnwrap(out Color overrideColor))
{
newTalentColor = overrideColor;
}
talentButton.IconComponent.Color = newTalentColor;
talentButton.IconComponent.HoverColor = hoverColor;
}
if (skillListBox is null) { return; }
TabMenu.CreateSkillList(character, info, skillListBox);
}
public void AddToGUIUpdateList()
{
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)
{
component.AddToGUIUpdateList(order: 1);
if (!component.Visible) { continue; }
if (keyboardInteracted || (mouseInteracted && !component.Rect.Contains(PlayerInput.MousePosition)))
{
component.Visible = false;
}
}
}
public void Update()
{
if (Character.Controlled?.Info is not { } characterInfo || 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);
}
}
}
}

View File

@@ -433,8 +433,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)
{
@@ -847,7 +847,7 @@ namespace Barotrauma
foreach (UpgradePrefab prefab in prefabs)
{
if (prefab.MaxLevel is 0) { continue; }
if (prefab.GetMaxLevelForCurrentSub() == 0) { continue; }
CreateUpgradeEntry(prefab, category, parent.Content, submarine, entitiesOnSub);
}
}
@@ -1080,7 +1080,7 @@ namespace Barotrauma
public static GUIFrame CreateUpgradeFrame(UpgradePrefab prefab, UpgradeCategory category, CampaignMode campaign, RectTransform rectTransform, bool addBuyButton = true)
{
int price = prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation);
int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation);
return CreateUpgradeEntry(rectTransform, prefab.Sprite, prefab.Name, prefab.Description, price, new CategoryData(category, prefab), addBuyButton, upgradePrefab: prefab, currentLevel: campaign.UpgradeManager.GetUpgradeLevel(prefab, category));
}
@@ -1177,11 +1177,12 @@ namespace Barotrauma
private static void UpdateUpgradePercentageText(GUITextBlock text, UpgradePrefab upgradePrefab, int currentLevel)
{
float nextIncrease = upgradePrefab.IncreaseOnTooltip * (Math.Min(currentLevel + 1, upgradePrefab.MaxLevel));
int maxLevel = upgradePrefab.GetMaxLevelForCurrentSub();
float nextIncrease = upgradePrefab.IncreaseOnTooltip * Math.Min(currentLevel + 1, maxLevel);
if (nextIncrease != 0f)
{
text.Text = $"{Math.Round(nextIncrease, 1)} %";
if (currentLevel == upgradePrefab.MaxLevel)
if (currentLevel == maxLevel)
{
text.TextColor = Color.Gray;
}
@@ -1221,7 +1222,7 @@ namespace Barotrauma
{
LocalizedString promptBody = TextManager.GetWithVariables("Upgrades.PurchasePromptBody",
("[upgradename]", prefab.Name),
("[amount]", prefab.Price.GetBuyprice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString()));
("[amount]", prefab.Price.GetBuyPrice(Campaign.UpgradeManager.GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation).ToString()));
currectConfirmation = EventEditorScreen.AskForConfirmation(TextManager.Get("Upgrades.PurchasePromptTitle"), promptBody, () =>
{
if (GameMain.NetworkMember != null)
@@ -1617,14 +1618,15 @@ namespace Barotrauma
{
int currentLevel = campaign.UpgradeManager.GetUpgradeLevel(prefab, category);
LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", prefab.MaxLevel.ToString()));
int maxLevel = prefab.GetMaxLevelForCurrentSub();
LocalizedString progressText = TextManager.GetWithVariables("upgrades.progressformat", ("[level]", currentLevel.ToString()), ("[maxlevel]", maxLevel.ToString()));
if (prefabFrame.FindChild("progressbar", true) is { } progressParent)
{
GUIProgressBar bar = progressParent.GetChild<GUIProgressBar>();
if (bar != null)
{
bar.BarSize = currentLevel / (float) prefab.MaxLevel;
bar.Color = currentLevel >= prefab.MaxLevel ? GUIStyle.Green : GUIStyle.Orange;
bar.BarSize = currentLevel / (float)maxLevel;
bar.Color = currentLevel >= maxLevel ? GUIStyle.Green : GUIStyle.Orange;
}
GUITextBlock block = progressParent.GetChild<GUITextBlock>();
@@ -1637,12 +1639,12 @@ namespace Barotrauma
GUITextBlock priceLabel = textBlocks[0];
priceLabel.Visible = true;
int price = prefab.Price.GetBuyprice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation);
int price = prefab.Price.GetBuyPrice(campaign.UpgradeManager.GetUpgradeLevel(prefab, category), campaign.Map?.CurrentLocation);
if (priceLabel != null && !WaitForServerUpdate)
{
priceLabel.Text = TextManager.FormatCurrency(price);
if (currentLevel >= prefab.MaxLevel)
if (currentLevel >= maxLevel)
{
priceLabel.Text = TextManager.Get("Upgrade.MaxedUpgrade");
}
@@ -1651,7 +1653,7 @@ namespace Barotrauma
GUIButton button = buttonParent.GetChild<GUIButton>();
if (button != null)
{
button.Enabled = currentLevel < prefab.MaxLevel;
button.Enabled = currentLevel < maxLevel;
if (WaitForServerUpdate || campaign.GetBalance() < price)
{
button.Enabled = false;
@@ -1697,13 +1699,14 @@ namespace Barotrauma
foreach (GUIComponent component in indicators.Children)
{
if (!(component is GUIImage image)) { continue; }
if (component is not GUIImage image) { continue; }
foreach (UpgradePrefab prefab in prefabs)
{
if (component.UserData != prefab) { continue; }
if (prefab.MaxLevel is 0)
int maxLevel = prefab.GetMaxLevelForCurrentSub();
if (maxLevel == 0)
{
component.Visible = false;
continue;
@@ -1715,7 +1718,6 @@ namespace Barotrauma
GUIComponentStyle onStyle = styles["upgradeindicatoron".ToIdentifier()];
GUIComponentStyle dimStyle = styles["upgradeindicatordim".ToIdentifier()];
GUIComponentStyle offStyle = styles["upgradeindicatoroff".ToIdentifier()];
int maxLevel = prefab.MaxLevel;
if (maxLevel == 0)
{

View File

@@ -30,7 +30,7 @@ namespace Barotrauma
Data = data,
OnClick = (GUITextBlock component, GUITextBlock.ClickableArea area) =>
{
GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/");
GameMain.ShowOpenUrlInWebBrowserPrompt("https://gameanalytics.com/privacy/");
}
});
}

View File

@@ -791,6 +791,10 @@ namespace Barotrauma
{
GUI.TogglePauseMenu();
}
else if (GameSession?.Campaign is { ShowCampaignUI: true, ForceMapUI: false })
{
GameSession.Campaign.ShowCampaignUI = false;
}
//open the pause menu if not controlling a character OR if the character has no UIs active that can be closed with ESC
else if ((Character.Controlled == null || !itemHudActive())
&& CharacterHealth.OpenHealthWindow == null
@@ -1200,7 +1204,7 @@ namespace Barotrauma
base.OnExiting(sender, args);
}
public void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null)
public static void ShowOpenUrlInWebBrowserPrompt(string url, string promptExtensionTag = null)
{
if (string.IsNullOrEmpty(url)) { return; }
if (GUIMessageBox.VisibleBox?.UserData as string == "verificationprompt") { return; }
@@ -1218,7 +1222,14 @@ namespace Barotrauma
};
msgBox.Buttons[0].OnClicked = (btn, userdata) =>
{
ToolBox.OpenFileWithShell(url);
try
{
ToolBox.OpenFileWithShell(url);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to open the url {url}", e);
}
msgBox.Close();
return true;
};

View File

@@ -86,10 +86,12 @@ namespace Barotrauma
}
}
private static bool IsOwner(Client client) => client != null && client.IsOwner;
/// <summary>
/// There is a server-side implementation of the method in <see cref="MultiPlayerCampaign"/>
/// </summary>
public bool AllowedToManageCampaign(ClientPermissions permissions)
public static bool AllowedToManageCampaign(ClientPermissions permissions)
{
//allow managing the round if the client has permissions, is the owner, the only client in the server,
//or if no-one has management permissions
@@ -97,9 +99,8 @@ namespace Barotrauma
return
GameMain.Client.HasPermission(permissions) ||
GameMain.Client.HasPermission(ClientPermissions.ManageCampaign) ||
GameMain.Client.ConnectedClients.Count == 1 ||
GameMain.Client.IsServerOwner ||
GameMain.Client.ConnectedClients.None(c => c.InGame && (c.IsOwner || c.HasPermission(permissions)));
AnyOneAllowedToManageCampaign(permissions);
}
public static bool AllowedToManageWallets()

View File

@@ -407,6 +407,11 @@ namespace Barotrauma
GUI.SetSavingIndicatorState(success);
crewDead = false;
if (success)
{
// Event history must be registered before ending the round or it will be cleared
GameMain.GameSession.EventManager.RegisterEventHistory();
}
GameMain.GameSession.EndRound("", traitorResults, transitionType);
var continueButton = GameMain.GameSession.RoundSummary?.ContinueButton;
RoundSummary roundSummary = null;
@@ -466,7 +471,6 @@ namespace Barotrauma
if (success)
{
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
GameMain.GameSession.EventManager.RegisterEventHistory();
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
}
else

View File

@@ -506,7 +506,7 @@ namespace Barotrauma
private LocalizedString GetHeaderText(bool gameOver, CampaignMode.TransitionType transitionType)
{
string locationName = Submarine.MainSub.AtEndExit ? endLocation?.Name : startLocation?.Name;
string locationName = Submarine.MainSub is { AtEndExit: true } ? endLocation?.Name : startLocation?.Name;
string textTag;
if (gameOver)

View File

@@ -774,7 +774,6 @@ namespace Barotrauma
}
else
{
bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any);
var selectedContainer = character.SelectedItem?.GetComponent<ItemContainer>();
if (selectedContainer != null &&
@@ -802,8 +801,7 @@ namespace Barotrauma
}
else if (character.HeldItems.Any(i =>
i.OwnInventory != null &&
/*disallow putting into equipped item if the item is equippable (equip as the quick action instead)*/
((i.OwnInventory.CanBePut(item) && (allowInventorySwap || !isEquippable)) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))))
(i.OwnInventory.CanBePut(item) || (i.OwnInventory.Capacity == 1 && i.OwnInventory.AllowSwappingContainedItems && i.OwnInventory.Container.CanBeContained(item)))))
{
return QuickUseAction.PutToEquippedItem;
}

View File

@@ -25,14 +25,31 @@ namespace Barotrauma.Items.Components
foreach (Node node in nodes)
{
GameMain.ParticleManager.CreateParticle("swirlysmoke", node.WorldPosition, Vector2.Zero);
if (node.ParentIndex > -1)
{
Vector2 diff = nodes[node.ParentIndex].WorldPosition - node.WorldPosition;
float dist = diff.Length();
Vector2 normalizedDiff = diff / dist;
for (float x = 0.0f; x < dist; x += 50.0f)
{
var spark = GameMain.ParticleManager.CreateParticle("ElectricShock", node.WorldPosition + normalizedDiff * x, Vector2.Zero);
if (spark != null)
{
spark.Size *= 0.3f;
}
}
}
}
}
public void DrawElectricity(SpriteBatch spriteBatch)
{
if (timer <= 0.0f) { return; }
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i].Length <= 1.0f) continue;
if (nodes[i].Length <= 1.0f) { continue; }
var node = nodes[i];
electricitySprite.Draw(spriteBatch,
(i + frameOffset) % electricitySprite.FrameCount,
@@ -46,10 +63,16 @@ namespace Barotrauma.Items.Components
if (GameMain.DebugDraw)
{
for (int i = 0; i < nodes.Count; i++)
for (int i = 1; i < nodes.Count; i++)
{
if (nodes[i].Length <= 1.0f) continue;
GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 5, Color.LightCyan, isFilled: true);
GUI.DrawLine(spriteBatch,
new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y),
new Vector2(nodes[nodes[i].ParentIndex].WorldPosition.X, -nodes[nodes[i].ParentIndex].WorldPosition.Y),
Color.LightCyan,
width: 3);
if (nodes[i].Length <= 1.0f) { continue; }
GUI.DrawRectangle(spriteBatch, new Vector2(nodes[i].WorldPosition.X, -nodes[i].WorldPosition.Y), Vector2.One * 10, Color.LightCyan, isFilled: true);
}
}
}

View File

@@ -265,6 +265,8 @@ namespace Barotrauma.Items.Components
foreach (DeconstructItem deconstructItem in it.Prefab.DeconstructItems)
{
if (!deconstructItem.IsValidDeconstructor(item)) { continue; }
float percentageHealth = it.Condition / it.MaxCondition;
if (percentageHealth < deconstructItem.MinCondition || percentageHealth > deconstructItem.MaxCondition) { continue; }
RegisterItem(deconstructItem.ItemIdentifier, deconstructItem.Amount);
}

View File

@@ -1119,7 +1119,7 @@ namespace Barotrauma.Items.Components
if (it.GetComponent<PowerContainer>() is { } battery)
{
int batteryCapacity = (int)(battery.Charge / battery.Capacity * 100f);
int batteryCapacity = (int)(battery.Charge / battery.GetCapacity() * 100f);
line2 = TextManager.GetWithVariable("statusmonitor.battery.tooltip", "[amount]", batteryCapacity.ToString());
}
else if (it.GetComponent<PowerTransfer>() is { } powerTransfer)

View File

@@ -1028,7 +1028,7 @@ namespace Barotrauma.Items.Components
{
foreach (var c in MineralClusters)
{
var unobtainedMinerals = c.resources.Where(i => i != null && i.GetRootInventoryOwner() == i);
var unobtainedMinerals = c.resources.Where(i => i != null && i.GetComponent<Holdable>() is { Attached: true });
if (unobtainedMinerals.None()) { continue; }
if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; }
var i = unobtainedMinerals.FirstOrDefault();

View File

@@ -390,7 +390,7 @@ namespace Barotrauma.Items.Components
!ActiveDockingSource.Docked && DockingTarget?.Item?.Submarine == Level.Loaded.StartOutpost && (DockingTarget?.Item?.Submarine?.Info.IsOutpost ?? false))
{
// Docking to an outpost
var subsToLeaveBehind = campaign.GetSubsToLeaveBehind(Item.Submarine);
var subsToLeaveBehind = CampaignMode.GetSubsToLeaveBehind(Item.Submarine);
if (subsToLeaveBehind.Any())
{
enterOutpostPrompt = new GUIMessageBox(

View File

@@ -133,35 +133,43 @@ namespace Barotrauma.Items.Components
public void Draw(SpriteBatch spriteBatch, bool editing = false, float itemDepth = -1)
{
if (indicatorSize.X <= 1.0f || indicatorSize.Y <= 1.0f) { return; }
Vector2 scaledIndicatorSize = indicatorSize * item.Scale;
if (scaledIndicatorSize.X <= 2.0f || scaledIndicatorSize.Y <= 2.0f) { return; }
const float outlineThickness = 1.0f;
Vector2 itemSize = new Vector2(item.Sprite.SourceRect.Width, item.Sprite.SourceRect.Height) * item.Scale;
Vector2 indicatorPos = -itemSize / 2 + indicatorPosition * item.Scale;
if (item.FlippedX && item.Prefab.CanSpriteFlipX) { indicatorPos.X = -indicatorPos.X - indicatorSize.X * item.Scale; }
if (item.FlippedY && item.Prefab.CanSpriteFlipY) { indicatorPos.Y = -indicatorPos.Y - indicatorSize.Y * item.Scale; }
Vector2 indicatorPos = -itemSize / 2.0f + indicatorPosition * item.Scale;
Vector2 itemPosition = new Vector2(item.DrawPosition.X, -item.DrawPosition.Y);
Vector2 flip = new Vector2(item.FlippedX && item.Prefab.CanSpriteFlipX ? -1.0f : 1.0f, item.FlippedY && item.Prefab.CanSpriteFlipY ? -1.0f : 1.0f);
Matrix rotate = Matrix.CreateRotationZ(item.RotationRad);
Vector2 center = Vector2.Transform((indicatorPos + (scaledIndicatorSize * 0.5f)) * flip, rotate) + itemPosition;
if (charge > 0 && capacity > 0)
{
float chargeRatio = MathHelper.Clamp(charge / capacity, 0.0f, 1.0f);
Color indicatorColor = ToolBox.GradientLerp(chargeRatio, Color.Red, Color.Orange, Color.Green);
if (!isHorizontal)
Vector2 indicatorCenter = (indicatorPos + (scaledIndicatorSize * 0.5f)) * flip;
Vector2 indicatorSize;
if (isHorizontal)
{
GUI.DrawRectangle(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y + ((indicatorSize.Y * item.Scale) * (1.0f - chargeRatio))) + indicatorPos,
new Vector2(indicatorSize.X * item.Scale, (indicatorSize.Y * item.Scale) * chargeRatio), indicatorColor, true,
depth: item.SpriteDepth - 0.00001f);
float indicatorLength = (scaledIndicatorSize.X - outlineThickness * 2.0f) * chargeRatio;
indicatorCenter.X += -scaledIndicatorSize.X * 0.5f + (flipIndicator ? scaledIndicatorSize.X - outlineThickness - indicatorLength * 0.5f : outlineThickness + indicatorLength * 0.5f);
indicatorSize = new Vector2(indicatorLength, scaledIndicatorSize.Y);
}
else
{
GUI.DrawRectangle(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos,
new Vector2((indicatorSize.X * item.Scale) * chargeRatio, indicatorSize.Y * item.Scale), indicatorColor, true,
depth: item.SpriteDepth - 0.00001f);
float indicatorLength = (scaledIndicatorSize.Y - outlineThickness * 2.0f) * chargeRatio;
indicatorCenter.Y += -scaledIndicatorSize.Y * 0.5f + (flipIndicator ? outlineThickness + indicatorLength * 0.5f : scaledIndicatorSize.Y - outlineThickness - indicatorLength * 0.5f);
indicatorSize = new Vector2(scaledIndicatorSize.X, indicatorLength);
}
indicatorCenter = Vector2.Transform(indicatorCenter, rotate) + itemPosition;
GUI.DrawFilledRectangle(spriteBatch, indicatorCenter, indicatorSize, indicatorSize * 0.5f, item.RotationRad, indicatorColor, item.SpriteDepth - 0.00001f);
}
GUI.DrawRectangle(spriteBatch,
new Vector2(item.DrawPosition.X, -item.DrawPosition.Y) + indicatorPos,
indicatorSize * item.Scale, Color.Black, depth: item.SpriteDepth - 0.000015f);
GUI.DrawRectangle(spriteBatch, center, scaledIndicatorSize, scaledIndicatorSize * 0.5f, item.RotationRad, Color.Black, item.SpriteDepth - 0.000015f, outlineThickness, GUI.OutlinePosition.Inside);
}
public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData)

View File

@@ -581,7 +581,7 @@ namespace Barotrauma.Items.Components
var battery = recipient.Item?.GetComponent<PowerContainer>();
if (battery == null || battery.Item.Condition <= 0.0f) { continue; }
availableCharge += battery.Charge;
availableCapacity += battery.Capacity;
availableCapacity += battery.GetCapacity();
}
}

View File

@@ -1415,6 +1415,15 @@ namespace Barotrauma
case EventType.ChangeProperty:
ReadPropertyChange(msg, false);
break;
case EventType.ItemStat:
byte length = msg.ReadByte();
for (int i = 0; i < length; i++)
{
var statIdentifier = INetSerializableStruct.Read<ItemStatManager.TalentStatIdentifier>(msg);
var statValue = msg.ReadSingle();
StatManager.ApplyStat(statIdentifier, statValue);
}
break;
case EventType.Upgrade:
Identifier identifier = msg.ReadIdentifier();
byte level = msg.ReadByte();

View File

@@ -208,6 +208,13 @@ namespace Barotrauma
DecorativeSpriteGroups = decorativeSpriteGroups.Select(kvp => (kvp.Key, kvp.Value.ToImmutableArray())).ToImmutableDictionary();
}
public bool CanCharacterBuy()
{
if (!DefaultPrice.RequiresUnlock) { return true; }
return Character.Controlled is not null && Character.Controlled.HasStoreAccessForItem(this);
}
public override void UpdatePlacing(Camera cam)
{
Vector2 position = Submarine.MouseToWorldGrid(cam, Submarine.MainSub);

View File

@@ -162,23 +162,27 @@ namespace Barotrauma
RemoveFogOfWar(StartLocation);
GenerateLocationConnectionVisuals();
GenerateAllLocationConnectionVisuals();
}
partial void GenerateLocationConnectionVisuals()
partial void GenerateAllLocationConnectionVisuals()
{
foreach (LocationConnection connection in Connections)
{
Vector2 connectionStart = connection.Locations[0].MapPosition;
Vector2 connectionEnd = connection.Locations[1].MapPosition;
float connectionLength = Vector2.Distance(connectionStart, connectionEnd);
int iterations = Math.Min((int)Math.Sqrt(connectionLength * generationParams.ConnectionIndicatorIterationMultiplier), 5);
connection.CrackSegments.Clear();
connection.CrackSegments.AddRange(MathUtils.GenerateJaggedLine(
connectionStart, connectionEnd,
iterations, connectionLength * generationParams.ConnectionIndicatorDisplacementMultiplier));
GenerateLocationConnectionVisuals(connection);
}
}
partial void GenerateLocationConnectionVisuals(LocationConnection connection)
{
Vector2 connectionStart = connection.Locations[0].MapPosition;
Vector2 connectionEnd = connection.Locations[1].MapPosition;
float connectionLength = Vector2.Distance(connectionStart, connectionEnd);
int iterations = Math.Min((int)Math.Sqrt(connectionLength * generationParams.ConnectionIndicatorIterationMultiplier), 5);
connection.CrackSegments.Clear();
connection.CrackSegments.AddRange(MathUtils.GenerateJaggedLine(
connectionStart, connectionEnd,
iterations, connectionLength * generationParams.ConnectionIndicatorDisplacementMultiplier));
}
private void LocationChanged(Location prevLocation, Location newLocation)
{
@@ -414,7 +418,7 @@ namespace Barotrauma
new GUIMessageBox(string.Empty, TextManager.Get("LockedPathTooltip"));
}
//clients aren't allowed to select the location without a permission
else if ((GameMain.GameSession?.GameMode as CampaignMode)?.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap) ?? false)
else if (CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap))
{
connectionHighlightState = 0.0f;
SelectedConnection = connection;

View File

@@ -277,16 +277,12 @@ namespace Barotrauma.Networking
public void Clear()
{
ID = 0;
lastReceivedID = 0;
firstNewID = null;
events.Clear();
eventLastSent.Clear();
MidRoundSyncingDone = false;
ClearSelf();
}
/// <summary>
@@ -297,6 +293,10 @@ namespace Barotrauma.Networking
{
ID = 0;
events.Clear();
if (thisClient != null)
{
thisClient.LastSentEntityEventID = 0;
}
}
}
}

View File

@@ -14,7 +14,7 @@ namespace Barotrauma.Networking
{
static class PingUtils
{
private static readonly Dictionary<IPAddress, int> activePings = new Dictionary<IPAddress, int>();
private static readonly Dictionary<IPEndPoint, int> activePings = new Dictionary<IPEndPoint, int>();
private static bool steamPingInfoReady;
@@ -36,9 +36,9 @@ namespace Barotrauma.Networking
switch (serverInfo.Endpoint)
{
case LidgrenEndpoint { NetEndpoint: { Address: var address } }:
case LidgrenEndpoint { NetEndpoint: var endPoint }:
GetIPAddressPing(serverInfo, address, onPingDiscovered);
GetIPAddressPing(serverInfo, endPoint, onPingDiscovered);
break;
case SteamP2PEndpoint steamP2PEndpoint:
TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})",
@@ -131,9 +131,9 @@ namespace Barotrauma.Networking
}
}
private static void GetIPAddressPing(ServerInfo serverInfo, IPAddress address, Action<ServerInfo> onPingDiscovered)
private static void GetIPAddressPing(ServerInfo serverInfo, IPEndPoint endPoint, Action<ServerInfo> onPingDiscovered)
{
if (IPAddress.IsLoopback(address))
if (IPAddress.IsLoopback(endPoint.Address))
{
serverInfo.Ping = Option<int>.Some(0);
onPingDiscovered(serverInfo);
@@ -142,24 +142,24 @@ namespace Barotrauma.Networking
{
lock (activePings)
{
if (activePings.ContainsKey(address)) { return; }
activePings.Add(address, activePings.Any() ? activePings.Values.Max() + 1 : 0);
if (activePings.ContainsKey(endPoint)) { return; }
activePings.Add(endPoint, activePings.Any() ? activePings.Values.Max() + 1 : 0);
}
serverInfo.Ping = Option<int>.None();
TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000),
TaskPool.Add($"PingServerAsync ({endPoint})", PingServerAsync(endPoint, 1000),
rtt =>
{
if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option<int>.None(); }
onPingDiscovered(serverInfo);
lock (activePings)
{
activePings.Remove(address);
activePings.Remove(endPoint);
}
});
}
}
private static async Task<Option<int>> PingServerAsync(IPAddress ipAddress, int timeOut)
private static async Task<Option<int>> PingServerAsync(IPEndPoint endPoint, int timeOut)
{
await Task.Yield();
bool shouldGo = false;
@@ -167,21 +167,21 @@ namespace Barotrauma.Networking
{
lock (activePings)
{
shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25;
shouldGo = activePings.Count(kvp => kvp.Value < activePings[endPoint]) < 25;
}
await Task.Delay(25);
}
if (ipAddress == null) { return Option<int>.None(); }
if (endPoint?.Address == null) { return Option<int>.None(); }
//don't attempt to ping if the address is IPv6 and it's not supported
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option<int>.None(); }
if (endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option<int>.None(); }
Ping ping = new Ping();
byte[] buffer = new byte[32];
try
{
PingReply pingReply = await ping.SendPingAsync(ipAddress, timeOut, buffer, new PingOptions(128, true));
PingReply pingReply = await ping.SendPingAsync(endPoint.Address, timeOut, buffer, new PingOptions(128, true));
return pingReply.Status switch
{
@@ -191,9 +191,9 @@ namespace Barotrauma.Networking
}
catch (Exception ex)
{
GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message));
GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + endPoint.Address, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message));
#if DEBUG
DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red);
DebugConsole.NewMessage("Failed to ping a server (" + endPoint.Address + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red);
#endif
return Option<int>.None();

View File

@@ -702,10 +702,30 @@ namespace Barotrauma.Networking
{
Enabled = !GameMain.NetworkMember.GameStarted
};
var cargoFrame = new GUIListBox(new RectTransform(new Vector2(0.6f, 0.7f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.BottomRight, Pivot.BottomLeft))
var cargoFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.7f), settingsTabs[(int)SettingsTab.Rounds].RectTransform, Anchor.BottomRight, Pivot.BottomLeft))
{
Visible = false
};
var cargoContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), cargoFrame.RectTransform, Anchor.Center))
{
Stretch = true
};
var filterText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), cargoContent.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont);
var entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), filterText.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true);
filterText.RectTransform.MinSize = new Point(0, entityFilterBox.RectTransform.MinSize.Y);
var cargoList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.8f), cargoContent.RectTransform));
entityFilterBox.OnTextChanged += (textBox, text) =>
{
foreach (var child in cargoList.Content.Children)
{
if (child.UserData is not ItemPrefab itemPrefab) { continue; }
child.Visible = string.IsNullOrEmpty(text) || itemPrefab.Name.Contains(text, StringComparison.OrdinalIgnoreCase);
}
return true;
};
cargoButton.UserData = cargoFrame;
cargoButton.OnClicked = (button, obj) =>
{
@@ -721,7 +741,7 @@ namespace Barotrauma.Networking
GUITextBlock.AutoScaleAndNormalize(buttonHolder.Children.Select(c => ((GUIButton)c).TextBlock));
foreach (ItemPrefab ip in ItemPrefab.Prefabs)
foreach (ItemPrefab ip in ItemPrefab.Prefabs.OrderBy(ip => ip.Name))
{
if (ip.AllowAsExtraCargo.HasValue)
{
@@ -732,10 +752,10 @@ namespace Barotrauma.Networking
if (!ip.CanBeBought) { continue; }
}
var itemFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), cargoFrame.Content.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true)
var itemFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), cargoList.Content.RectTransform) { MinSize = new Point(0, 30) }, isHorizontal: true)
{
Stretch = true,
UserData = cargoFrame,
UserData = ip,
RelativeSpacing = 0.05f
};
@@ -778,7 +798,7 @@ namespace Barotrauma.Networking
numberInput.IntValue = ExtraCargo.ContainsKey(ip) ? ExtraCargo[ip] : 0;
CoroutineManager.Invoke(() =>
{
foreach (var child in cargoFrame.Content.GetAllChildren())
foreach (var child in cargoList.Content.GetAllChildren())
{
if (child.GetChild<GUINumberInput>() is GUINumberInput otherNumberInput)
{

View File

@@ -82,6 +82,9 @@ namespace Barotrauma.Particles
[Editable, Serialize(false, IsPropertySaveable.Yes)]
public bool CopyEntityAngle { get; set; }
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Should the entity heading direction be applied to the particle rotation? Only affects after flipping the texture and when CopyEntityAngle is true.")]
public bool CopyEntityDir { get; set; }
[Editable, Serialize("1,1,1,1", IsPropertySaveable.Yes)]
public Color ColorMultiplier { get; set; }

View File

@@ -167,7 +167,7 @@ namespace Barotrauma
foreach (GUITickBox tickBox in missionTickBoxes)
{
bool disable = hasMaxMissions && !tickBox.Selected;
tickBox.Enabled = Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap) && !disable;
tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap) && !disable;
tickBox.Box.DisabledColor = disable ? tickBox.Box.Color * 0.5f : tickBox.Box.Color * 0.8f;
foreach (GUIComponent child in tickBox.Parent.Parent.Children)
{
@@ -315,7 +315,7 @@ namespace Barotrauma
if (GUI.MouseOn == tickBox) { return false; }
if (tickBox != null)
{
if (Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap) && tickBox.Enabled)
if (CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap) && tickBox.Enabled)
{
tickBox.Selected = !tickBox.Selected;
}
@@ -356,10 +356,10 @@ namespace Barotrauma
};
tickBox.RectTransform.MinSize = new Point(tickBox.Rect.Height, 0);
tickBox.RectTransform.IsFixedSize = true;
tickBox.Enabled = Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap);
tickBox.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap);
tickBox.OnSelected += (GUITickBox tb) =>
{
if (!Campaign.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; }
if (!CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap)) { return false; }
if (tb.Selected)
{
@@ -379,7 +379,7 @@ namespace Barotrauma
UpdateMaxMissions(connection.OtherLocation(currentDisplayLocation));
if ((Campaign is MultiPlayerCampaign multiPlayerCampaign) && !multiPlayerCampaign.SuppressStateSending &&
Campaign.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap))
CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.ManageMap))
{
GameMain.Client?.SendCampaignState();
}
@@ -472,7 +472,12 @@ namespace Barotrauma
{
TextGetter = () =>
{
return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{Campaign.NumberOfMissionsAtLocation(destination)}/{Campaign.Settings.TotalMaxMissionCount}");
int missionCount = 0;
if (GameMain.GameSession != null && Campaign.Map?.CurrentLocation?.SelectedMissions != null)
{
missionCount = Campaign.Map.CurrentLocation.SelectedMissions.Count(m => m.Locations.Contains(location) && !GameMain.GameSession.Missions.Contains(m));
}
return TextManager.AddPunctuation(':', TextManager.Get("Missions"), $"{missionCount}/{Campaign.Settings.TotalMaxMissionCount}");
}
};
@@ -500,7 +505,7 @@ namespace Barotrauma
return true;
},
Enabled = true,
Visible = Campaign.AllowedToManageCampaign(ClientPermissions.ManageMap)
Visible = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap)
};
buttonArea.RectTransform.MinSize = new Point(0, StartButton.RectTransform.MinSize.Y);

View File

@@ -351,7 +351,7 @@ namespace Barotrauma.CharacterEditor
{
if (string.IsNullOrEmpty(contentPackageNameElement.Text))
{
contentPackageNameElement.Flash();
contentPackageNameElement.Flash(useRectangleFlash: true);
return false;
}
if (ContentPackageManager.AllPackages.Any(cp => cp.Name.ToLower() == contentPackageNameElement.Text.ToLower()))
@@ -405,7 +405,7 @@ namespace Barotrauma.CharacterEditor
{
if (ContentPackage == null)
{
contentPackageDropDown.Flash();
contentPackageDropDown.Flash(useRectangleFlash: true);
return false;
}
@@ -417,7 +417,7 @@ namespace Barotrauma.CharacterEditor
if (!File.Exists(evaluatedTexturePath))
{
GUI.AddMessage(GetCharacterEditorTranslation("TextureDoesNotExist"), GUIStyle.Red);
texturePathElement.Flash(GUIStyle.Red);
texturePathElement.Flash(useRectangleFlash: true);
return false;
}
}
@@ -425,7 +425,7 @@ namespace Barotrauma.CharacterEditor
if (!path.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
{
GUI.AddMessage(TextManager.Get("WrongFileType"), GUIStyle.Red);
texturePathElement.Flash(GUIStyle.Red);
texturePathElement.Flash(useRectangleFlash: true);
return false;
}
if (IsCopy)
@@ -486,7 +486,8 @@ namespace Barotrauma.CharacterEditor
{
PlaySoundOnSelect = true,
};
var removeLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton")
var limbButtonSize = Vector2.One * 0.8f;
var removeLimbButton = new GUIButton(new RectTransform(limbButtonSize, limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIMinusButton")
{
OnClicked = (b, d) =>
{
@@ -497,7 +498,7 @@ namespace Barotrauma.CharacterEditor
return true;
}
};
var addLimbButton = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton")
var addLimbButton = new GUIButton(new RectTransform(limbButtonSize, limbEditLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIPlusButton")
{
OnClicked = (b, d) =>
{

View File

@@ -16,8 +16,8 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = true;
Hull.EditFire = false;
Hull.EditWater = false;
#endif
HumanAIController.DisableCrewAI = false;
#endif
}
protected virtual void DeselectEditorSpecific() { }

View File

@@ -334,7 +334,7 @@ namespace Barotrauma
OnClicked = (button, userData) =>
{
string url = TextManager.Get("EditorDisclaimerWikiUrl").Fallback("https://barotraumagame.com/wiki").Value;
GameMain.Instance.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice");
GameMain.ShowOpenUrlInWebBrowserPrompt(url, promptExtensionTag: "wikinotice");
return true;
}
};
@@ -1011,7 +1011,7 @@ namespace Barotrauma
GUI.DrawLine(spriteBatch, textPos, textPos - Vector2.UnitX * textSize.X, mouseOn ? Color.White : Color.White * 0.7f);
if (mouseOn && PlayerInput.PrimaryMouseButtonClicked())
{
GameMain.Instance.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com");
GameMain.ShowOpenUrlInWebBrowserPrompt("http://privacypolicy.daedalic.com");
}
}
textPos.Y -= textSize.Y;

View File

@@ -1817,7 +1817,11 @@ namespace Barotrauma
subList = dropDown.ListBox.Content;
}
var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), subList.RectTransform) { MinSize = new Point(0, 25) },
var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), subList.RectTransform)
{
//enough space for 2 lines (price and class) + some padding
MinSize = new Point(0, (int)(GUIStyle.SmallFont.LineHeight * 2.3f))
},
style: "ListBoxElement")
{
ToolTip = sub.Description,

View File

@@ -7,6 +7,8 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Xml.Linq;
namespace Barotrauma
@@ -953,8 +955,19 @@ namespace Barotrauma
okButton.Enabled = false;
okButton.OnClicked = (btn, userdata) =>
{
if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; }
JoinServer(endpoint, "");
if (Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint))
{
JoinServer(endpoint, "");
}
else if (LidgrenEndpoint.ParseFromWithHostNameCheck(endpointBox.Text, tryParseHostName: true).TryUnwrap(out var lidgrenEndpoint))
{
JoinServer(lidgrenEndpoint, "");
}
else
{
new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("invalidipaddress", "[serverip]:[port]", endpointBox.Text));
endpointBox.Flash();
}
msgBox.Close();
return false;
};

View File

@@ -379,6 +379,9 @@ namespace Barotrauma
void CreateSprite(ContentXElement element)
{
//empty element, probably an item variant?
if (element.Attributes().None()) { return; }
string spriteFolder = "";
ContentPath texturePath = null;

View File

@@ -1920,9 +1920,15 @@ namespace Barotrauma
{
filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}";
}
if (!modProject.Files.Any(f => f.Type == subFileType &&
f.Path == filePath))
if (!modProject.Files.Any(f => f.Type == subFileType && f.Path == filePath))
{
//check if there's a file with the same name but different filename case
var matchingFile = modProject.Files.FirstOrDefault(f => f.Type == subFileType && filePath.CleanUpPath().Equals(f.Path.CleanUpPath(), StringComparison.OrdinalIgnoreCase));
if (matchingFile != null)
{
File.Delete(matchingFile.Path.Replace(ContentPath.ModDirStr, packageDir));
modProject.RemoveFile(matchingFile);
}
var newFile = ModProject.File.FromPath(filePath, subFileType);
modProject.AddFile(newFile);
}
@@ -2479,7 +2485,7 @@ namespace Barotrauma
new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), tierGroup.RectTransform), NumberType.Int)
{
IntValue = SubmarineInfo.GetDefaultTier(MainSub.Info.Price),
IntValue = MainSub.Info.Tier,
MinValueInt = 1,
MaxValueInt = 3,
OnValueChanged = (numberInput) =>
@@ -2821,6 +2827,7 @@ namespace Barotrauma
OnClicked = (button, o) =>
{
var requiredPackages = MapEntity.mapEntityList.Select(e => e.Prefab.ContentPackage)
.Where(cp => cp != null)
.Distinct().OfType<ContentPackage>().Select(p => p.Name).ToHashSet();
var tickboxes = requiredContentPackList.Content.Children.OfType<GUITickBox>().ToArray();
tickboxes.ForEach(tb => tb.Selected = requiredPackages.Contains(tb.UserData as string ?? ""));
@@ -2919,7 +2926,7 @@ namespace Barotrauma
subTypeDropdown.SelectItem(MainSub.Info.Type);
if (quickSave) { SaveSub(null); }
if (quickSave) { SaveSub(packageToSaveInList.SelectedData as ContentPackage); }
}
private void CreateSaveAssemblyScreen()

View File

@@ -21,6 +21,7 @@ namespace Barotrauma
public static Character? dummyCharacter;
public static Effect? BlueprintEffect;
public TabMenu? TabMenu;
public TestScreen()
{
@@ -49,9 +50,10 @@ namespace Barotrauma
}
dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false);
dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.Where(jp => TalentTree.JobTalentTrees.ContainsKey(jp.Identifier)).GetRandom(Rand.RandSync.Unsynced));
dummyCharacter.Info.Job = new Job(JobPrefab.Prefabs.FirstOrDefault(static jp => jp.Identifier == "assistant"));
dummyCharacter.Info.Name = "Galldren";
dummyCharacter.Inventory.CreateSlots();
dummyCharacter.Info.GiveExperience(999999);
miniMapItem = new Item(ItemPrefab.Find(null, "deconstructor".ToIdentifier()), Vector2.Zero, null, 1337, false);
@@ -61,6 +63,7 @@ namespace Barotrauma
}
Character.Controlled = dummyCharacter;
GameMain.World.ProcessChanges();
TabMenu = new TabMenu();
}
public override void AddToGUIUpdateList()
@@ -68,35 +71,37 @@ namespace Barotrauma
Frame.AddToGUIUpdateList();
CharacterHUD.AddToGUIUpdateList(dummyCharacter);
dummyCharacter?.SelectedItem?.AddToGUIUpdateList();
TabMenu?.AddToGUIUpdateList();
}
public override void Update(double deltaTime)
{
base.Update(deltaTime);
TabMenu?.Update((float)deltaTime);
if (dummyCharacter is { } dummy && miniMapItem is { } item)
{
if (dummy.SelectedItem != item)
{
dummy.SelectedItem = item;
}
dummy.SelectedItem?.UpdateHUD(Cam, dummy, (float)deltaTime);
Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position);
foreach (Limb limb in dummy.AnimController.Limbs)
{
limb.body.SetTransform(pos, 0.0f);
}
if (dummy.AnimController?.Collider is { } collider)
{
collider.SetTransform(pos, 0);
}
dummy.ControlLocalPlayer((float)deltaTime, Cam, false);
dummy.Control((float)deltaTime, Cam);
}
// if (dummyCharacter is { } dummy && miniMapItem is { } item)
// {
// if (dummy.SelectedConstruction != item)
// {
// dummy.SelectedConstruction = item;
// }
//
// dummy.SelectedConstruction?.UpdateHUD(Cam, dummy, (float)deltaTime);
// Vector2 pos = FarseerPhysics.ConvertUnits.ToSimUnits(item.Position);
//
// foreach (Limb limb in dummy.AnimController.Limbs)
// {
// limb.body.SetTransform(pos, 0.0f);
// }
//
// if (dummy.AnimController?.Collider is { } collider)
// {
// collider.SetTransform(pos, 0);
// }
//
// dummy.ControlLocalPlayer((float)deltaTime, Cam, false);
// dummy.Control((float)deltaTime, Cam);
// }
}
public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)

View File

@@ -1332,16 +1332,18 @@ namespace Barotrauma
}
}
private void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property)
private static void TrySendNetworkUpdate(ISerializableEntity entity, SerializableProperty property)
{
if (entity is ItemComponent e)
if (GameMain.Client != null)
{
entity = e.Item;
}
if (GameMain.Client != null && entity is Item item)
{
GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property));
if (entity is Item item)
{
GameMain.Client.CreateEntityEvent(item, new Item.ChangePropertyEventData(property, item));
}
else if (entity is ItemComponent ic)
{
GameMain.Client.CreateEntityEvent(ic.Item, new Item.ChangePropertyEventData(property, ic));
}
}
}

View File

@@ -785,13 +785,7 @@ namespace Barotrauma
{
OnClicked = (btn, obj) =>
{
GameSettings.SetCurrentConfig(unsavedConfig);
if (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu &&
mutableWorkshopMenu.CurrentTab == MutableWorkshopMenu.Tab.InstalledMods)
{
mutableWorkshopMenu.Apply();
}
GameSettings.SaveCurrentConfig();
ApplyInstalledModChanges();
mainFrame.Flash(color: GUIStyle.Green);
return false;
},
@@ -804,6 +798,17 @@ namespace Barotrauma
};
}
public void ApplyInstalledModChanges()
{
GameSettings.SetCurrentConfig(unsavedConfig);
if (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu &&
mutableWorkshopMenu.CurrentTab == MutableWorkshopMenu.Tab.InstalledMods)
{
mutableWorkshopMenu.Apply();
}
GameSettings.SaveCurrentConfig();
}
public void Close()
{
if (GameMain.Client is null || GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled)

View File

@@ -21,6 +21,13 @@ namespace Barotrauma
private Entity soundEmitter;
private double loopStartTime;
private bool loopSound;
/// <summary>
/// Each new sound overrides the existing sounds that were launched with this status effect, meaning the old sound will be faded out and disposed and the new sound will be played instead of the old.
/// Normally the call to play the sound is ignored if there's an existing sound playing when the effect triggers.
/// Used for example for ensuring that rapid playing sounds restart playing even when the previous clip(s) have not yet stopped.
/// Use with caution.
/// </summary>
private bool forcePlaySounds;
partial void InitProjSpecific(ContentXElement element, string parentDebugName)
{
@@ -50,6 +57,7 @@ namespace Barotrauma
break;
}
}
forcePlaySounds = element.GetAttributeBool(nameof(forcePlaySounds), false);
}
partial void ApplyProjSpecific(float deltaTime, Entity entity, IReadOnlyList<ISerializableEntity> targets, Hull hull, Vector2 worldPosition, bool playSound)
@@ -71,7 +79,7 @@ namespace Barotrauma
{
angle = item.body.Rotation + ((item.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
particleRotation = -item.body.Rotation;
if (item.body.Dir < 0.0f)
if (emitter.Prefab.Properties.CopyEntityDir && item.body.Dir < 0.0f)
{
particleRotation += MathHelper.Pi;
mirrorAngle = true;
@@ -96,7 +104,9 @@ namespace Barotrauma
{
angle = targetLimb.body.Rotation + ((targetLimb.body.Dir > 0.0f) ? 0.0f : MathHelper.Pi);
particleRotation = -targetLimb.body.Rotation;
if (targetLimb.body.Dir < 0.0f)
float offset = targetLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
particleRotation += offset;
if (emitter.Prefab.Properties.CopyEntityDir && targetLimb.body.Dir < 0.0f)
{
particleRotation += MathHelper.Pi;
mirrorAngle = true;
@@ -112,10 +122,14 @@ namespace Barotrauma
private void PlaySound(Entity entity, Hull hull, Vector2 worldPosition)
{
if (sounds.Count == 0) return;
if (sounds.Count == 0) { return; }
if (soundChannel == null || !soundChannel.IsPlaying)
if (soundChannel == null || !soundChannel.IsPlaying || forcePlaySounds)
{
if (soundChannel != null && soundChannel.IsPlaying)
{
soundChannel.FadeOutAndDispose();
}
if (soundSelectionMode == SoundSelectionMode.All)
{
foreach (RoundSound sound in sounds)

View File

@@ -93,7 +93,7 @@ namespace Barotrauma.Steam
return (left, center, right);
}
private void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to)
private static void HandleDraggingAcrossModLists(GUIListBox from, GUIListBox to)
{
if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null)
{
@@ -197,7 +197,11 @@ namespace Barotrauma.Steam
out onInstalledInfoButtonHit, out var deselect);
GUILayoutGroup mainLayout =
new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter);
new GUILayoutGroup(new RectTransform(Vector2.One, outerContainer.Content.RectTransform), childAnchor: Anchor.TopCenter)
{
Stretch = true,
AbsoluteSpacing = GUI.IntScale(5)
};
mainLayout.RectTransform.SetAsFirstChild();
var (topLeft, _, topRight) = CreateSidebars(mainLayout, centerWidth: 0.05f, leftWidth: 0.475f, rightWidth: 0.475f, height: 0.13f);
@@ -257,7 +261,12 @@ namespace Barotrauma.Steam
right.ChildAnchor = Anchor.TopRight;
//enabled mods
Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont);
var label = Label(left, TextManager.Get("enabledregular"), GUIStyle.SubHeadingFont);
new GUIImage(new RectTransform(new Point(label.Rect.Height), label.RectTransform, Anchor.CenterRight), style: "GUIButtonInfo")
{
ToolTip = TextManager.Get("ModLoadOrderExplanation")
};
var enabledModsList = new GUIListBox(new RectTransform((1.0f, 0.93f), left.RectTransform))
{
CurrentDragMode = GUIListBox.DragMode.DragOutsideBox,
@@ -478,7 +487,7 @@ namespace Barotrauma.Steam
{
string str = modsListFilter.Text;
enabledRegularModsList.Content.Children.Concat(disabledRegularModsList.Content.Children)
.ForEach(c => c.Visible = !(c.UserData is ContentPackage p)
.ForEach(c => c.Visible = c.UserData is not ContentPackage p
|| ModNameMatches(p, str) && ModMatchesTickboxes(p, c));
}
@@ -504,12 +513,12 @@ namespace Barotrauma.Steam
//are enabled, and all files match either of them so show this mod
}
else if (modsListFilterTickboxes[Filter.ShowOnlySubs].Selected
&& p.Files.Any(f => !(f is BaseSubFile)))
&& p.Files.Any(f => f is not BaseSubFile))
{
matches = false;
}
else if (modsListFilterTickboxes[Filter.ShowOnlyItemAssemblies].Selected
&& p.Files.Any(f => !(f is ItemAssemblyFile)))
&& p.Files.Any(f => f is not ItemAssemblyFile))
{
matches = false;
}
@@ -520,7 +529,7 @@ namespace Barotrauma.Steam
private void PrepareToShowModInfo(ContentPackage mod)
{
if (!mod.UgcId.TryUnwrap(out var ugcId)
|| !(ugcId is SteamWorkshopId workshopId)) { return; }
|| ugcId is not SteamWorkshopId workshopId) { return; }
TaskPool.Add($"PrepareToShow{mod.UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value),
t =>
{

View File

@@ -598,13 +598,14 @@ namespace Barotrauma.Steam
bool reinstallAction(GUIButton button, object o)
{
SettingsMenu.Instance?.ApplyInstalledModChanges();
int prevIndex = ContentPackageManager.EnabledPackages.Regular.IndexOf(contentPackage);
TaskPool.AddIfNotFound($"Reinstall{workshopItem.Id}",
SteamManager.Workshop.Reinstall(workshopItem), t =>
{
ContentPackageManager.WorkshopPackages.Refresh();
ContentPackageManager.EnabledPackages.RefreshUpdatedMods();
if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu)
if (SettingsMenu.Instance?.WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu && !mutableWorkshopMenu.ViewingItemDetails)
{
mutableWorkshopMenu.PopulateInstalledModLists(forceRefreshEnabled: true);
}

View File

@@ -44,12 +44,26 @@ namespace Barotrauma.Steam
public MutableWorkshopMenu(GUIFrame parent) : base(parent)
{
var mainLayout
= new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false);
= new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: false)
{
Stretch = true,
AbsoluteSpacing = GUI.IntScale(4)
};
tabber = new GUILayoutGroup(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), isHorizontal: true)
{ Stretch = true };
tabContents = new Dictionary<Tab, (GUIButton Button, GUIFrame Content)>();
new GUIButton(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform, Anchor.BottomLeft),
style: "GUIButtonSmall", text: TextManager.Get("FindModsButton"))
{
OnClicked = (button, o) =>
{
SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/");
return false;
}
};
contentFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), mainLayout.RectTransform), style: null);
new GUICustomComponent(new RectTransform(Vector2.Zero, mainLayout.RectTransform),
@@ -130,17 +144,8 @@ namespace Barotrauma.Steam
{
tabContents[Tab.PopularMods].Button.Enabled = false;
}
GUIFrame listFrame = new GUIFrame(new RectTransform((1.0f, 0.95f), content.RectTransform), style: null);
GUIFrame listFrame = new GUIFrame(new RectTransform(Vector2.One, content.RectTransform), style: null);
CreateWorkshopItemList(listFrame, out _, out popularModsList, onSelected: PopulateFrameWithItemInfo);
new GUIButton(new RectTransform((1.0f, 0.05f), content.RectTransform, Anchor.BottomLeft),
style: "GUIButtonSmall", text: TextManager.Get("FindModsButton"))
{
OnClicked = (button, o) =>
{
SteamManager.OverlayCustomUrl($"https://steamcommunity.com/app/{SteamManager.AppID}/workshop/");
return false;
}
};
}
private void CreatePublishTab(out GUIListBox selfModsList)

View File

@@ -543,7 +543,8 @@ namespace Barotrauma.Steam
var localModProject = new ModProject(localPackage)
{
UgcId = Option<ContentPackageId>.Some(new SteamWorkshopId(resultId))
UgcId = Option<ContentPackageId>.Some(new SteamWorkshopId(resultId)),
ModVersion = modVersion
};
localModProject.DiscardHashAndInstallTime();
localModProject.Save(localPackage.Path);

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.19.11.0</Version>
<Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.19.11.0</Version>
<Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.19.11.0</Version>
<Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.19.11.0</Version>
<Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.19.11.0</Version>
<Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -70,7 +70,7 @@ namespace Barotrauma
msg.WriteByte((byte)Job.Variant);
foreach (SkillPrefab skillPrefab in Job.Prefab.Skills.OrderBy(s => s.Identifier))
{
msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier).Level);
msg.WriteSingle(Job.GetSkill(skillPrefab.Identifier)?.Level ?? 0.0f);
}
}
else

View File

@@ -219,9 +219,9 @@ namespace Barotrauma
else if (NetIdUtils.Difference(networkUpdateID, LastNetworkUpdateID) > 500)
{
#if DEBUG || UNSTABLE
DebugConsole.AddWarning($"Large disrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID.");
DebugConsole.AddWarning($"Large discrepancy between a client character's network update ID server-side and client-side (client: {networkUpdateID}, server: {LastNetworkUpdateID}). Resetting the ID.");
#endif
LastNetworkUpdateID = networkUpdateID;
LastNetworkUpdateID = LastProcessedID = networkUpdateID;
}
if (memInput.Count > 60)
{
@@ -549,7 +549,7 @@ namespace Barotrauma
msg.WriteByte((byte)statType);
foreach (var savedStatValue in Info.SavedStatValues[statType])
{
msg.WriteString(savedStatValue.StatIdentifier);
msg.WriteIdentifier(savedStatValue.StatIdentifier);
msg.WriteSingle(savedStatValue.StatValue);
msg.WriteBoolean(savedStatValue.RemoveOnDeath);
}

View File

@@ -1408,6 +1408,21 @@ namespace Barotrauma
GameMain.Server.PrintSenderTransters();
}));
AssignOnExecute("resetcharacternetstate", (string[] args) =>
{
if (GameMain.Server == null) { return; }
if (args.Length < 1)
{
ThrowError("Invalid parameters. The command should be formatted as \"resetcharacternetstate [character]\". If the names consist of multiple words, you should surround them with quotation marks.");
return;
}
var character = FindMatchingCharacter(args.Skip(1).ToArray(), false);
character?.ResetNetState();
});
commands.Add(new Command("eventdata", "", (string[] args) =>
{
if (args.Length == 0) { return; }

View File

@@ -158,7 +158,7 @@ namespace Barotrauma
XDocument doc = XMLExtensions.TryLoadXml(ServerSettings.SettingsFile);
if (doc?.Root == null)
{
DebugConsole.ThrowError("File \"" + ServerSettings.SettingsFile + "\" not found. Starting the server with default settings.");
DebugConsole.AddWarning("File \"" + ServerSettings.SettingsFile + "\" not found. Starting the server with default settings.");
}
else
{

View File

@@ -16,16 +16,15 @@ namespace Barotrauma
/// <summary>
/// There is a client-side implementation of the method in <see cref="CampaignMode"/>
/// </summary>
public bool AllowedToManageCampaign(Client client, ClientPermissions permissions)
public static bool AllowedToManageCampaign(Client client, ClientPermissions permissions)
{
//allow managing the campaign if the client has permissions, is the owner, or the only client in the server,
//or if no-one has management permissions
return
client.HasPermission(permissions) ||
client.HasPermission(ClientPermissions.ManageCampaign) ||
GameMain.Server.ConnectedClients.Count == 1 ||
IsOwner(client) ||
GameMain.Server.ConnectedClients.None(c => c.InGame && (IsOwner(c) || c.HasPermission(permissions)));
AnyOneAllowedToManageCampaign(permissions);
}
public bool AllowedToManageWallets(Client client)

View File

@@ -347,6 +347,8 @@ namespace Barotrauma
(GameMain.GameSession?.GameMode as MultiPlayerCampaign)?.SaveExperiencePoints(c);
}
}
// Event history must be registered before ending the round or it will be cleared
GameMain.GameSession.EventManager.RegisterEventHistory();
}
GameMain.GameSession.EndRound("", traitorResults, transitionType);
@@ -360,7 +362,6 @@ namespace Barotrauma
LeaveUnconnectedSubs(leavingSub);
NextLevel = newLevel;
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
GameMain.GameSession.EventManager.RegisterEventHistory();
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
}
else
@@ -1019,7 +1020,7 @@ namespace Barotrauma
UpgradeManager.PurchaseUpgrade(prefab, category, client: sender);
// unstable logging
int price = prefab.Price.GetBuyprice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation);
int price = prefab.Price.GetBuyPrice(UpgradeManager.GetUpgradeLevel(prefab, category), Map?.CurrentLocation);
int level = UpgradeManager.GetUpgradeLevel(prefab, category);
GameServer.Log($"SERVER: Purchased level {level} {category.Identifier}.{prefab.Identifier} for {price}", ServerLog.MessageType.ServerMessage);
}

View File

@@ -17,7 +17,7 @@ namespace Barotrauma.Items.Components
{
var allowOutpostAutoDocking = (AllowOutpostAutoDocking)msg.ReadByte();
if (outpostAutoDockingPromptShown &&
(GameMain.GameSession?.Campaign?.AllowedToManageCampaign(c, ClientPermissions.ManageMap) ?? false))
CampaignMode.AllowedToManageCampaign(c, ClientPermissions.ManageMap))
{
this.allowOutpostAutoDocking = allowOutpostAutoDocking;
}

View File

@@ -106,6 +106,14 @@ namespace Barotrauma
$"Failed to write a ChangeProperty network event for the item \"{Name}\" ({e.Message})");
}
break;
case SetItemStatEventData setItemStatEventData:
msg.WriteByte((byte)setItemStatEventData.Stats.Count);
foreach (var (key, value) in setItemStatEventData.Stats)
{
msg.WriteNetSerializableStruct(key);
msg.WriteSingle(value);
}
break;
case UpgradeEventData upgradeEventData:
var upgrade = upgradeEventData.Upgrade;
var upgradeTargets = upgrade.TargetComponents;

View File

@@ -152,7 +152,9 @@ namespace Barotrauma.Networking
public bool IsBanned(AccountId accountId, out string reason)
{
RemoveExpired();
var bannedPlayer = bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out AccountId id) && accountId.Equals(id));
var bannedPlayer =
bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out AccountId id) && accountId.Equals(id)) ??
bannedPlayers.Find(bp => bp.AddressOrAccountId.TryGet(out Address adr) && adr is SteamP2PAddress steamAdr && steamAdr.SteamId.Equals(accountId));
reason = bannedPlayer?.Reason ?? string.Empty;
return bannedPlayer != null;
}

View File

@@ -40,6 +40,8 @@ namespace Barotrauma.Networking
public float ChatSpamTimer;
public int ChatSpamCount;
public string RejectedName;
public int RoundsSincePlayedAsTraitor;
public float KickAFKTimer;
@@ -69,6 +71,9 @@ namespace Barotrauma.Networking
public DateTime JoinTime;
public static readonly TimeSpan NameChangeCoolDown = new TimeSpan(hours: 0, minutes: 0, seconds: 30);
public DateTime LastNameChangeTime;
private CharacterInfo characterInfo;
public CharacterInfo CharacterInfo
{

View File

@@ -486,9 +486,18 @@ namespace Barotrauma.Networking
// -> something wen't wrong during startup, re-enable start button and reset AutoRestartTimer
if (startGameCoroutine != null && !CoroutineManager.IsCoroutineRunning(startGameCoroutine))
{
if (ServerSettings.AutoRestart) ServerSettings.AutoRestartTimer = Math.Max(ServerSettings.AutoRestartInterval, 5.0f);
//GameMain.NetLobbyScreen.StartButtonEnabled = true;
if (ServerSettings.AutoRestart) { ServerSettings.AutoRestartTimer = Math.Max(ServerSettings.AutoRestartInterval, 5.0f); }
if (startGameCoroutine.Exception != null && OwnerConnection != null)
{
SendConsoleMessage(
startGameCoroutine.Exception.Message + '\n' +
(startGameCoroutine.Exception.StackTrace?.CleanupStackTrace() ?? "null"),
connectedClients.Find(c => c.Connection == OwnerConnection),
Color.Red);
}
EndGame();
GameMain.NetLobbyScreen.LastUpdateID++;
startGameCoroutine = null;
@@ -1377,9 +1386,9 @@ namespace Barotrauma.Networking
bool end = inc.ReadBoolean();
if (end)
{
if (mpCampaign == null ||
mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageRound) ||
mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign))
if (mpCampaign == null ||
CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageRound) ||
CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign))
{
bool save = inc.ReadBoolean();
if (GameStarted)
@@ -1409,7 +1418,7 @@ namespace Barotrauma.Networking
SendDirectChatMessage("Cannot continue the campaign from the previous save (round already running).", sender, ChatMessageType.Error);
break;
}
else if (mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))
else if (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))
{
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath);
}
@@ -1420,7 +1429,7 @@ namespace Barotrauma.Networking
Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage);
StartGame();
}
else if (mpCampaign != null && (mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || mpCampaign.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)))
else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)))
{
var availableTransition = mpCampaign.GetAvailableTransition(out _, out _);
//don't force location if we've teleported
@@ -1991,7 +2000,7 @@ namespace Barotrauma.Networking
//and assume the message was received, so we don't have to keep resending
//these large initial messages until the client acknowledges receiving them
c.LastRecvLobbyUpdate++;
c.LastRecvLobbyUpdate = GameMain.NetLobbyScreen.LastUpdateID;
}
else
@@ -2010,7 +2019,7 @@ namespace Barotrauma.Networking
c.ChatMsgQueue.RemoveAll(cMsg => !NetIdUtils.IdMoreRecent(cMsg.NetStateID, c.LastRecvChatMsgID));
for (int i = 0; i < c.ChatMsgQueue.Count && i < ChatMessage.MaxMessagesPerPacket; i++)
{
if (outmsg.LengthBytes + c.ChatMsgQueue[i].EstimateLengthBytesServer(c) > MsgConstants.MTU - 5)
if (outmsg.LengthBytes + c.ChatMsgQueue[i].EstimateLengthBytesServer(c) > MsgConstants.MTU - 5 && i > 0)
{
//not enough room in this packet
return;
@@ -2589,26 +2598,24 @@ namespace Barotrauma.Networking
public void EndGame(CampaignMode.TransitionType transitionType = CampaignMode.TransitionType.None, bool wasSaved = false)
{
if (!GameStarted)
if (GameStarted)
{
return;
}
if (GameSettings.CurrentConfig.VerboseLogging)
{
Log("Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(), ServerLog.MessageType.ServerMessage);
if (GameSettings.CurrentConfig.VerboseLogging)
{
Log("Ending the round...\n" + Environment.StackTrace.CleanupStackTrace(), ServerLog.MessageType.ServerMessage);
}
else
{
Log("Ending the round...", ServerLog.MessageType.ServerMessage);
}
else
{
Log("Ending the round...", ServerLog.MessageType.ServerMessage);
}
}
string endMessage = TextManager.FormatServerMessage("RoundSummaryRoundHasEnded");
var traitorResults = TraitorManager?.GetEndResults() ?? new List<TraitorMissionResult>();
List<Mission> missions = GameMain.GameSession.Missions.ToList();
if (GameMain.GameSession.IsRunning)
if (GameMain.GameSession is { IsRunning: true })
{
GameMain.GameSession.EndRound(endMessage, traitorResults);
}
@@ -2634,7 +2641,10 @@ namespace Barotrauma.Networking
c.PositionUpdateLastSent.Clear();
}
KarmaManager.OnRoundEnded();
if (GameStarted)
{
KarmaManager.OnRoundEnded();
}
RespawnManager = null;
GameStarted = false;
@@ -2703,9 +2713,24 @@ namespace Barotrauma.Networking
CharacterTeamType newTeam = (CharacterTeamType)inc.ReadByte();
if (c == null || string.IsNullOrEmpty(newName) || !NetIdUtils.IdMoreRecent(nameId, c.NameId)) { return false; }
var timeSinceNameChange = DateTime.Now - c.LastNameChangeTime;
if (timeSinceNameChange < Client.NameChangeCoolDown)
{
//only send once per second at most to prevent using this for spamming
if (timeSinceNameChange.TotalSeconds > 1)
{
var coolDownRemaining = Client.NameChangeCoolDown - timeSinceNameChange;
SendDirectChatMessage($"ServerMessage.NameChangeFailedCooldownActive~[seconds]={(int)coolDownRemaining.TotalSeconds}", c);
}
c.NameId = nameId;
c.RejectedName = newName;
return false;
}
if (!newJob.IsEmpty)
{
if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob)
if (!JobPrefab.Prefabs.TryGet(newJob, out JobPrefab newJobPrefab) || newJobPrefab.HiddenJob)
{
newJob = Identifier.Empty;
}
@@ -2721,26 +2746,25 @@ namespace Barotrauma.Networking
public bool TryChangeClientName(Client c, string newName)
{
newName = Client.SanitizeName(newName);
//update client list even if the name cannot be changed to the one sent by the client,
//so the client will be informed what their actual name is
LastClientListUpdateID++;
if (newName == c.Name || string.IsNullOrEmpty(newName)) { return false; }
if (IsNameValid(c, newName))
if (newName != c.Name && !string.IsNullOrEmpty(newName) && IsNameValid(c, newName))
{
c.LastNameChangeTime = DateTime.Now;
string oldName = c.Name;
c.Name = newName;
c.RejectedName = string.Empty;
SendChatMessage($"ServerMessage.NameChangeSuccessful~[oldname]={oldName}~[newname]={newName}", ChatMessageType.Server);
LastClientListUpdateID++;
return true;
}
else
{
//update client list even if the name cannot be changed to the one sent by the client,
//so the client will be informed what their actual name is
LastClientListUpdateID++;
return false;
}
}
private bool IsNameValid(Client c, string newName)
{
newName = Client.SanitizeName(newName);

View File

@@ -168,9 +168,10 @@ namespace Barotrauma.Networking
if (!bufferedEvent.Character.IsIncapacitated &&
NetIdUtils.IdMoreRecent(bufferedEvent.CharacterStateID, bufferedEvent.Character.LastProcessedID))
{
DebugConsole.Log($"Delaying reading entity event sent by a client until the character state has been processed. Event's character state: {bufferedEvent.CharacterStateID}, last processed character state: {bufferedEvent.Character.LastProcessedID}");
continue;
}
try
{
ReadEvent(bufferedEvent.Data, bufferedEvent.TargetEntity, bufferedEvent.Sender);

View File

@@ -250,7 +250,9 @@ namespace Barotrauma.Networking
structToSend = new ServerPeerContentPackageOrderPacket
{
ServerName = GameMain.Server.ServerName,
ContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile))
ContentPackages = ContentPackageManager.EnabledPackages.All
.Where(cp => cp.Files.Any())
.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile))
.Select(contentPackage => new ServerContentPackage(contentPackage, timeNow))
.ToImmutableArray()
};

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.19.11.0</Version>
<Version>0.20.0.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -442,6 +442,7 @@ namespace Barotrauma
base.Update(deltaTime);
UpdateTriggers(deltaTime);
Character.ClearInputs();
Reverse = false;
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f && (-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
if (steeringManager == insideSteering)
@@ -804,10 +805,6 @@ namespace Barotrauma
Reverse = true;
run = true;
}
else
{
Reverse = false;
}
SteeringManager.SteeringManual(deltaTime, dir * 0.2f);
}
else
@@ -1490,40 +1487,26 @@ namespace Barotrauma
canAttack = angle < MathHelper.ToRadians(AttackLimb.attack.RequiredAngle);
if (canAttack && AttackLimb.attack.AvoidFriendlyFire)
{
float minDistance = MathUtils.Pow(ConvertUnits.ToDisplayUnits(Character.AnimController.Collider.GetMaxExtent() * 3), 2);
bool IsFarEnough(Character other) => Vector2.DistanceSquared(Character.WorldPosition, other.WorldPosition) > minDistance;
if (SwarmBehavior != null)
canAttack = !IsBlocked(Character.GetRelativeSimPosition(SelectedAiTarget.Entity));
bool IsBlocked(Vector2 targetPosition)
{
canAttack = SwarmBehavior.Members.All(c => c == Character || IsFarEnough(c));
}
else
{
canAttack = Character.CharacterList.All(c => c == Character || !Character.IsFriendly(c) || IsFarEnough(c));
}
if (canAttack)
{
canAttack = !IsBlocked(attackSimPos) && !IsBlocked(AttackLimb.SimPosition + forward * ConvertUnits.ToSimUnits(AttackLimb.attack.Range));
bool IsBlocked(Vector2 targetPosition)
foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
{
foreach (var body in Submarine.PickBodies(AttackLimb.SimPosition, targetPosition, myBodies, Physics.CollisionCharacter))
Character hitTarget = null;
if (body.UserData is Character c)
{
Character hitTarget = null;
if (body.UserData is Character c)
{
hitTarget = c;
}
else if (body.UserData is Limb limb)
{
hitTarget = limb.character;
}
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
{
return true;
}
hitTarget = c;
}
else if (body.UserData is Limb limb)
{
hitTarget = limb.character;
}
if (hitTarget != null && !hitTarget.IsDead && Character.IsFriendly(hitTarget))
{
return true;
}
return false;
}
return false;
}
}
}
@@ -1854,7 +1837,33 @@ namespace Barotrauma
}
}
if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
if (AttackLimb is Limb attackLimb && attackLimb.attack.Ranged)
{
bool advance = !canAttack && Character.InWater || distance > attackLimb.attack.Range * 0.9f;
bool fallBack = canAttack && distance < Math.Min(250, attackLimb.attack.Range * 0.25f);
if (fallBack)
{
Reverse = true;
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
else if (advance)
{
if (pathSteering != null)
{
pathSteering.SteeringSeek(steerPos, weight: 10, minGapWidth: minGapSize);
}
else
{
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else if (!Character.InWater)
{
SteeringManager.Reset();
FaceTarget(SelectedAiTarget.Entity);
}
}
else if (!canAttack || distance > Math.Min(AttackLimb.attack.Range * 0.9f, 100))
{
if (pathSteering != null)
{
@@ -1865,20 +1874,30 @@ namespace Barotrauma
SteeringManager.SteeringSeek(steerPos, 10);
}
}
else if (AttackLimb.attack.Ranged)
{
// Too close
UpdateFallBack(attackWorldPos, deltaTime, followThrough: false);
}
if (Character.CurrentHull == null && (SelectedAiTarget?.Entity is Character c && c.Submarine == null || distance == 0 || distance > ConvertUnits.ToDisplayUnits(avoidLookAheadDistance * 2)))
{
SteeringManager.SteeringAvoid(deltaTime, lookAheadDistance: avoidLookAheadDistance, weight: 30);
}
}
}
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget?.Entity as IDamageable;
if (AttackLimb?.attack is Attack { Ranged: true} attack)
{
Limb limb = GetLimbToRotate(attack);
if (limb != null)
{
Vector2 toTarget = damageTarget.WorldPosition - limb.WorldPosition;
float offset = limb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
limb.body.SuppressSmoothRotationCalls = false;
float angle = MathUtils.VectorToAngle(toTarget);
limb.body.SmoothRotate(angle + offset, attack.AimRotationTorque);
limb.body.SuppressSmoothRotationCalls = true;
}
}
if (canAttack)
{
if (!UpdateLimbAttack(deltaTime, AttackLimb, attackSimPos, distance, attackTargetLimb))
if (!UpdateLimbAttack(deltaTime, attackSimPos, damageTarget, distance, attackTargetLimb))
{
IgnoreTarget(SelectedAiTarget);
}
@@ -2114,13 +2133,14 @@ namespace Barotrauma
}
// 10 dmg, 100 health -> 0.1
private float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f);
private static float GetRelativeDamage(float dmg, float vitality) => dmg / Math.Max(vitality, 1.0f);
private bool UpdateLimbAttack(float deltaTime, Limb attackingLimb, Vector2 attackSimPos, float distance = -1, Limb targetLimb = null)
private bool UpdateLimbAttack(float deltaTime, Vector2 attackSimPos, IDamageable damageTarget, float distance = -1, Limb targetLimb = null)
{
if (SelectedAiTarget?.Entity == null) { return false; }
if (attackingLimb?.attack == null) { return false; }
ActiveAttack = attackingLimb.attack;
if (AttackLimb?.attack == null) { return false; }
if (damageTarget == null) { return false; }
ActiveAttack = AttackLimb.attack;
if (wallTarget != null)
{
// If the selected target is not the wall target, make the wall target the selected target.
@@ -2131,83 +2151,94 @@ namespace Barotrauma
State = AIState.Attack;
}
}
IDamageable damageTarget = wallTarget != null ? wallTarget.Structure : SelectedAiTarget.Entity as IDamageable;
if (damageTarget != null)
if (damageTarget == null) { return false; }
if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
{
if (Character.Params.CanInteract && Character.Inventory != null)
Limb referenceLimb = GetLimbToRotate(ActiveAttack);
if (referenceLimb != null)
{
// Use equipped items (weapons)
Item item = GetEquippedItem(attackingLimb);
if (item != null)
Vector2 toTarget = damageTarget.WorldPosition - referenceLimb.WorldPosition;
float offset = referenceLimb.Params.GetSpriteOrientation() - MathHelper.PiOver2;
Vector2 forward = VectorExtensions.Forward(referenceLimb.body.TransformedRotation - offset * referenceLimb.Dir);
float angle = MathHelper.ToDegrees(VectorExtensions.Angle(forward, toTarget));
if (angle > ActiveAttack.RequiredAngleToShoot)
{
if (item.RequireAimToUse)
{
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
{
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
}
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
return true;
}
}
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (!ActiveAttack.IsRunning)
}
if (Character.Params.CanInteract && Character.Inventory != null)
{
// Use equipped items (weapons)
Item item = GetEquippedItem(AttackLimb);
if (item != null)
{
if (item.RequireAimToUse)
{
if (!Aim(deltaTime, damageTarget as ISpatialEntity, item))
{
// Valid target, but can't shoot -> return true so that it will not be ignored.
return true;
}
}
Character.SetInput(item.IsShootable ? InputType.Shoot : InputType.Use, false, true);
item.Use(deltaTime, Character);
}
}
//simulate attack input to get the character to attack client-side
Character.SetInput(InputType.Attack, true, true);
if (!ActiveAttack.IsRunning)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
attackingLimb,
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif
}
}
if (attackingLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
if (AttackLimb.UpdateAttack(deltaTime, attackSimPos, damageTarget, out AttackResult attackResult, distance, targetLimb))
{
if (ActiveAttack.CoolDownTimer > 0)
{
if (attackingLimb.attack.CoolDownTimer > 0)
SetAimTimer(Math.Min(ActiveAttack.CoolDown, 1.5f));
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (damageTarget is not Barotrauma.Character)
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
{
LatchOntoAI.SetAttachTarget(targetCharacter);
}
if (!ActiveAttack.Ranged)
{
if (damageTarget.Health > 0 && attackResult.Damage > 0)
{
SetAimTimer(Math.Min(attackingLimb.attack.CoolDown, 1.5f));
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (!(damageTarget is Character))
if (damageTarget is not Barotrauma.Character)
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
if (LatchOntoAI != null && SelectedAiTarget.Entity is Character targetCharacter)
else
{
LatchOntoAI.SetAttachTarget(targetCharacter);
}
if (!attackingLimb.attack.Ranged)
{
if (damageTarget.Health > 0 && attackResult.Damage > 0)
{
// Managed to hit a living/non-destroyed target. Increase the priority more if the target is low in health -> dies easily/soon
float greed = AIParams.AggressionGreed;
if (!(damageTarget is Character))
{
// Halve the greed for attacking non-characters.
greed /= 2;
}
selectedTargetMemory.Priority += GetRelativeDamage(attackResult.Damage, damageTarget.Health) * greed;
}
else
{
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
return selectedTargetMemory.Priority > 1;
}
selectedTargetMemory.Priority -= Math.Max(selectedTargetMemory.Priority / 2, 1);
return selectedTargetMemory.Priority > 1;
}
}
return true;
}
return false;
return true;
}
private float aimTimer;
@@ -2299,7 +2330,6 @@ namespace Barotrauma
{
if (attackVector == null)
{
// TODO: test adding some random variance here?
attackVector = attackWorldPos - WorldPosition;
}
Vector2 dir = Vector2.Normalize(followThrough ? attackVector.Value : -attackVector.Value);
@@ -2319,6 +2349,16 @@ namespace Barotrauma
return true;
}
private Limb GetLimbToRotate(Attack attack)
{
Limb limb = AttackLimb;
if (attack.RotationLimbIndex > -1 && attack.RotationLimbIndex < Character.AnimController.Limbs.Length)
{
limb = Character.AnimController.Limbs[attack.RotationLimbIndex];
}
return limb;
}
#endregion
#region Eat
@@ -3429,7 +3469,7 @@ namespace Barotrauma
private void ChangeParams(string tag, AIState state, float? priority = null, bool onlyExisting = false)
=> ChangeParams(tag.ToIdentifier(), state, priority, onlyExisting);
private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false)
private void ChangeParams(Identifier tag, AIState state, float? priority = null, bool onlyExisting = false, bool ignoreAttacksIfNotInSameSub = false)
{
if (!AIParams.TryGetTarget(tag, out CharacterParams.TargetParams targetParams))
{
@@ -3437,6 +3477,11 @@ namespace Barotrauma
{
if (AIParams.TryAddNewTarget(tag, state, priority ?? minPriority, out targetParams))
{
if (state == AIState.Attack)
{
// Only applies to new temp target params. Shouldn't affect any existing definitions (handled below).
targetParams.IgnoreIfNotInSameSub = ignoreAttacksIfNotInSameSub;
}
tempParams.Add(tag, targetParams);
}
}
@@ -3470,7 +3515,7 @@ namespace Barotrauma
{
isStateChanged = true;
SetStateResetTimer();
ChangeParams(target.SpeciesName, state, priority);
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
if (target.IsHuman)
{
priority = GetTargetParams("human")?.Priority;

View File

@@ -748,6 +748,9 @@ namespace Barotrauma
}
if (!character.HasEquippedItem(Weapon, predicate: IsHandSlotType))
{
//clear aim and shoot inputs so the bot doesn't immediately fire the weapon if it was previously e.g. using a scooter
character.ClearInput(InputType.Aim);
character.ClearInput(InputType.Shoot);
Weapon.TryInteract(character, forceSelectKey: true);
var slots = Weapon.AllowedSlots.Where(s => IsHandSlotType(s));
if (character.Inventory.TryPutItem(Weapon, character, slots))
@@ -764,7 +767,7 @@ namespace Barotrauma
}
return true;
bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
static bool IsHandSlotType(InvSlotType s) => s == InvSlotType.LeftHand || s == InvSlotType.RightHand || s == (InvSlotType.LeftHand | InvSlotType.RightHand);
}
private float findHullTimer;

View File

@@ -186,8 +186,8 @@ namespace Barotrauma
{
if (character.SelectedItem != Item)
{
if (Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true) ||
Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true))
if (Item.TryInteract(character, ignoreRequiredItems: true, forceUseKey: true) ||
Item.TryInteract(character, ignoreRequiredItems: true, forceSelectKey: true))
{
character.SelectedItem = Item;
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using static Barotrauma.CharacterParams;
namespace Barotrauma
{
@@ -44,7 +45,7 @@ namespace Barotrauma
public float PlayTimer { get; set; }
private float? unstunY { get; set; }
public EnemyAIController AiController { get; private set; } = null;
public EnemyAIController AIController { get; private set; } = null;
public Character Owner { get; set; }
@@ -134,8 +135,8 @@ namespace Barotrauma
aggregate += Items[i].Commonness;
if (aggregate >= r && Items[i].Prefab != null)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AiController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AiController.Character.WorldPosition);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetProducedItem:" + pet.AIController.Character.SpeciesName + ":" + Items[i].Prefab.Identifier);
Entity.Spawner.AddItemToSpawnQueue(Items[i].Prefab, pet.AIController.Character.WorldPosition);
break;
}
}
@@ -160,8 +161,8 @@ namespace Barotrauma
public PetBehavior(XElement element, EnemyAIController aiController)
{
AiController = aiController;
AiController.Character.CanBeDragged = true;
AIController = aiController;
AIController.Character.CanBeDragged = true;
MaxHappiness = element.GetAttributeFloat("maxhappiness", 100.0f);
MaxHunger = element.GetAttributeFloat("maxhunger", 100.0f);
@@ -218,7 +219,7 @@ namespace Barotrauma
bool success = OnEat(item.GetTags());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + item.Prefab.Identifier);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + item.Prefab.Identifier);
}
return success;
}
@@ -229,7 +230,7 @@ namespace Barotrauma
bool success = OnEat("dead".ToIdentifier());
if (success)
{
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AiController.Character.SpeciesName + ":" + character.SpeciesName);
GameAnalyticsManager.AddDesignEvent("MicroInteraction:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "null") + ":PetEat:" + AIController.Character.SpeciesName + ":" + character.SpeciesName);
}
return success;
}
@@ -252,7 +253,7 @@ namespace Barotrauma
Hunger += foods[i].Hunger;
Happiness += foods[i].Happiness;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.5f);
#endif
return true;
}
@@ -265,20 +266,20 @@ namespace Barotrauma
if (PlayTimer > 0.0f) { return; }
if (Owner == null) { Owner = player; }
PlayTimer = 5.0f;
AiController.Character.IsRagdolled = true;
AIController.Character.IsRagdolled = true;
Happiness += 10.0f;
AiController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AiController.Character.SimPosition.Y;
AIController.Character.AnimController.MainLimb.body.LinearVelocity += new Vector2(0, PlayForce);
unstunY = AIController.Character.SimPosition.Y;
#if CLIENT
AiController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
AIController.Character.PlaySound(CharacterSound.SoundType.Happy, 0.9f);
#endif
}
public string GetTagName()
{
if (AiController.Character.Inventory != null)
if (AIController.Character.Inventory != null)
{
foreach (Item item in AiController.Character.Inventory.AllItems)
foreach (Item item in AIController.Character.Inventory.AllItems)
{
var tag = item.GetComponent<NameTag>();
if (tag != null && !string.IsNullOrWhiteSpace(tag.WrittenName))
@@ -293,7 +294,7 @@ namespace Barotrauma
public void Update(float deltaTime)
{
var character = AiController.Character;
var character = AIController.Character;
if (character?.Removed ?? true || character.IsDead) { return; }
if (unstunY.HasValue)
@@ -332,16 +333,27 @@ namespace Barotrauma
Food food = foods[i];
if (Hunger >= food.HungerRange.X && Hunger <= food.HungerRange.Y)
{
if (food.TargetParams == null &&
AiController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out CharacterParams.TargetParams targetParams))
if (food.TargetParams == null)
{
targetParams.IgnoreContained = food.IgnoreContained;
food.TargetParams = targetParams;
if (AIController.AIParams.TryGetTarget(food.Tag, out TargetParams target))
{
food.TargetParams = target;
}
else if (AIController.AIParams.TryAddNewTarget(food.Tag, AIState.Eat, food.Priority, out TargetParams targetParams))
{
food.TargetParams = targetParams;
}
if (food.TargetParams != null)
{
food.TargetParams.State = AIState.Eat;
food.TargetParams.Priority = food.Priority;
food.TargetParams.IgnoreContained = food.IgnoreContained;
}
}
}
else if (food.TargetParams != null)
{
AiController.AIParams.RemoveTarget(food.TargetParams);
AIController.AIParams.RemoveTarget(food.TargetParams);
food.TargetParams = null;
}
}

View File

@@ -116,10 +116,10 @@ namespace Barotrauma
}
// accept only the highest priority order
if (CurrentOrder != null && OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder)
if (CurrentOrder == null || OrderedCharacter.GetCurrentOrderWithTopPriority() != CurrentOrder)
{
#if DEBUG
ShipCommandManager.ShipCommandLog($"Order {CurrentOrder.Name} did not match current order for character {OrderedCharacter} in {this}");
ShipCommandManager.ShipCommandLog($"{this} is no longer the top priority of {OrderedCharacter}, considering the issue unattended.");
#endif
return false;
}

View File

@@ -356,7 +356,7 @@ namespace Barotrauma
ShipIssueWorkers.Add(new ShipIssueWorkerSteer(this, order));
}
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret")))
foreach (Item item in CommandedSubmarine.GetItems(true).FindAll(i => i.HasTag("turret") && !i.HasTag("hardpoint")))
{
var order = new Order(OrderPrefab.Prefabs["operateweapons"], item, item.GetComponent<Turret>());
ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order));

View File

@@ -87,7 +87,7 @@ namespace Barotrauma
}
public bool CanWalk => RagdollParams.CanWalk;
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir);
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir) && CurrentAnimationParams is not FishGroundedParams { Flip: false };
// TODO: define death anim duration in XML
protected float deathAnimTimer, deathAnimDuration = 5.0f;

View File

@@ -610,15 +610,18 @@ namespace Barotrauma
torsoAngle -= herpesStrength / 150.0f;
torso.body.SmoothRotate(torsoAngle * Dir, CurrentGroundedParams.TorsoTorque);
}
if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
if (!head.Disabled)
{
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
}
else
{
RotateHead(head);
if (!Aiming && CurrentGroundedParams.FixedHeadAngle && HeadAngle.HasValue)
{
float headAngle = HeadAngle.Value;
if (Crouching && !movingHorizontally) { headAngle -= HumanCrouchParams.ExtraHeadAngleWhenStationary; }
head.body.SmoothRotate(headAngle * Dir, CurrentGroundedParams.HeadTorque);
}
else
{
RotateHead(head);
}
}
if (!onGround)
@@ -1389,7 +1392,7 @@ namespace Barotrauma
target.Oxygen += deltaTime * 0.5f; //Stabilize them
}
bool powerfulCPR = character.HasAbilityFlag(AbilityFlags.PowerfulCPR);
float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
int skill = (int)character.GetSkillLevel("medical");
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
@@ -1406,7 +1409,7 @@ namespace Barotrauma
{
if (target.Oxygen < -10.0f)
{
if (powerfulCPR)
if (cprBoost >= 1f)
{
//prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f);
@@ -1453,7 +1456,7 @@ namespace Barotrauma
reviveChance = (float)Math.Pow(reviveChance, CPRSettings.Active.ReviveChanceExponent);
reviveChance = MathHelper.Clamp(reviveChance, CPRSettings.Active.ReviveChanceMin, CPRSettings.Active.ReviveChanceMax);
if (powerfulCPR) { reviveChance *= 2.0f; }
reviveChance *= 1f + cprBoost;
if (Rand.Range(0.0f, 1.0f, Rand.RandSync.ServerAndClient) <= reviveChance)
{

View File

@@ -873,7 +873,7 @@ namespace Barotrauma
foreach (Limb limb in Limbs)
{
if (limb == null || limb.IsSevered) { continue; }
if (limb == null || limb.IsSevered || !limb.DoesFlip) { continue; }
limb.Dir = Dir;
limb.MouthPos = new Vector2(-limb.MouthPos.X, limb.MouthPos.Y);
limb.MirrorPullJoint();
@@ -1337,7 +1337,7 @@ namespace Barotrauma
bool limbsValid = true;
foreach (Limb limb in limbs)
{
if (limb.body == null || !limb.body.Enabled) { continue; }
if (limb?.body == null || !limb.body.Enabled) { continue; }
if (!CheckValidity(limb.body))
{
limbsValid = false;
@@ -1959,7 +1959,7 @@ namespace Barotrauma
{
foreach (Limb l in Limbs)
{
l.Remove();
l?.Remove();
}
limbs = null;
}
@@ -1968,7 +1968,7 @@ namespace Barotrauma
{
foreach (PhysicsBody b in collider)
{
b.Remove();
b?.Remove();
}
collider = null;
}
@@ -1977,7 +1977,7 @@ namespace Barotrauma
{
foreach (var joint in LimbJoints)
{
var j = joint.Joint;
var j = joint?.Joint;
if (GameMain.World.JointList.Contains(j))
{
GameMain.World.Remove(j);

View File

@@ -189,6 +189,15 @@ namespace Barotrauma
[Serialize(20f, IsPropertySaveable.Yes)]
public float RequiredAngle { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "By default uses the same value as RequiredAngle. Use if you want to allow selecting the attack but not shooting until the angle is smaller. Only affects ranged attacks."), Editable]
public float RequiredAngleToShoot { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the attack limb is rotated towards the target. Default 0 = no rotation. Only affects ranged attacks."), Editable]
public float AimRotationTorque { get; set; }
[Serialize(-1, IsPropertySaveable.Yes, description: "Reference to the limb we apply the aim rotation to. By default same as the attack limb. Only affects ranged attacks."), Editable]
public int RotationLimbIndex { get; set; }
/// <summary>
/// Legacy support. Use Afflictions.
/// </summary>
@@ -529,6 +538,12 @@ namespace Barotrauma
effect.Apply(effectType, deltaTime, targetEntity, attacker, worldPosition);
}
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
}
}
return attackResult;
@@ -591,6 +606,12 @@ namespace Barotrauma
{
effect.Apply(effectType, deltaTime, targetLimb.character, attacker, worldPosition);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.Clear();
targets.AddRange(attacker.Inventory.AllItems);
effect.Apply(effectType, deltaTime, attacker, targets);
}
}
return attackResult;

View File

@@ -129,6 +129,7 @@ namespace Barotrauma
public bool IsCommanding => IsPlayer || (AIController is HumanAIController humanAI && humanAI.ShipCommandManager != null && humanAI.ShipCommandManager.Active);
public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled;
public bool IsEscorted { get; set; }
public Identifier JobIdentifier => Info?.Job?.Prefab.Identifier ?? Identifier.Empty;
public readonly Dictionary<Identifier, SerializableProperty> Properties;
public Dictionary<Identifier, SerializableProperty> SerializableProperties
@@ -611,7 +612,9 @@ namespace Barotrauma
CharacterHealth.SetHealthBarVisibility(value == null);
#endif
bool isServerOrSingleplayer = GameMain.IsSingleplayer || GameMain.NetworkMember is { IsServer: true };
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance } grabbedWallet } && balance > 0)
CheckTalents(AbilityEffectType.OnLootCharacter, new AbilityCharacterLoot(value));
if (IsPlayer && isServerOrSingleplayer && value is { IsDead: true, Wallet: { Balance: var balance and > 0 } grabbedWallet })
{
#if SERVER
if (GameMain.GameSession.Campaign is MultiPlayerCampaign mpCampaign && GameMain.Server is { ServerSettings: { } settings })
@@ -999,7 +1002,7 @@ namespace Barotrauma
}
}
public bool InWater => AnimController?.InWater ?? false;
public bool InWater => AnimController is AnimController { InWater: true };
public bool GodMode = false;
@@ -1053,6 +1056,8 @@ namespace Barotrauma
}
}
public HashSet<Identifier> MarkedAsLooted = new();
public bool IsInFriendlySub => Submarine != null && Submarine.TeamID == TeamID;
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
@@ -1574,14 +1579,23 @@ namespace Barotrauma
}
if (createNetworkEvent && GameMain.NetworkMember is { IsServer: true })
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()]));
GameMain.NetworkMember.CreateEntityEvent(item, new Item.ChangePropertyEventData(item.SerializableProperties[nameof(item.Tags).ToIdentifier()], item));
}
}
}
public float GetSkillLevel(string skillIdentifier) =>
GetSkillLevel(skillIdentifier.ToIdentifier());
private static readonly ImmutableDictionary<Identifier, StatTypes> overrideStatTypes = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillOverride },
{ new("medical"), StatTypes.MedicalSkillOverride },
{ new("weapons"), StatTypes.WeaponsSkillOverride },
{ new("electrical"), StatTypes.ElectricalSkillOverride },
{ new("mechanical"), StatTypes.MechanicalSkillOverride }
}.ToImmutableDictionary();
public float GetSkillLevel(Identifier skillIdentifier)
{
if (Info?.Job == null) { return 0.0f; }
@@ -1617,6 +1631,16 @@ namespace Barotrauma
skillLevel += GetStatValue(GetSkillStatType(skillIdentifier));
if (overrideStatTypes.TryGetValue(skillIdentifier, out StatTypes statType))
{
float skillOverride = GetStatValue(statType);
if (skillOverride > skillLevel)
{
skillLevel = skillOverride;
}
}
return skillLevel;
}
@@ -2058,30 +2082,42 @@ namespace Barotrauma
{
foreach (Item item in HeldItems)
{
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
tryUseItem(item, deltaTime);
}
foreach (Item item in Inventory.AllItems)
{
if (item.GetComponent<Wearable>() is { AllowUseWhenWorn: true } && HasEquippedItem(item))
{
item.SecondaryUse(deltaTime, this);
tryUseItem(item, deltaTime);
}
if (IsKeyDown(InputType.Use) && !item.IsShootable)
}
}
void tryUseItem(Item item, float deltaTime)
{
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
{
item.SecondaryUse(deltaTime, this);
}
if (IsKeyDown(InputType.Use) && !item.IsShootable)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
item.Use(deltaTime, this);
}
item.Use(deltaTime, this);
}
if (IsKeyDown(InputType.Shoot) && item.IsShootable)
}
if (IsKeyDown(InputType.Shoot) && item.IsShootable)
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
{
item.Use(deltaTime, this);
}
item.Use(deltaTime, this);
}
#if CLIENT
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
{
HintManager.OnShootWithoutAiming(this, item);
}
#endif
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
{
HintManager.OnShootWithoutAiming(this, item);
}
#endif
}
}
@@ -2721,6 +2757,11 @@ namespace Barotrauma
}
}
bool selectInputSameAsDeselect = false;
#if CLIENT
selectInputSameAsDeselect = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Select] == GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Deselect];
#endif
if (SelectedCharacter != null && (IsKeyHit(InputType.Grab) || IsKeyHit(InputType.Health))) //Let people use ladders and buttons and stuff when dragging chars
{
DeselectCharacter();
@@ -2760,14 +2801,16 @@ namespace Barotrauma
{
FocusedCharacter.onCustomInteract(FocusedCharacter, this);
}
else if (IsKeyHit(InputType.Deselect) && SelectedItem != null)
else if (IsKeyHit(InputType.Deselect) && SelectedItem != null &&
(focusedItem == null || focusedItem == SelectedItem || !selectInputSameAsDeselect))
{
SelectedItem = null;
#if CLIENT
CharacterHealth.OpenHealthWindow = null;
#endif
}
else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null)
else if (IsKeyHit(InputType.Deselect) && SelectedSecondaryItem != null && SelectedSecondaryItem.GetComponent<Ladder>() == null &&
(focusedItem == null || focusedItem == SelectedSecondaryItem || !selectInputSameAsDeselect))
{
SelectedSecondaryItem = null;
#if CLIENT
@@ -2782,6 +2825,10 @@ namespace Barotrauma
{
#if CLIENT
if (CharacterInventory.DraggingItemToWorld) { return; }
if (selectInputSameAsDeselect)
{
keys[(int)InputType.Deselect].Reset();
}
#endif
bool canInteract = focusedItem.TryInteract(this);
#if CLIENT
@@ -3787,7 +3834,7 @@ namespace Barotrauma
return;
}
#endif
if (damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (damage > 0 && damage < targetLimb.Params.MinSeveranceDamage) { return; }
if (!IsDead)
{
if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
@@ -3805,7 +3852,7 @@ namespace Barotrauma
var referenceLimb = targetLimb.type == LimbType.Head && targetLimb.Params.ID == 0 ? joint.LimbA : joint.LimbB;
if (referenceLimb != targetLimb) { continue; }
float probability = severLimbsProbability;
if (!IsDead)
if (!IsDead && probability < 1)
{
probability *= joint.Params.SeveranceProbabilityModifier;
}
@@ -4778,6 +4825,32 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
private readonly HashSet<Hull> sameRoomHulls = new();
/// <summary>
/// Check if the character is in the same room
/// Room and hull differ in that a room can consist of multiple linked hulls
/// </summary>
public bool IsInSameRoomAs(Character character)
{
if (character == this) { return true; }
if (character.CurrentHull is null || CurrentHull is null)
{
// Outside doesn't count as a room
return false;
}
if (character.Submarine != Submarine) { return false; }
if (character.CurrentHull == CurrentHull) { return true; }
sameRoomHulls.Clear();
CurrentHull.GetLinkedEntities(sameRoomHulls);
sameRoomHulls.Add(CurrentHull);
return sameRoomHulls.Contains(character.CurrentHull);
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))
@@ -4786,7 +4859,7 @@ namespace Barotrauma
{
foreach (TalentOption talentOption in talentSubTree.TalentOptionStages)
{
if (talentOption.TalentIdentifiers.None(t => HasTalent(t)))
if (talentOption.TalentIdentifiers.None(HasTalent))
{
return false;
}
@@ -4831,6 +4904,19 @@ namespace Barotrauma
return characterTalents.Any(t => t.UnlockedRecipes.Contains(recipeIdentifier));
}
public bool HasStoreAccessForItem(ItemPrefab prefab)
{
foreach (CharacterTalent talent in characterTalents)
{
foreach (Identifier unlockedItem in talent.UnlockedStoreItems)
{
if (prefab.Tags.Contains(unlockedItem)) { return true; }
}
}
return false;
}
/// <summary>
/// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains.
/// </summary>
@@ -5043,6 +5129,16 @@ namespace Barotrauma
}
}
internal sealed class AbilityCharacterLoot : AbilityObject, IAbilityCharacter
{
public Character Character { get; set; }
public AbilityCharacterLoot(Character character)
{
Character = character;
}
}
class AbilityCharacterKill : AbilityObject, IAbilityCharacter
{
public AbilityCharacterKill(Character character, Character killer)

View File

@@ -543,7 +543,7 @@ namespace Barotrauma
private void GetName(Rand.RandSync randSync, out string name)
{
var nameElement = CharacterConfigElement.GetChildElement("names") ?? CharacterConfigElement.GetChildElement("name");
ContentXElement nameElement = CharacterConfigElement.GetChildElement("names") ?? CharacterConfigElement.GetChildElement("name");
ContentPath namesXmlFile = nameElement?.GetAttributeContentPath("path") ?? ContentPath.Empty;
XElement namesXml = null;
if (!namesXmlFile.IsNullOrEmpty()) //names.xml is defined
@@ -554,8 +554,8 @@ namespace Barotrauma
else //the legacy firstnames.txt/lastnames.txt shit is defined
{
namesXml = new XElement("names", new XAttribute("format", "[firstname] [lastname]"));
var firstNamesPath = ReplaceVars(nameElement.GetAttributeContentPath("firstname")?.Value ?? "");
var lastNamesPath = ReplaceVars(nameElement.GetAttributeContentPath("lastname")?.Value ?? "");
string firstNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("firstname")?.Value ?? "");
string lastNamesPath = nameElement == null ? string.Empty : ReplaceVars(nameElement.GetAttributeContentPath("lastname")?.Value ?? "");
if (File.Exists(firstNamesPath) && File.Exists(lastNamesPath))
{
var firstNames = File.ReadAllLines(firstNamesPath);
@@ -735,9 +735,7 @@ namespace Barotrauma
Name = infoElement.GetAttributeString("name", "");
OriginalName = infoElement.GetAttributeString("originalname", null);
Salary = infoElement.GetAttributeInt("salary", 1000);
ExperiencePoints = infoElement.GetAttributeInt("experiencepoints", 0);
UnlockedTalents = new HashSet<Identifier>(infoElement.GetAttributeIdentifierArray("unlockedtalents", Array.Empty<Identifier>()));
AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0);
HashSet<Identifier> tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToHashSet();
LoadTagsBackwardsCompatibility(infoElement, tags);
@@ -813,18 +811,22 @@ namespace Barotrauma
infoElement.GetAttributeIdentifier("npcid", Identifier.Empty));
MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0);
UnlockedTalents = new HashSet<Identifier>();
foreach (var subElement in infoElement.Elements())
{
bool jobCreated = false;
if (subElement.Name.ToString().Equals("job", StringComparison.OrdinalIgnoreCase) && !jobCreated)
Identifier elementName = subElement.Name.ToIdentifier();
if (elementName == "job" && !jobCreated)
{
Job = new Job(subElement);
jobCreated = true;
// there used to be a break here, but it had to be removed to make room for statvalues
// using the jobCreated boolean to make sure that only the first job found is created
}
else if (subElement.Name.ToString().Equals("savedstatvalues", StringComparison.OrdinalIgnoreCase))
else if (elementName == "savedstatvalues")
{
foreach (XElement savedStat in subElement.Elements())
{
@@ -838,8 +840,8 @@ namespace Barotrauma
float value = savedStat.GetAttributeFloat("statvalue", 0f);
if (value == 0f) { continue; }
string statIdentifier = savedStat.GetAttributeString("statidentifier", "").ToLowerInvariant();
if (string.IsNullOrEmpty(statIdentifier))
Identifier statIdentifier = savedStat.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError("Stat identifier not specified for Stat Value when loading character data in CharacterInfo!");
return;
@@ -849,6 +851,20 @@ namespace Barotrauma
ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath);
}
}
else if (elementName == "talents")
{
Version version = subElement.GetAttributeVersion("version", GameMain.Version); // for future maybe
foreach (XElement talentElement in subElement.Elements())
{
if (talentElement.Name.ToIdentifier() != "talent") { continue; }
Identifier talentIdentifier = talentElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (talentIdentifier == Identifier.Empty) { continue; }
UnlockedTalents.Add(talentIdentifier);
}
}
}
LoadHeadAttachments();
}
@@ -1149,13 +1165,17 @@ namespace Barotrauma
increase *= 1f + Character.GetStatValue(StatTypes.SkillGainSpeed);
increase = GetSkillSpecificGain(increase, skillIdentifier);
float prevLevel = Job.GetSkillLevel(skillIdentifier);
Job.IncreaseSkillLevel(skillIdentifier, increase, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
float newLevel = Job.GetSkillLevel(skillIdentifier);
if ((int)newLevel > (int)prevLevel)
{
{
float extraLevel = Character.GetStatValue(StatTypes.ExtraLevelGain);
Job.IncreaseSkillLevel(skillIdentifier, extraLevel, Character.HasAbilityFlag(AbilityFlags.GainSkillPastMaximum));
// assume we are getting at least 1 point in skill, since this logic only runs in such cases
float increaseSinceLastSkillPoint = MathHelper.Max(increase, 1f);
var abilitySkillGain = new AbilitySkillGain(increaseSinceLastSkillPoint, skillIdentifier, Character, gainedFromAbility);
@@ -1169,6 +1189,25 @@ namespace Barotrauma
OnSkillChanged(skillIdentifier, prevLevel, newLevel);
}
private static readonly ImmutableDictionary<Identifier, StatTypes> skillGainStatValues = new Dictionary<Identifier, StatTypes>
{
{ new("helm"), StatTypes.HelmSkillGainSpeed },
{ new("medical"), StatTypes.WeaponsSkillGainSpeed },
{ new("weapons"), StatTypes.MedicalSkillGainSpeed },
{ new("electrical"), StatTypes.ElectricalSkillGainSpeed },
{ new("mechanical"), StatTypes.MechanicalSkillGainSpeed }
}.ToImmutableDictionary();
private float GetSkillSpecificGain(float increase, Identifier skillIdentifier)
{
if (skillGainStatValues.TryGetValue(skillIdentifier, out StatTypes statType))
{
increase *= 1f + Character.GetStatValue(statType);
}
return increase;
}
public void SetSkillLevel(Identifier skillIdentifier, float level)
{
if (Job == null) { return; }
@@ -1314,7 +1353,6 @@ namespace Barotrauma
new XAttribute("tags", string.Join(",", Head.Preset.TagSet)),
new XAttribute("salary", Salary),
new XAttribute("experiencepoints", ExperiencePoints),
new XAttribute("unlockedtalents", string.Join(",", UnlockedTalents)),
new XAttribute("additionaltalentpoints", AdditionalTalentPoints),
new XAttribute("hairindex", Head.HairIndex),
new XAttribute("beardindex", Head.BeardIndex),
@@ -1363,7 +1401,16 @@ namespace Barotrauma
}
}
XElement talentElement = new XElement("Talents");
talentElement.Add(new XAttribute("version", GameMain.Version.ToString()));
foreach (Identifier talentIdentifier in UnlockedTalents)
{
talentElement.Add(new XElement("Talent", new XAttribute("identifier", talentIdentifier)));
}
charElement.Add(savedStatElement);
charElement.Add(talentElement);
parentElement?.Add(charElement);
return charElement;
}
@@ -1717,20 +1764,33 @@ namespace Barotrauma
}
}
public void ResetSavedStatValue(string statIdentifier)
public void ResetSavedStatValue(Identifier statIdentifier)
{
foreach (StatTypes statType in SavedStatValues.Keys)
{
bool changed = false;
foreach (SavedStatValue savedStatValue in SavedStatValues[statType])
{
if (savedStatValue.StatIdentifier != statIdentifier) { continue; }
if (!MatchesIdentifier(savedStatValue.StatIdentifier, statIdentifier)) { continue; }
if (MathUtils.NearlyEqual(savedStatValue.StatValue, 0.0f)) { continue; }
savedStatValue.StatValue = 0.0f;
changed = true;
}
if (changed) { OnPermanentStatChanged(statType); }
}
static bool MatchesIdentifier(Identifier statIdentifier, Identifier identifier)
{
if (statIdentifier == identifier) { return true; }
if (identifier.IndexOf('*') is var index and > -1)
{
return statIdentifier.StartsWith(identifier[0..index]);
}
return false;
}
}
public float GetSavedStatValue(StatTypes statType)
@@ -1756,7 +1816,7 @@ namespace Barotrauma
}
}
public void ChangeSavedStatValue(StatTypes statType, float value, string statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
public void ChangeSavedStatValue(StatTypes statType, float value, Identifier statIdentifier, bool removeOnDeath, float maxValue = float.MaxValue, bool setValue = false)
{
if (!SavedStatValues.ContainsKey(statType))
{
@@ -1779,13 +1839,13 @@ namespace Barotrauma
}
}
public class SavedStatValue
internal sealed class SavedStatValue
{
public string StatIdentifier { get; set; }
public Identifier StatIdentifier { get; set; }
public float StatValue { get; set; }
public bool RemoveOnDeath { get; set; }
public SavedStatValue(string statIdentifier, float value, bool removeOnDeath)
public SavedStatValue(Identifier statIdentifier, float value, bool removeOnDeath)
{
StatValue = value;
RemoveOnDeath = removeOnDeath;
@@ -1793,7 +1853,7 @@ namespace Barotrauma
}
}
class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
internal sealed class AbilitySkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier, IAbilityCharacter
{
public AbilitySkillGain(float skillAmount, Identifier skillIdentifier, Character character, bool gainedFromAbility)
{

View File

@@ -384,6 +384,8 @@ namespace Barotrauma
private readonly ConstructorInfo constructor;
public readonly bool ResetBetweenRounds;
public IEnumerable<KeyValuePair<Identifier, float>> TreatmentSuitability
{
get
@@ -465,6 +467,8 @@ namespace Barotrauma
AfflictionOverlayAlphaIsLinear = element.GetAttributeBool("afflictionoverlayalphaislinear", false);
AchievementOnRemoved = element.GetAttributeIdentifier("achievementonremoved", "");
ResetBetweenRounds = element.GetAttributeBool("resetbetweenrounds", false);
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())

View File

@@ -140,9 +140,20 @@ namespace Barotrauma
private float vitality;
public float Vitality
{
get
{
return Character.IsDead ? minVitality : vitality;
get
{
if (Character.IsDead)
{
return minVitality;
}
if (Character.HasAbilityFlag(AbilityFlags.CanNotDieToAfflictions))
{
return Math.Max(vitality, MinVitality + 1);
}
return vitality;
}
private set
{
@@ -881,6 +892,9 @@ namespace Barotrauma
float oxygenlowResistance = GetResistance(oxygenLowAffliction.Prefab);
decreaseSpeed *= (1f - oxygenlowResistance);
increaseSpeed *= (1f + oxygenlowResistance);
float holdBreathMultiplier = 1f + GetStatValue(StatTypes.HoldBreathMultiplier);
decreaseSpeed *= holdBreathMultiplier;
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
}
@@ -1217,6 +1231,7 @@ namespace Barotrauma
var affliction = kvp.Key;
var limbHealth = kvp.Value;
if (affliction.Strength <= 0.0f || limbHealth != null) { continue; }
if (kvp.Key.Prefab.ResetBetweenRounds) { continue; }
healthElement.Add(new XElement("Affliction",
new XAttribute("identifier", affliction.Identifier),
new XAttribute("strength", affliction.Strength.ToString("G", CultureInfo.InvariantCulture))));

View File

@@ -778,6 +778,7 @@ namespace Barotrauma
{
var abilityAfflictionCharacter = new AbilityAfflictionCharacter(newAffliction, character);
attacker.CheckTalents(AbilityEffectType.OnAddDamageAffliction, abilityAfflictionCharacter);
newAffliction = abilityAfflictionCharacter.Affliction;
}
if (applyAffliction)
{
@@ -896,6 +897,12 @@ namespace Barotrauma
{
reEnableTimer = duration;
}
#if CLIENT
if (Hidden && LightSource != null)
{
LightSource.Enabled = false;
}
#endif
}
public void ReEnable()
@@ -1194,7 +1201,25 @@ namespace Barotrauma
}
else
{
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
if (statusEffect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory is { } inventory)
{
foreach (Item item in inventory.AllItems)
{
if (statusEffect.TargetIdentifiers != null &&
!statusEffect.TargetIdentifiers.Contains(item.Prefab.Identifier) &&
statusEffect.TargetIdentifiers.None(id => item.HasTag(id)))
{
continue;
}
if (statusEffect.TargetSlot > -1)
{
if (inventory.FindIndex(item) != statusEffect.TargetSlot) { continue; }
}
targets.Add(item);
}
}
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
{
statusEffect.Apply(actionType, deltaTime, character, character, WorldPosition);
}

View File

@@ -649,7 +649,7 @@ namespace Barotrauma
if (HasTag(tag))
{
target = null;
DebugConsole.ThrowError($"Multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
DebugConsole.AddWarning($"Trying to add multiple targets with the same tag ('{tag}') defined! Only the first will be used!");
return false;
}
else

View File

@@ -1,8 +1,5 @@
using Microsoft.Xna.Framework;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -34,6 +31,7 @@ namespace Barotrauma.Abilities
Alive = 4,
Monster = 5,
InFriendlySubmarine = 6,
Large = 7,
};
protected List<TargetType> ParseTargetTypes(string[] targetTypeStrings)
@@ -41,8 +39,7 @@ namespace Barotrauma.Abilities
List<TargetType> targetTypes = new List<TargetType>();
foreach (string targetTypeString in targetTypeStrings)
{
TargetType targetType = TargetType.Any;
if (!Enum.TryParse(targetTypeString, true, out targetType))
if (!Enum.TryParse(targetTypeString, true, out TargetType targetType))
{
DebugConsole.ThrowError("Invalid target type type \"" + targetTypeString + "\" in CharacterTalent (" + characterTalent.DebugIdentifier + ")");
}
@@ -83,6 +80,9 @@ namespace Barotrauma.Abilities
return !targetCharacter.IsHuman;
case TargetType.InFriendlySubmarine:
return targetCharacter.Submarine != null && targetCharacter.Submarine.TeamID == character.TeamID;
case TargetType.Large:
// mass of mudraptor is ~48
return targetCharacter.AnimController is { Mass: > 50.0f };
default:
return true;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
@@ -8,11 +9,13 @@ namespace Barotrauma.Abilities
{
private readonly List<TargetType> targetTypes;
private List<PropertyConditional> conditionals = new List<PropertyConditional>();
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty<string>(), convertToLowerInvariant: true));
targetTypes = ParseTargetTypes(
conditionElement.GetAttributeStringArray("targettypes",
conditionElement.GetAttributeStringArray("targettype", Array.Empty<string>())));
foreach (XElement subElement in conditionElement.Elements())
{
@@ -28,13 +31,18 @@ namespace Barotrauma.Abilities
break;
}
}
if (!targetTypes.Any() && !conditionals.Any())
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No target types or conditionals defined - the condition will match any character.");
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityCharacter abilityCharacter)
{
if (!(abilityCharacter.Character is Character character)) { return false; }
if (abilityCharacter.Character is not Character character) { return false; }
if (!IsViableTarget(targetTypes, character)) { return false; }
foreach (var conditional in conditionals)
{

View File

@@ -0,0 +1,19 @@
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterNotLooted : AbilityConditionData
{
private readonly Identifier identifier;
public AbilityConditionCharacterNotLooted(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
identifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter ability) { return false; }
return !ability.Character.MarkedAsLooted.Contains(identifier);
}
}
}

View File

@@ -0,0 +1,16 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCharacterUnconcious : AbilityConditionData
{
public AbilityConditionCharacterUnconcious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is not IAbilityCharacter targetCharacter) { return false; }
return targetCharacter.Character.IsUnconscious;
}
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
@@ -13,6 +12,11 @@ namespace Barotrauma.Abilities
{
identifiers = conditionElement.GetAttributeStringArray("identifiers", Array.Empty<string>(), convertToLowerInvariant: true);
tags = conditionElement.GetAttributeStringArray("tags", Array.Empty<string>(), convertToLowerInvariant: true);
if (!identifiers.Any() && !tags.Any())
{
DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers or tags defined.");
}
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)

View File

@@ -8,6 +8,7 @@ namespace Barotrauma.Abilities
{
private readonly bool? hasOutpost;
private readonly Identifier[] locationIdentifiers;
private readonly bool isPositiveReputation;
public AbilityConditionLocation(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
@@ -16,12 +17,19 @@ namespace Barotrauma.Abilities
hasOutpost = conditionElement.GetAttributeBool("hasoutpost", false);
}
locationIdentifiers = conditionElement.GetAttributeIdentifierArray("locationtype", Array.Empty<Identifier>());
isPositiveReputation = conditionElement.GetAttributeBool("ispositivereputation", false);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if (abilityObject is IAbilityLocation abilityLocation)
{
if (isPositiveReputation)
{
if (abilityLocation.Location.Reputation.Faction.Reputation.Value <= 0) { return false; }
}
if (locationIdentifiers.Any())
{
if (!locationIdentifiers.Contains(abilityLocation.Location.Type.Identifier)) { return false; }

View File

@@ -1,38 +1,50 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{
class AbilityConditionMission : AbilityConditionData
{
private readonly MissionType missionType;
private readonly ImmutableHashSet<MissionType> missionType;
private readonly bool isAffiliated;
public AbilityConditionMission(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
string missionTypeString = conditionElement.GetAttributeString("missiontype", "None");
if (!Enum.TryParse(missionTypeString, out missionType))
string[] missionTypeStrings = conditionElement.GetAttributeStringArray("missiontype", new []{ "None" })!;
HashSet<MissionType> missionTypes = new HashSet<MissionType>();
foreach (string missionTypeString in missionTypeStrings)
{
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - \"" + missionTypeString + "\" is not a valid mission type.");
return;
}
if (missionType == MissionType.None)
{
DebugConsole.ThrowError("Error in AbilityConditionMission \"" + characterTalent.DebugIdentifier + "\" - mission type cannot be none.");
return;
if (!Enum.TryParse(missionTypeString, out MissionType parsedMission) || parsedMission is MissionType.None)
{
DebugConsole.ThrowError($"Error in AbilityConditionMission \"{characterTalent.DebugIdentifier}\" - \"{missionTypeString}\" is not a valid mission type.");
return;
}
missionTypes.Add(parsedMission);
}
missionType = missionTypes.ToImmutableHashSet();
isAffiliated = conditionElement.GetAttributeBool("isaffiliated", false);
}
protected override bool MatchesConditionSpecific(AbilityObject abilityObject)
{
if ((abilityObject as IAbilityMission)?.Mission is Mission mission)
if (abilityObject is IAbilityMission { Mission: { } mission })
{
return mission.Prefab.Type == missionType;
}
else
{
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
return false;
if (isAffiliated && GameMain.GameSession?.Campaign?.Factions.MaxBy(static f => f.Reputation.Value) is { } highestFaction)
{
if (highestFaction.Reputation.Value < 0 || !mission.ReputationRewards.ContainsKey(highestFaction.Reputation.Identifier))
{
return false;
}
}
return missionType.Contains(mission.Prefab.Type);
}
LogAbilityConditionError(abilityObject, typeof(IAbilityMission));
return false;
}
}
}

View File

@@ -1,5 +1,4 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Abilities
{

View File

@@ -0,0 +1,47 @@
using System;
using Microsoft.Xna.Framework;
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionAllyNearby : AbilityConditionDataless
{
private enum NearbyCharacterTruthy
{
OneCharacterMatches,
NoCharacterMatches
}
private readonly NearbyCharacterTruthy truthyWhen;
private readonly float distance;
public AbilityConditionAllyNearby(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
truthyWhen = conditionElement.GetAttributeEnum("truthywhen", NearbyCharacterTruthy.OneCharacterMatches);
distance = conditionElement.GetAttributeFloat("distance", 10f);
}
protected override bool MatchesConditionSpecific()
{
bool trueCondition = truthyWhen switch
{
NearbyCharacterTruthy.OneCharacterMatches => true,
NearbyCharacterTruthy.NoCharacterMatches => false,
_ => throw new ArgumentOutOfRangeException(nameof(truthyWhen))
};
foreach (Character ally in Character.GetFriendlyCrew(character))
{
if (ally == character) { continue; }
float distanceToCharacter = Vector2.DistanceSquared(ally.WorldPosition, character.WorldPosition);
if (distanceToCharacter < distance * distance)
{
return trueCondition;
}
}
return !trueCondition;
}
}
}

View File

@@ -0,0 +1,22 @@
#nullable enable
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionCrewMemberUnconscious : AbilityConditionDataless
{
public AbilityConditionCrewMemberUnconscious(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { }
protected override bool MatchesConditionSpecific()
{
foreach (Character c in GameSession.GetSessionCrewCharacters(CharacterType.Both))
{
if (c.IsUnconscious)
{
return true;
}
}
return false;
}
}
}

View File

@@ -17,7 +17,7 @@
{
var affliction = character.CharacterHealth.GetAffliction(afflictionIdentifier);
if (affliction == null) { return false; }
return minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength;
return affliction.Strength >= affliction.Prefab.ActivationThreshold && minimumPercentage <= affliction.Strength / affliction.Prefab.MaxStrength;
}
return false;
}

View File

@@ -22,7 +22,7 @@ namespace Barotrauma.Abilities
{
if (tags.None())
{
return character.GetEquippedItem(null) is Item;
return character.GetEquippedItem(null) != null;
}
if (requireAll)

View File

@@ -0,0 +1,43 @@
#nullable enable
using System;
namespace Barotrauma.Abilities
{
internal sealed class AbilityConditionHasLevel : AbilityConditionDataless
{
private readonly Option<int> matchedLevel;
private readonly Option<int> minLevel;
public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
matchedLevel = conditionElement.GetAttributeInt("levelequals", 0) is var match and not 0
? Option<int>.Some(match)
: Option<int>.None();
minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0
? Option<int>.Some(min)
: Option<int>.None();
if (matchedLevel.IsNone() && minLevel.IsNone())
{
throw new Exception($"{nameof(AbilityConditionHasLevel)} must have either \"levelequals\" or \"minlevel\" attribute.");
}
}
protected override bool MatchesConditionSpecific()
{
if (matchedLevel.TryUnwrap(out int match))
{
return character.Info.GetCurrentLevel() == match;
}
if (minLevel.TryUnwrap(out int min))
{
return character.Info.GetCurrentLevel() >= min;
}
return false;
}
}
}

View File

@@ -1,13 +1,11 @@
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{
class AbilityConditionHasPermanentStat : AbilityConditionDataless
{
private readonly Identifier statIdentifier;
private readonly StatTypes statType;
private readonly float min;
private readonly PermanentStatPlaceholder placeholder;
public AbilityConditionHasPermanentStat(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement)
{
@@ -19,11 +17,14 @@ namespace Barotrauma.Abilities
string statTypeName = conditionElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, characterTalent.DebugIdentifier);
min = conditionElement.GetAttributeFloat("min", 0f);
placeholder = conditionElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None);
}
protected override bool MatchesConditionSpecific()
{
return character.Info.GetSavedStatValue(statType, statIdentifier) >= min;
Identifier identifier = CharacterAbilityGivePermanentStat.HandlePlaceholders(placeholder, statIdentifier);
return character.Info.GetSavedStatValue(statType, identifier) >= min;
}
}
}

Some files were not shown because too many files have changed in this diff Show More