diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs index dc50608c8..007c6d8a3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Animation/Ragdoll.cs @@ -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); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs index 99557594c..99cd9bd2d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/Character.cs @@ -109,6 +109,22 @@ namespace Barotrauma set => grainStrength = Math.Max(0, value); } + /// + /// Can be used to set camera shake from status effects + /// + 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 bloodEmitters = new List(); public IEnumerable BloodEmitters { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs index f4742ac64..df6381f18 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterHUD.cs @@ -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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs index 4a9a99547..8d5e63f26 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Characters/CharacterNetworking.cs @@ -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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs index bfa8a5194..9d6be3cbd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs @@ -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 missingTexts = new HashSet(); //key = text tag, value = list of languages the tag is missing from Dictionary> missingTags = new Dictionary>(); @@ -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($"{property.Name.FormatCamelCaseWithSpaces()}"); + } - 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($"{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(); } 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}")); + 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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs index 094a4b81c..2456e2ba8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/ChatBox.cs @@ -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) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs index b25b07ca4..85afac3c9 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs @@ -25,7 +25,7 @@ namespace Barotrauma private PlayerBalanceElement? playerBalanceElement; private List PendingHires => campaign.Map?.CurrentLocation?.HireManager?.PendingHires; - private bool HasPermission => campaignUI.Campaign.AllowedToManageCampaign(ClientPermissions.ManageHires); + private bool HasPermission => CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageHires); private Point resolutionWhenCreated; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs index 4fcd29809..373fecbbf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/FileSelection.cs @@ -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 diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs index c158fc21d..fb4fd861b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUI.cs @@ -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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs index 99461d746..e4a94bc56 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/GUITextBlock.cs @@ -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) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs index eca7c4334..b48a3867f 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs @@ -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)); diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs index 7a9a97fc5..0bdb26de7 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs @@ -673,7 +673,7 @@ namespace Barotrauma { if (GameMain.GameSession?.Campaign?.PendingSubmarineSwitch == null) { - return Submarine.MainSub.Info; + return Submarine.MainSub?.Info; } else { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs index 8a9d4e8c1..7518da497 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TabMenu.cs @@ -34,7 +34,7 @@ namespace Barotrauma private List 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 registeredEvents = new HashSet(); + 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 selectedTalents = new List(); - - private GUITextBlock experienceText; - private GUIProgressBar experienceBar; - private GUITextBlock talentPointText; - private GUIListBox skillListBox; - - private GUIButton talentApplyButton, - talentResetButton; private GUIImage talentPointNotification; - private readonly ImmutableDictionary talentStageStyles = new Dictionary - { - { 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 talentStageBackgroundColors = new Dictionary - { - { 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 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 subTreeNames = new List(); - 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()) - { - 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 skillNames = new List(); @@ -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() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs new file mode 100644 index 000000000..a3151bf46 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/TalentMenu.cs @@ -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 talentStageStyles = + new Dictionary + { + [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 talentButtons = new HashSet(); + private readonly HashSet showCaseTalentFrames = new HashSet(); + private readonly HashSet talentCornerIcons = new HashSet(); + private HashSet selectedTalents = new HashSet(); + + 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 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 subTreeNames = new List(); + 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 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()) + { + 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); + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs index 08a186081..b58bcc3fc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GUI/UpgradeStore.cs @@ -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(); 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(); @@ -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(); 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) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs index e8069b34b..a0a55d717 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameAnalytics/GameAnalyticsManager.cs @@ -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/"); } }); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs index 226487ec1..5965d350e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameMain.cs @@ -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; }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs index 54d942d3b..3311d1650 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs @@ -86,10 +86,12 @@ namespace Barotrauma } } + private static bool IsOwner(Client client) => client != null && client.IsOwner; + /// /// There is a server-side implementation of the method in /// - 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() diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs index dc7bc12b1..965b3a274 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/SinglePlayerCampaign.cs @@ -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 diff --git a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs index a4ec49919..da2c6b35b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs @@ -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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs index 1645e1be1..ef7418390 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/CharacterInventory.cs @@ -774,7 +774,6 @@ namespace Barotrauma } else { - bool isEquippable = item.AllowedSlots.Any(s => s != InvSlotType.Any); var selectedContainer = character.SelectedItem?.GetComponent(); 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; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs index 1684642d0..f649e423c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/ElectricalDischarger.cs @@ -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); } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs index dd3b30b66..71cfdb1b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Deconstructor.cs @@ -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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs index 3b25c87b0..c0b717496 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs @@ -1119,7 +1119,7 @@ namespace Barotrauma.Items.Components if (it.GetComponent() 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() is { } powerTransfer) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs index 188c75cf9..b6ff975de 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Sonar.cs @@ -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() is { Attached: true }); if (unobtainedMinerals.None()) { continue; } if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; } var i = unobtainedMinerals.FirstOrDefault(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs index 7f06e5dbf..1eff3054d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Steering.cs @@ -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( diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs index 72070c051..c3f44c01d 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Power/PowerContainer.cs @@ -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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs index 03e4e2eff..a5395e8e0 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Components/Turret.cs @@ -581,7 +581,7 @@ namespace Barotrauma.Items.Components var battery = recipient.Item?.GetComponent(); if (battery == null || battery.Item.Condition <= 0.0f) { continue; } availableCharge += battery.Charge; - availableCapacity += battery.Capacity; + availableCapacity += battery.GetCapacity(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs index 0c35c29eb..05513e00a 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/Item.cs @@ -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(msg); + var statValue = msg.ReadSingle(); + StatManager.ApplyStat(statIdentifier, statValue); + } + break; case EventType.Upgrade: Identifier identifier = msg.ReadIdentifier(); byte level = msg.ReadByte(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs index 8c6aed81b..bc69b2e51 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Items/ItemPrefab.cs @@ -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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs index 278e3a36c..c32f82187 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Map/Map/Map.cs @@ -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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs index 4a46f3c9e..c6cce06c2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/NetEntityEvent/ClientEntityEventManager.cs @@ -277,16 +277,12 @@ namespace Barotrauma.Networking public void Clear() { - ID = 0; - lastReceivedID = 0; - firstNewID = null; - - events.Clear(); eventLastSent.Clear(); - MidRoundSyncingDone = false; + + ClearSelf(); } /// @@ -297,6 +293,10 @@ namespace Barotrauma.Networking { ID = 0; events.Clear(); + if (thisClient != null) + { + thisClient.LastSentEntityEventID = 0; + } } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs index fc23c4df9..56138ec83 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerList/PingUtils.cs @@ -14,7 +14,7 @@ namespace Barotrauma.Networking { static class PingUtils { - private static readonly Dictionary activePings = new Dictionary(); + private static readonly Dictionary activePings = new Dictionary(); 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 onPingDiscovered) + private static void GetIPAddressPing(ServerInfo serverInfo, IPEndPoint endPoint, Action onPingDiscovered) { - if (IPAddress.IsLoopback(address)) + if (IPAddress.IsLoopback(endPoint.Address)) { serverInfo.Ping = Option.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.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.None(); } onPingDiscovered(serverInfo); lock (activePings) { - activePings.Remove(address); + activePings.Remove(endPoint); } }); } } - private static async Task> PingServerAsync(IPAddress ipAddress, int timeOut) + private static async Task> 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.None(); } + if (endPoint?.Address == null) { return Option.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.None(); } + if (endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option.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.None(); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs index 10829d4fb..b6c1b2583 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/ServerSettings.cs @@ -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() is GUINumberInput otherNumberInput) { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs index cae3906e3..468f07e54 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Particles/ParticleEmitter.cs @@ -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; } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs index a405d730e..f8450df3b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CampaignUI.cs @@ -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); diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs index 2f8f73962..642648f1b 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/CharacterEditor/Wizard.cs @@ -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) => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs index 38202f624..66ed3c9fd 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/EditorScreen.cs @@ -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() { } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs index 420328bc7..e89550ec1 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/MainMenuScreen.cs @@ -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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs index 9471d8128..6d966c50e 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs @@ -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, diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs index 8e54407a4..dcb6ab842 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs @@ -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; }; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs index df8fc28ba..f32050a63 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SpriteEditorScreen.cs @@ -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; diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs index 0952422e7..aa6c37369 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/SubEditorScreen.cs @@ -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().Select(p => p.Name).ToHashSet(); var tickboxes = requiredContentPackList.Content.Children.OfType().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() diff --git a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs index abfbb86e2..e36471206 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Screens/TestScreen.cs @@ -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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs index a63e7c905..c3c4c3873 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Serialization/SerializableEntityEditor.cs @@ -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)); + } } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs index 8e2172550..65a6e87bc 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Settings/SettingsMenu.cs @@ -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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs index 82f0bc176..86fb3e0f8 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/StatusEffects/StatusEffect.cs @@ -21,6 +21,13 @@ namespace Barotrauma private Entity soundEmitter; private double loopStartTime; private bool loopSound; + /// + /// 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. + /// + 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 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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs index cce1627d8..afb6f8bd3 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/InstalledTab.cs @@ -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 => { diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs index 17a677d57..62e930476 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/ItemList.cs @@ -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); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs index c9cbd489d..3193aa9ef 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/MutableWorkshopMenu.cs @@ -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(); + 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) diff --git a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs index 61d849dbe..09bdaafe2 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Steam/WorkshopMenu/Mutable/PublishTab.cs @@ -543,7 +543,8 @@ namespace Barotrauma.Steam var localModProject = new ModProject(localPackage) { - UgcId = Option.Some(new SteamWorkshopId(resultId)) + UgcId = Option.Some(new SteamWorkshopId(resultId)), + ModVersion = modVersion }; localModProject.DiscardHashAndInstallTime(); localModProject.Save(localPackage.Path); diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index 10bc6b9b4..41e27b185 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.11.0 + 0.20.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 7fad64083..24e07ca66 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.11.0 + 0.20.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index c88fd0394..4b80f7c8d 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma - 0.19.11.0 + 0.20.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 Barotrauma diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 91c01c717..7e7a57032 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.11.0 + 0.20.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index 9ddb35313..34e6882e6 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.11.0 + 0.20.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs index 49893f4dd..5aca9fbf7 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterInfo.cs @@ -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 diff --git a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs index a3e74f054..a80e80caa 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Characters/CharacterNetworking.cs @@ -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); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs index 29f57d01b..fb3703133 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs @@ -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; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 1cf319471..05e6c6f4e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -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 { diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs index 9360c71d2..4283c7206 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/CampaignMode.cs @@ -16,16 +16,15 @@ namespace Barotrauma /// /// There is a client-side implementation of the method in /// - 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) diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs index 502f148fc..bdd6a7b0e 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameSession/GameModes/MultiPlayerCampaign.cs @@ -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); } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs index b5349e891..c73a10dce 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Components/DockingPort.cs @@ -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; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs index 862478ce6..3dc30f6cf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Items/Item.cs @@ -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; diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs index 15f4d365a..9febcddaf 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/BanList.cs @@ -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; } diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs index fdea971a5..c6d6559cc 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Client.cs @@ -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 { diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs index 370627ddb..e48d786f1 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs @@ -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(); List 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); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs index abcbbf42c..f49b85fd2 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/NetEntityEvent/ServerEntityEventManager.cs @@ -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); diff --git a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs index 4e16b15bb..302b5b959 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/Networking/Primitives/Peers/Server/ServerPeer.cs @@ -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() }; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 4b9035163..f69fca38a 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -6,7 +6,7 @@ Barotrauma FakeFish, Undertow Games Barotrauma Dedicated Server - 0.19.11.0 + 0.20.0.0 Copyright © FakeFish 2018-2022 AnyCPU;x64 DedicatedServer diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs index 1708c86da..4cf5983f2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/EnemyAIController.cs @@ -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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs index c0c6f9c69..0a509fdd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveCombat.cs @@ -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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs index 78fc1a261..e8b5dea26 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveRepairItem.cs @@ -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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs index 1105eab19..24f0ef67d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/PetBehavior.cs @@ -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(); 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; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs index 95ff37b9c..b1a83f852 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorker.cs @@ -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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs index a112758f1..b4a18d756 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommandManager.cs @@ -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()); ShipIssueWorkers.Add(new ShipIssueWorkerOperateWeapons(this, order)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs index 928dd8e74..a2e4e7a3a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/AnimController.cs @@ -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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs index e2d69fe08..99b8a56dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/HumanoidAnimController.cs @@ -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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs index 8c90345cf..81e272765 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Animation/Ragdoll.cs @@ -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); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs index d5d02d2d4..387472cde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs @@ -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; } + /// /// Legacy support. Use Afflictions. /// @@ -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; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs index b03d51803..78d43ee78 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs @@ -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 Properties; public Dictionary 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 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 overrideStatTypes = new Dictionary + { + { 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() 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() == 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 sameRoomHulls = new(); + + /// + /// Check if the character is in the same room + /// Room and hull differ in that a room can consist of multiple linked hulls + /// + 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; + } + /// /// Shows visual notification of money gained by the specific player. Useful for mid-mission monetary gains. /// @@ -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) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs index b524580df..393db009a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/CharacterInfo.cs @@ -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(infoElement.GetAttributeIdentifierArray("unlockedtalents", Array.Empty())); AdditionalTalentPoints = infoElement.GetAttributeInt("additionaltalentpoints", 0); HashSet tags = infoElement.GetAttributeIdentifierArray("tags", Array.Empty()).ToHashSet(); LoadTagsBackwardsCompatibility(infoElement, tags); @@ -813,18 +811,22 @@ namespace Barotrauma infoElement.GetAttributeIdentifier("npcid", Identifier.Empty)); MissionsCompletedSinceDeath = infoElement.GetAttributeInt("missionscompletedsincedeath", 0); + UnlockedTalents = new HashSet(); 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 skillGainStatValues = new Dictionary + { + { 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs index a18f012bc..91c1998b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs @@ -384,6 +384,8 @@ namespace Barotrauma private readonly ConstructorInfo constructor; + public readonly bool ResetBetweenRounds; + public IEnumerable> 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()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs index cd028c9ca..b7825be29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs @@ -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)))); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs index 030e232b1..03afe476f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Limb.cs @@ -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); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs index 5b45d8882..b2323c54d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs @@ -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 diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs index 1b3564d0f..3ce84413a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityCondition.cs @@ -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 ParseTargetTypes(string[] targetTypeStrings) @@ -41,8 +39,7 @@ namespace Barotrauma.Abilities List targetTypes = new List(); 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs index 43a16839d..0ac951fd6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacter.cs @@ -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 targetTypes; - private List conditionals = new List(); + private readonly List conditionals = new List(); public AbilityConditionCharacter(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { - targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty(), convertToLowerInvariant: true)); + targetTypes = ParseTargetTypes( + conditionElement.GetAttributeStringArray("targettypes", + conditionElement.GetAttributeStringArray("targettype", Array.Empty()))); 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) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs new file mode 100644 index 000000000..a79830e5b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterNotLooted.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs new file mode 100644 index 000000000..2809f3546 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionCharacterUnconcious.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs index 5811b3d66..ae8597e64 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionItem.cs @@ -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(), convertToLowerInvariant: true); tags = conditionElement.GetAttributeStringArray("tags", Array.Empty(), convertToLowerInvariant: true); + + if (!identifiers.Any() && !tags.Any()) + { + DebugConsole.ThrowError($"Error in talent \"{characterTalent}\". No identifiers or tags defined."); + } } protected override bool MatchesConditionSpecific(AbilityObject abilityObject) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs index 9e85d367e..bfdb89205 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionLocation.cs @@ -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()); + + 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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs index 0e19ec19e..fd1d55682 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionMission.cs @@ -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; + 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 missionTypes = new HashSet(); + 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; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs index d3ece3bf8..c27404723 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionData/AbilityConditionReduceAffliction.cs @@ -1,5 +1,4 @@ using System; -using System.Xml.Linq; namespace Barotrauma.Abilities { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyNearby.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyNearby.cs new file mode 100644 index 000000000..e877c657b --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionAllyNearby.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs new file mode 100644 index 000000000..09fa7aaf4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionCrewMemberUnconscious.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs index 8470362c8..bc1e1e71f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasAffliction.cs @@ -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; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs index 04d7ebf62..e4458d78b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasItem.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Abilities { if (tags.None()) { - return character.GetEquippedItem(null) is Item; + return character.GetEquippedItem(null) != null; } if (requireAll) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs new file mode 100644 index 000000000..f14552583 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasLevel.cs @@ -0,0 +1,43 @@ +#nullable enable + +using System; + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionHasLevel : AbilityConditionDataless + { + private readonly Option matchedLevel; + private readonly Option minLevel; + + public AbilityConditionHasLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + matchedLevel = conditionElement.GetAttributeInt("levelequals", 0) is var match and not 0 + ? Option.Some(match) + : Option.None(); + + minLevel = conditionElement.GetAttributeInt("minlevel", 0) is var min and not 0 + ? Option.Some(min) + : Option.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; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs index 344a580f2..c4017a87f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasPermanentStat.cs @@ -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; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs new file mode 100644 index 000000000..de2f98107 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHasTalent.cs @@ -0,0 +1,19 @@ + +namespace Barotrauma.Abilities +{ + class AbilityConditionHasTalent : AbilityConditionDataless + { + private readonly Identifier talentIdentifier; + + public AbilityConditionHasTalent(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + talentIdentifier = conditionElement.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + protected override bool MatchesConditionSpecific() + { + bool result = character.HasTalent(talentIdentifier); + return result; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs new file mode 100644 index 000000000..5c6c201de --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionHoldingItem.cs @@ -0,0 +1,34 @@ +#nullable enable +using System.Collections.Immutable; + +namespace Barotrauma.Abilities; + +internal sealed class AbilityConditionHoldingItem : AbilityConditionDataless +{ + private readonly ImmutableHashSet tags; + + public AbilityConditionHoldingItem(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + tags = conditionElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); + } + + protected override bool MatchesConditionSpecific() + { + if (tags.Count is 0) + { + return HasItemInHand(character, null); + } + + foreach (Identifier tag in tags) + { + if (HasItemInHand(character, tag)) { return true; } + } + + return false; + + static bool HasItemInHand(Character character, Identifier? tagOrIdentifier) => + character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.RightHand) is not null || + character.GetEquippedItem(tagOrIdentifier?.Value, InvSlotType.LeftHand) is not null; + + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs new file mode 100644 index 000000000..3cf37c2b9 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionLowestLevel.cs @@ -0,0 +1,23 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class AbilityConditionLowestLevel : AbilityConditionDataless + { + public AbilityConditionLowestLevel(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) { } + + protected override bool MatchesConditionSpecific() + { + int ownLevel = character.Info.GetCurrentLevel(); + + foreach (Character crew in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (crew == character) { continue; } + + if (crew.Info.GetCurrentLevel() < ownLevel) { return false; } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs new file mode 100644 index 000000000..e8b084f07 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityConditionals/AbilityConditionDataless/AbilityConditionNearbyCharacterCount.cs @@ -0,0 +1,39 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using Microsoft.Xna.Framework; + +namespace Barotrauma.Abilities; + +internal sealed class AbilityConditionNearbyCharacterCount : AbilityConditionDataless +{ + private readonly float distance; + private readonly int count; + private readonly ImmutableHashSet targetTypes; + + public AbilityConditionNearbyCharacterCount(CharacterTalent characterTalent, ContentXElement conditionElement) : base(characterTalent, conditionElement) + { + distance = conditionElement.GetAttributeFloat("distance", 10f); + count = conditionElement.GetAttributeInt("count", 1); + targetTypes = ParseTargetTypes(conditionElement.GetAttributeStringArray("targettypes", Array.Empty(), convertToLowerInvariant: true)).ToImmutableHashSet(); + } + + protected override bool MatchesConditionSpecific() + { + int amountNeeded = count; + foreach (Character otherCharacter in Character.CharacterList) + { + if (character.Submarine != otherCharacter.Submarine) { continue; } + if (!IsViableTarget(targetTypes, otherCharacter)) { return false; } + + if (Vector2.DistanceSquared(character.WorldPosition, otherCharacter.WorldPosition) < distance * distance) + { + amountNeeded--; + + if (amountNeeded <= 0) { return true; } + } + } + + return false; + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs index 9d1e093b2..698e7f207 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/AbilityObjects.cs @@ -15,5 +15,4 @@ namespace Barotrauma.Abilities } public Character Character { get; set; } } - } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs index 044d960a9..c4d73f406 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbility.cs @@ -67,7 +67,7 @@ namespace Barotrauma.Abilities if (abilityObject is null) { ApplyEffect(); - } + } else { ApplyEffect(abilityObject); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectToNonHumans.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectToNonHumans.cs new file mode 100644 index 000000000..c7ef4461e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectToNonHumans.cs @@ -0,0 +1,35 @@ +#nullable enable + +using Microsoft.Xna.Framework; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityApplyStatusEffectToNonHumans : CharacterAbilityApplyStatusEffects + { + private readonly float maxDistance; + + public CharacterAbilityApplyStatusEffectToNonHumans(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); + } + + protected override void ApplyEffect() + { + foreach (Character character in Character.CharacterList) + { + if (character.IsHuman) { continue; } + + if (maxDistance < float.MaxValue) + { + if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } + } + ApplyEffectSpecific(character); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs index 5711c0ed5..d408fbc83 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffects.cs @@ -17,6 +17,8 @@ namespace Barotrauma.Abilities readonly List targets = new List(); + private bool effectBeingApplied; + public CharacterAbilityApplyStatusEffects(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { statusEffects = CharacterAbilityGroup.ParseStatusEffects(CharacterTalent, abilityElement.GetChildElement("statuseffects")); @@ -29,44 +31,57 @@ namespace Barotrauma.Abilities protected void ApplyEffectSpecific(Character targetCharacter) { - foreach (var statusEffect in statusEffects) + //prevent an infinite loop if an effect triggers itself + //(e.g. a talent that triggers when an affliction is applied, and applies that same affliction) + if (effectBeingApplied) { return; } + + effectBeingApplied = true; + + try { - if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget)) + foreach (var statusEffect in statusEffects) { - // currently used to spawn items on the targeted character - statusEffect.SetUser(targetCharacter); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter); - } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) - { - targets.Clear(); - targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets)); - if (!nearbyCharactersAppliesToSelf) + if (statusEffect.HasTargetType(StatusEffect.TargetType.UseTarget)) { - targets.RemoveAll(c => c == Character); + // currently used to spawn items on the targeted character + statusEffect.SetUser(targetCharacter); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targetCharacter); } - if (!nearbyCharactersAppliesToAllies) + else if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { - targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character)); + targets.Clear(); + targets.AddRange(statusEffect.GetNearbyTargets(targetCharacter.WorldPosition, targets)); + if (!nearbyCharactersAppliesToSelf) + { + targets.RemoveAll(c => c == Character); + } + if (!nearbyCharactersAppliesToAllies) + { + targets.RemoveAll(c => c is Character otherCharacter && HumanAIController.IsFriendly(otherCharacter, Character)); + } + if (!nearbyCharactersAppliesToEnemies) + { + targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character)); + } + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); } - if (!nearbyCharactersAppliesToEnemies) + else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) { - targets.RemoveAll(c => c is Character otherCharacter && !HumanAIController.IsFriendly(otherCharacter, Character)); + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); + } + else + { + statusEffect.SetUser(Character); + statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); } - statusEffect.SetUser(Character); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, targetCharacter, targets); - } - else if (statusEffect.HasTargetType(StatusEffect.TargetType.Character)) - { - statusEffect.SetUser(Character); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, targetCharacter); - } - else - { - statusEffect.SetUser(Character); - statusEffect.Apply(ActionType.OnAbility, EffectDeltaTime, Character, Character); } } + finally + { + effectBeingApplied = false; + } } protected override void ApplyEffect() { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs index 70b871963..83d56c28d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToAllies.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework; +using System.Collections.Immutable; +using Microsoft.Xna.Framework; namespace Barotrauma.Abilities { @@ -6,11 +7,15 @@ namespace Barotrauma.Abilities { private readonly bool allowSelf; private readonly float maxDistance = float.MaxValue; + private readonly bool inSameRoom; + private readonly ImmutableHashSet jobIdentifiers; public CharacterAbilityApplyStatusEffectsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { allowSelf = abilityElement.GetAttributeBool("allowself", true); maxDistance = abilityElement.GetAttributeFloat("maxdistance", float.MaxValue); + inSameRoom = abilityElement.GetAttributeBool("insameroom", false); + jobIdentifiers = abilityElement.GetAttributeIdentifierImmutableHashSet("jobs", ImmutableHashSet.Empty); } @@ -19,6 +24,27 @@ namespace Barotrauma.Abilities foreach (Character character in Character.GetFriendlyCrew(Character)) { if (!allowSelf && character == Character) { continue; } + + if (!jobIdentifiers.IsEmpty) + { + bool hadJob = false; + foreach (Identifier job in jobIdentifiers) + { + if (character.HasJob(job.Value)) + { + hadJob = true; + break; + } + } + + if (!hadJob) { continue; } + } + + if (inSameRoom && !character.IsInSameRoomAs(Character)) + { + continue; + } + if (maxDistance < float.MaxValue) { if (Vector2.DistanceSquared(character.WorldPosition, Character.WorldPosition) > maxDistance * maxDistance) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs new file mode 100644 index 000000000..27d4afe94 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityApplyStatusEffectsToApprenticeship.cs @@ -0,0 +1,63 @@ +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityApplyStatusEffectsToApprenticeship : CharacterAbilityApplyStatusEffects + { + private readonly bool invert; + private readonly ImmutableHashSet jobPrefabList = JobPrefab.Prefabs.ToImmutableHashSet(); + + public CharacterAbilityApplyStatusEffectsToApprenticeship(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + invert = abilityElement.GetAttributeBool("invert", false); + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + JobPrefab? apprenticeJob = GetApprenticeJob(Character, jobPrefabList); + if (apprenticeJob is null) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + return; + } + + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + JobPrefab? characterJob = character.Info?.Job?.Prefab; + if (characterJob is null) { continue; } + + switch (characterJob.Identifier == apprenticeJob.Identifier) + { + case true when invert: + continue; + case false when !invert: + continue; + } + + ApplyEffectSpecific(character); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + + public static JobPrefab? GetApprenticeJob(Character character, IReadOnlyCollection jobList) + { + foreach (JobPrefab prefab in jobList) + { + if (character.Info.GetSavedStatValue(StatTypes.Apprenticeship, prefab.Identifier) > 0) + { + return prefab; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs index 1204769de..4161a881e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGainSimultaneousSkill.cs @@ -6,12 +6,15 @@ namespace Barotrauma.Abilities class CharacterAbilityGainSimultaneousSkill : CharacterAbility { private readonly Identifier skillIdentifier; - private readonly bool ignoreAbilitySkillGain; + + private readonly bool ignoreAbilitySkillGain, + targetAllies; public CharacterAbilityGainSimultaneousSkill(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", ""); ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); + targetAllies = abilityElement.GetAttributeBool("targetallies", false); } protected override void ApplyEffect(AbilityObject abilityObject) @@ -19,7 +22,20 @@ namespace Barotrauma.Abilities if (abilityObject is AbilitySkillGain abilitySkillGain) { if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; } - Character.Info?.IncreaseSkillLevel(skillIdentifier, abilitySkillGain.Value, gainedFromAbility: true); + Identifier identifier = skillIdentifier == "inherit" ? abilitySkillGain.SkillIdentifier : skillIdentifier; + + if (targetAllies) + { + foreach (Character character in Character.GetFriendlyCrew(Character)) + { + if (character == Character) { continue; } + Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true); + } + } + else + { + Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true); + } } else { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs new file mode 100644 index 000000000..5b7d50aa0 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveExperience.cs @@ -0,0 +1,35 @@ +namespace Barotrauma.Abilities; + +internal sealed class CharacterAbilityGiveExperience : CharacterAbility +{ + public override bool AppliesEffectOnIntervalUpdate => true; + + private readonly int amount; + + public CharacterAbilityGiveExperience(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + amount = abilityElement.GetAttributeInt("amount", 0); + } + + private void ApplyEffectSpecific(Character targetCharacter) + { + targetCharacter.Info?.GiveExperience(amount); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if ((abilityObject as IAbilityCharacter)?.Character is { } targetCharacter) + { + ApplyEffectSpecific(targetCharacter); + } + else + { + ApplyEffectSpecific(Character); + } + } + + protected override void ApplyEffect() + { + ApplyEffectSpecific(Character); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs new file mode 100644 index 000000000..4fe2c1c86 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStat.cs @@ -0,0 +1,31 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveItemStat : CharacterAbility + { + private readonly ItemTalentStats stat; + private readonly float value; + + public CharacterAbilityGiveItemStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); + value = abilityElement.GetAttributeFloat("value", 0f); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityItem ability) { return; } + + ability.Item.StatManager.ApplyStat(stat, value, CharacterTalent); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs new file mode 100644 index 000000000..707663745 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveItemStatToTags.cs @@ -0,0 +1,39 @@ +#nullable enable + +using System.Collections.Immutable; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveItemStatToTags: CharacterAbility + { + private readonly ItemTalentStats stat; + private readonly float value; + private readonly ImmutableHashSet tags; + + public CharacterAbilityGiveItemStatToTags(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + stat = abilityElement.GetAttributeEnum("stattype", ItemTalentStats.None); + value = abilityElement.GetAttributeFloat("value", 0f); + tags = abilityElement.GetAttributeIdentifierImmutableHashSet("tags", ImmutableHashSet.Empty); + } + + protected override void VerifyState(bool conditionsMatched, float timeSinceLastUpdate) + { + if (conditionsMatched) + { + ApplyEffect(); + } + } + + protected override void ApplyEffect() + { + foreach (Item item in Character.Submarine.GetItems(true)) + { + if (item.HasTag(tags) || tags.Contains(item.Prefab.Identifier)) + { + item.StatManager.ApplyStat(stat, value, CharacterTalent); + } + } + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs index a0750d5d4..bd54f9413 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGivePermanentStat.cs @@ -1,11 +1,17 @@ -using Barotrauma.Extensions; -using System.Xml.Linq; +using System; namespace Barotrauma.Abilities { + public enum PermanentStatPlaceholder + { + None, + LocationName, + LocationIndex + } + class CharacterAbilityGivePermanentStat : CharacterAbility { - private readonly string statIdentifier; + private readonly Identifier statIdentifier; private readonly StatTypes statType; private readonly float value; private readonly float maxValue; @@ -13,6 +19,7 @@ namespace Barotrauma.Abilities private readonly bool removeOnDeath; private readonly bool giveOnAddingFirstTime; private readonly bool setValue; + private readonly PermanentStatPlaceholder placeholder; //private readonly float maximumValue; public override bool AllowClientSimulation => true; @@ -20,7 +27,7 @@ namespace Barotrauma.Abilities public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier); value = abilityElement.GetAttributeFloat("value", 0f); @@ -29,6 +36,7 @@ namespace Barotrauma.Abilities removeOnDeath = abilityElement.GetAttributeBool("removeondeath", false); giveOnAddingFirstTime = abilityElement.GetAttributeBool("giveonaddingfirsttime", characterAbilityGroup.AbilityEffectType == AbilityEffectType.None); setValue = abilityElement.GetAttributeBool("setvalue", false); + placeholder = abilityElement.GetAttributeEnum("placeholder", PermanentStatPlaceholder.None); } public override void InitializeAbility(bool addingFirstTime) @@ -51,14 +59,33 @@ namespace Barotrauma.Abilities private void ApplyEffectSpecific() { + Identifier identifier = HandlePlaceholders(placeholder, statIdentifier); if (targetAllies) { - Character.GetFriendlyCrew(Character).ForEach(c => c?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue)); + foreach (Character c in Character.GetFriendlyCrew(Character)) + { + c?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + } } else { - Character?.Info.ChangeSavedStatValue(statType, value, statIdentifier, removeOnDeath, maxValue: maxValue, setValue: setValue); + Character?.Info.ChangeSavedStatValue(statType, value, identifier, removeOnDeath, maxValue: maxValue, setValue: setValue); } } + + public static Identifier HandlePlaceholders(PermanentStatPlaceholder placeholder, Identifier original) + { + if (GameMain.GameSession?.Campaign?.Map is not { } map) { return original; } + + switch (placeholder) + { + case PermanentStatPlaceholder.LocationName when map.CurrentLocation is { } location: + return original.Replace("[placeholder]", location.Name); + case PermanentStatPlaceholder.LocationIndex: + return original.Replace("[placeholder]", map.CurrentLocationIndex.ToString()); + } + + return original; + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs new file mode 100644 index 000000000..37fb09935 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityGiveReputation.cs @@ -0,0 +1,31 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityGiveReputation : CharacterAbility + { + private readonly Identifier factionIdentifier; + private readonly float amount; + + public CharacterAbilityGiveReputation(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + factionIdentifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); + amount = abilityElement.GetAttributeFloat("amount", 0f); + } + + protected override void ApplyEffect() + { + if (GameMain.GameSession?.Campaign is not { } campaign) { return; } + + foreach (Faction faction in campaign.Factions) + { + if (faction.Prefab.Identifier != factionIdentifier) { continue; } + + faction.Reputation.AddReputation(amount); + break; + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) => ApplyEffect(); + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs new file mode 100644 index 000000000..c9460ff46 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityMarkAsLooted.cs @@ -0,0 +1,18 @@ +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityMarkAsLooted: CharacterAbility + { + private readonly Identifier identifier; + public CharacterAbilityMarkAsLooted(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter { Character: { } character }) { return; } + + character.MarkedAsLooted.Add(identifier); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs index 97b0302bd..6c275b1c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityModifyAffliction.cs @@ -1,30 +1,36 @@ -using System.Collections.Generic; -using System.Xml.Linq; - -namespace Barotrauma.Abilities +namespace Barotrauma.Abilities { class CharacterAbilityModifyAffliction : CharacterAbility { - private readonly string[] afflictionIdentifiers; + private readonly Identifier[] afflictionIdentifiers; + + private readonly Identifier replaceWith; private readonly float addedMultiplier; public CharacterAbilityModifyAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - afflictionIdentifiers = abilityElement.GetAttributeStringArray("afflictionidentifiers", new string[0], convertToLowerInvariant: true); + afflictionIdentifiers = abilityElement.GetAttributeIdentifierArray("afflictionidentifiers", System.Array.Empty()); + replaceWith = abilityElement.GetAttributeIdentifier("replacewith", Identifier.Empty); addedMultiplier = abilityElement.GetAttributeFloat("addedmultiplier", 0f); } protected override void ApplyEffect(AbilityObject abilityObject) { - if ((abilityObject as IAbilityAffliction)?.Affliction is Affliction affliction) + var abilityAffliction = abilityObject as IAbilityAffliction; + if (abilityAffliction?.Affliction is Affliction affliction) { - foreach (string afflictionIdentifier in afflictionIdentifiers) + foreach (Identifier afflictionIdentifier in afflictionIdentifiers) { - if (affliction.Identifier == afflictionIdentifier) + if (affliction.Identifier != afflictionIdentifier) { continue; } + affliction.Strength *= 1 + addedMultiplier; + if (!replaceWith.IsEmpty) { - affliction.Strength *= 1 + addedMultiplier; - } + if (AfflictionPrefab.Prefabs.TryGet(replaceWith, out AfflictionPrefab afflictionPrefab)) + { + abilityAffliction.Affliction = new Affliction(afflictionPrefab, abilityAffliction.Affliction.Strength); + } + } } } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs new file mode 100644 index 000000000..14a84d337 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityReduceAffliction.cs @@ -0,0 +1,27 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityReduceAffliction : CharacterAbility + { + private readonly Identifier afflictionId; + private readonly float amount; + + public CharacterAbilityReduceAffliction(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + afflictionId = abilityElement.GetAttributeIdentifier("afflictionid", abilityElement.GetAttributeIdentifier("affliction", Identifier.Empty)); + amount = abilityElement.GetAttributeFloat("amount", 0); + + if (afflictionId.IsEmpty) + { + DebugConsole.ThrowError($"Error in {nameof(CharacterAbilityReduceAffliction)} - affliction identifier not set."); + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not IAbilityCharacter character) { return; } + character.Character.CharacterHealth.ReduceAfflictionOnAllLimbs(afflictionId, amount); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs new file mode 100644 index 000000000..78d7f501a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityRemoveRandomIngredient.cs @@ -0,0 +1,19 @@ +#nullable enable + +using Barotrauma.Items.Components; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityRemoveRandomIngredient : CharacterAbility + { + public CharacterAbilityRemoveRandomIngredient(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + if (abilityObject is not Fabricator.AbilityFabricationItemIngredients { Items.Count: > 0 } ingredients) { return; } + + int randomIndex = Rand.Int(ingredients.Items.Count, Rand.RandSync.Unsynced); + ingredients.Items.RemoveAt(randomIndex); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs index 1660c54e1..df57f5f1c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityResetPermanentStat.cs @@ -1,16 +1,15 @@ -using System.Xml.Linq; - + namespace Barotrauma.Abilities { class CharacterAbilityResetPermanentStat : CharacterAbility { - private readonly string statIdentifier; + private readonly Identifier statIdentifier; public override bool AppliesEffectOnIntervalUpdate => true; public override bool AllowClientSimulation => true; public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { - statIdentifier = abilityElement.GetAttributeString("statidentifier", "").ToLowerInvariant(); + statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); } protected override void ApplyEffect(AbilityObject abilityObject) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs new file mode 100644 index 000000000..d24a201f3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilitySetMetadataInt.cs @@ -0,0 +1,29 @@ +#nullable enable + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilitySetMetadataInt : CharacterAbility + { + private readonly Identifier identifier; + private readonly int value; + + public CharacterAbilitySetMetadataInt(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) + { + identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); + value = abilityElement.GetAttributeInt("value", 0); + } + + protected override void ApplyEffect() + { + if (identifier == Identifier.Empty) { return; } + if (GameMain.GameSession?.Campaign?.CampaignMetadata is not { } metadata) { return; } + + metadata.SetValue(identifier, value); + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs deleted file mode 100644 index e8d0ad788..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CharacterAbilityUnlockTree.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Barotrauma.Abilities -{ - class CharacterAbilityUnlockTree : CharacterAbility - { - public CharacterAbilityUnlockTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) - { - } - - public override void InitializeAbility(bool addingFirstTime) - { - if (!TalentTree.JobTalentTrees.TryGet(Character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return; } - - var subTree = talentTree.TalentSubTrees.Find(t => t.AllTalentIdentifiers.Contains(CharacterTalent.Prefab.Identifier)); - if (subTree == null) { return; } - - subTree.ForceUnlock = true; - if (!addingFirstTime) { return; } - - foreach (var talentId in subTree.AllTalentIdentifiers) - { - if (talentId == CharacterTalent.Prefab.Identifier) { continue; } - if (Character.GiveTalent(talentId)) - { - Character.Info.AdditionalTalentPoints++; - } - } - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs index d58d73b49..ff1e5ccde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityTandemFire.cs @@ -13,21 +13,23 @@ namespace Barotrauma.Abilities protected override void ApplyEffect() { - if (!SelectedItemHasTag(Character)) { return; } + if (!SelectedItemHasTag(Character, tag)) { return; } Character closestCharacter = null; float closestDistance = squaredMaxDistance; foreach (Character crewCharacter in Character.GetFriendlyCrew(Character)) { - if (crewCharacter != Character && Vector2.DistanceSquared(Character.SimPosition, Character.GetRelativeSimPosition(crewCharacter)) is float tempDistance && tempDistance < closestDistance) + if (crewCharacter != Character && + Vector2.DistanceSquared(Character.WorldPosition, crewCharacter.WorldPosition) is float tempDistance && tempDistance < closestDistance && + SelectedItemHasTag(crewCharacter, tag)) { closestCharacter = crewCharacter; closestDistance = tempDistance; } } - if (closestCharacter == null || !SelectedItemHasTag(closestCharacter)) { return; } + if (closestCharacter == null) { return; } if (closestDistance < squaredMaxDistance) { @@ -35,7 +37,7 @@ namespace Barotrauma.Abilities ApplyEffectSpecific(closestCharacter); } - bool SelectedItemHasTag(Character character) => + static bool SelectedItemHasTag(Character character, string tag) => (character.SelectedItem != null && character.SelectedItem.HasTag(tag)) || (character.SelectedSecondaryItem != null && character.SelectedSecondaryItem.HasTag(tag)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs new file mode 100644 index 000000000..9994e51e6 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/Abilities/CustomAbilities/CharacterAbilityUnlockApprenticeshipTalentTree.cs @@ -0,0 +1,48 @@ +#nullable enable + +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.Extensions; + +namespace Barotrauma.Abilities +{ + internal sealed class CharacterAbilityUnlockApprenticeshipTalentTree : CharacterAbility + { + public CharacterAbilityUnlockApprenticeshipTalentTree(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) { } + + public override void InitializeAbility(bool addingFirstTime) + { + JobPrefab? apprentice = CharacterAbilityApplyStatusEffectsToApprenticeship.GetApprenticeJob(Character, JobPrefab.Prefabs.ToImmutableHashSet()); + if (apprentice is null) + { + DebugConsole.ThrowError($"{nameof(CharacterAbilityUnlockApprenticeshipTalentTree)}: Could not find apprentice job for character {Character.Name}"); + return; + } + + if (!TalentTree.JobTalentTrees.TryGet(apprentice.Identifier, out TalentTree? talentTree)) { return; } + + HashSet> talentsTrees = new HashSet>(); + foreach (TalentSubTree subTree in talentTree.TalentSubTrees) + { + if (subTree.Type != TalentTreeType.Specialization) { continue; } + talentsTrees.Add(subTree.AllTalentIdentifiers); + } + + ImmutableHashSet selectedTalentTree = talentsTrees.GetRandomUnsynced(); + + foreach (Identifier identifier in selectedTalentTree) + { + if (Character.HasTalent(identifier)) { continue; } + if (Character.GiveTalent(identifier)) + { + Character.Info.AdditionalTalentPoints++; + } + } + } + + protected override void ApplyEffect(AbilityObject abilityObject) + { + ApplyEffect(); + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs index bd4b45998..766b48ea3 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroup.cs @@ -18,12 +18,15 @@ namespace Barotrauma.Abilities protected readonly int maxTriggerCount; protected int timesTriggered = 0; - - // add support for OR conditions? + // add support for OR conditions? protected readonly List abilityConditions = new List(); - // separate dictionaries for each type of characterability? - protected readonly List characterAbilities = new List(); + /// + /// List of abilities that are triggered by this group. + /// Fallback abilities are triggered if the conditional fails + /// + protected readonly List characterAbilities = new List(), + fallbackAbilities = new List(); public CharacterAbilityGroup(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) { @@ -38,6 +41,9 @@ namespace Barotrauma.Abilities case "abilities": LoadAbilities(subElement); break; + case "fallbackabilities": + LoadFallbackAbilities(subElement); + break; case "conditions": LoadConditions(subElement); break; @@ -47,10 +53,23 @@ namespace Barotrauma.Abilities public void ActivateAbilityGroup(bool addingFirstTime) { + if (!CheckActivatingCondition()) { return; } + foreach (var characterAbility in characterAbilities) { characterAbility.InitializeAbility(addingFirstTime); } + + foreach (var characterAbility in fallbackAbilities) + { + characterAbility.InitializeAbility(addingFirstTime); + } + } + + private bool CheckActivatingCondition() + { + if (AbilityEffectType is not AbilityEffectType.None) { return true; } + return !abilityConditions.Any(static abilityCondition => !abilityCondition.MatchesCondition()); } public void LoadConditions(ContentXElement conditionElements) @@ -85,6 +104,17 @@ namespace Barotrauma.Abilities characterAbilities.Add(characterAbility); } + public void AddFallbackAbility(CharacterAbility characterAbility) + { + if (characterAbility == null) + { + DebugConsole.ThrowError($"Trying to add null ability for talent {CharacterTalent.DebugIdentifier}!"); + return; + } + + fallbackAbilities.Add(characterAbility); + } + // XML private AbilityCondition ConstructCondition(CharacterTalent characterTalent, ContentXElement conditionElement, bool errorMessages = true) { @@ -135,6 +165,14 @@ namespace Barotrauma.Abilities } } + private void LoadFallbackAbilities(ContentXElement abilityElements) + { + foreach (var abilityElementGroup in abilityElements.Elements()) + { + AddFallbackAbility(ConstructAbility(abilityElementGroup, CharacterTalent)); + } + } + private CharacterAbility ConstructAbility(ContentXElement abilityElement, CharacterTalent characterTalent) { CharacterAbility newAbility = CharacterAbility.Load(abilityElement, this); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs index b77f4332d..b9418955f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupEffect.cs @@ -1,29 +1,38 @@ -namespace Barotrauma.Abilities +using System.Collections.Generic; + +namespace Barotrauma.Abilities { class CharacterAbilityGroupEffect : CharacterAbilityGroup { - public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : + public CharacterAbilityGroupEffect(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) { } public void CheckAbilityGroup(AbilityObject abilityObject) { if (!IsActive) { return; } - if (IsApplicable(abilityObject)) + + if (IsOverTriggerCount) { return; } + + List abilities = IsApplicable(abilityObject) ? characterAbilities : fallbackAbilities; + + foreach (CharacterAbility characterAbility in abilities) { - foreach (var characterAbility in characterAbilities) + if (characterAbility.IsViable()) { - if (characterAbility.IsViable()) - { - characterAbility.ApplyAbilityEffect(abilityObject); - } + characterAbility.ApplyAbilityEffect(abilityObject); } + } + + if (abilities.Count > 0) + { timesTriggered++; } } + private bool IsOverTriggerCount => timesTriggered >= maxTriggerCount; + private bool IsApplicable(AbilityObject abilityObject) { - if (timesTriggered >= maxTriggerCount) { return false; } foreach (var abilityCondition in abilityConditions) { if (!abilityCondition.MatchesCondition(abilityObject)) @@ -31,7 +40,8 @@ return false; } } + return true; } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs index 7cc1e24eb..18725da29 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/AbilityGroups/CharacterAbilityGroupInterval.cs @@ -1,4 +1,7 @@ -namespace Barotrauma.Abilities +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma.Abilities { class CharacterAbilityGroupInterval : CharacterAbilityGroup { @@ -9,48 +12,71 @@ private float effectDelayTimer; - public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : + public CharacterAbilityGroupInterval(AbilityEffectType abilityEffectType, CharacterTalent characterTalent, ContentXElement abilityElementGroup) : base(abilityEffectType, characterTalent, abilityElementGroup) - { + { // too many overlapping intervals could cause hitching? maybe randomize a little interval = abilityElementGroup.GetAttributeFloat("interval", 0f); effectDelay = abilityElementGroup.GetAttributeFloat("effectdelay", 0f); } + public void UpdateAbilityGroup(float deltaTime) { if (!IsActive) { return; } - TimeSinceLastUpdate += deltaTime; - if (TimeSinceLastUpdate >= interval) - { - bool conditionsMatched = IsApplicable(); - effectDelayTimer = conditionsMatched ? effectDelayTimer + TimeSinceLastUpdate : 0f; - conditionsMatched &= effectDelayTimer >= effectDelay; - foreach (var characterAbility in characterAbilities) - { - if (characterAbility.IsViable()) - { - characterAbility.UpdateCharacterAbility(conditionsMatched, TimeSinceLastUpdate); - } - } - if (conditionsMatched) - { - timesTriggered++; - } - TimeSinceLastUpdate = 0; + TimeSinceLastUpdate += deltaTime; + if (TimeSinceLastUpdate < interval) { return; } + + bool shouldApplyDelayedEffect; + bool conditionsDidntMatch; + + if (AllConditionsMatched()) + { + effectDelayTimer += TimeSinceLastUpdate; + shouldApplyDelayedEffect = effectDelayTimer >= effectDelay; + conditionsDidntMatch = false; } + else + { + effectDelayTimer = 0f; + shouldApplyDelayedEffect = false; + conditionsDidntMatch = true; + } + + bool hasFallbacks = fallbackAbilities.Count > 0; + + List abilitiesToRun = + conditionsDidntMatch && hasFallbacks + ? fallbackAbilities + : characterAbilities; + + foreach (var characterAbility in abilitiesToRun) + { + if (!characterAbility.IsViable()) { continue; } + + characterAbility.UpdateCharacterAbility( + shouldApplyDelayedEffect || conditionsDidntMatch, + TimeSinceLastUpdate); + } + + if (shouldApplyDelayedEffect || (conditionsDidntMatch && hasFallbacks)) + { + timesTriggered++; + } + + TimeSinceLastUpdate = 0; } - private bool IsApplicable() + + private bool AllConditionsMatched() { if (timesTriggered >= maxTriggerCount) { return false; } + foreach (var abilityCondition in abilityConditions) { - if (!abilityCondition.MatchesCondition()) - { - return false; - } + if (!abilityCondition.MatchesCondition()) { return false; } } + return true; } } -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs index 0cf4b3420..4cc374e79 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/CharacterTalent.cs @@ -19,6 +19,7 @@ namespace Barotrauma // works functionally but a missing recipe is not represented on GUI side. this might be better placed in the character class itself, though it might be fine here as well public List UnlockedRecipes { get; } = new List(); + public List UnlockedStoreItems { get; } = new List(); public CharacterTalent(TalentPrefab talentPrefab, Character character) { @@ -45,7 +46,17 @@ namespace Barotrauma } else { - DebugConsole.ThrowError("No recipe identifier defined for talent " + DebugIdentifier); + DebugConsole.ThrowError($"No recipe identifier defined for talent {DebugIdentifier}"); + } + break; + case "addedstoreitem": + if (subElement.GetAttributeIdentifier("itemtag", Identifier.Empty) is { IsEmpty: false } storeItemTag) + { + UnlockedStoreItems.Add(storeItemTag); + } + else + { + DebugConsole.ThrowError($"No store item identifier defined for talent {DebugIdentifier}"); } break; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs index d8f954873..d3cc70e56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentPrefab.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Xml.Linq; +#if CLIENT +using Microsoft.Xna.Framework; +#endif namespace Barotrauma { @@ -14,6 +14,10 @@ namespace Barotrauma public readonly Sprite Icon; +#if CLIENT + public readonly Option ColorOverride; +#endif + public static readonly PrefabCollection TalentPrefabs = new PrefabCollection(); public ContentXElement ConfigElement @@ -28,8 +32,22 @@ namespace Barotrauma DisplayName = TextManager.Get($"talentname.{Identifier}").Fallback(Identifier.Value); - Description = ""; - + Identifier nameIdentifier = element.GetAttributeIdentifier("nameidentifier", Identifier.Empty); + if (!nameIdentifier.IsEmpty) + { + DisplayName = TextManager.Get(nameIdentifier).Fallback(Identifier.Value); + } + + Description = string.Empty; + +#if CLIENT + Color colorOverride = element.GetAttributeColor("coloroverride", Color.TransparentBlack); + + ColorOverride = colorOverride != Color.TransparentBlack + ? Option.Some(colorOverride) + : Option.None(); +#endif + foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs index e3379314f..396244f9a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Characters/Talents/TalentTree.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Barotrauma { - class TalentTree : Prefab + internal sealed class TalentTree : Prefab { public enum TalentTreeStageState { @@ -40,16 +40,17 @@ namespace Barotrauma DebugConsole.ThrowError($"No job defined for talent tree in \"{file.Path}\"!"); return; } - + List subTrees = new List(); foreach (var subTreeElement in element.GetChildElements("subtree")) { subTrees.Add(new TalentSubTree(subTreeElement)); } + TalentSubTrees = subTrees.ToImmutableArray(); AllTalentIdentifiers = TalentSubTrees.SelectMany(t => t.AllTalentIdentifiers).ToImmutableHashSet(); } - + public bool TalentIsInTree(Identifier talentIdentifier) { return AllTalentIdentifiers.Contains(talentIdentifier); @@ -57,29 +58,42 @@ namespace Barotrauma public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier) { - return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (ICollection)Array.Empty()); + return IsViableTalentForCharacter(character, talentIdentifier, character?.Info?.UnlockedTalents ?? (IReadOnlyCollection)Array.Empty()); + } + + public static bool TalentTreeMeetsRequirements(TalentTree tree, TalentSubTree targetTree, IReadOnlyCollection selectedTalents) + { + IEnumerable blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)), + requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier)); + + return requiredSubTrees.All(tst => tst.IsCompleted(selectedTalents)) && // check if we meet requirements + !blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents)); // check if any other talent trees are blocking this one } // i hate this function - markus // me too - joonas - public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, List selectedTalents) + public static TalentTreeStageState GetTalentOptionStageState(Character character, Identifier subTreeIdentifier, int index, IReadOnlyCollection selectedTalents) { if (character?.Info?.Job.Prefab is null) { return TalentTreeStageState.Invalid; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return TalentTreeStageState.Invalid; } - TalentSubTree subTree = talentTree.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); + TalentSubTree subTree = talentTree!.TalentSubTrees.FirstOrDefault(tst => tst.Identifier == subTreeIdentifier); + if (subTree is null) { return TalentTreeStageState.Invalid; } - if (subTree == null) { return TalentTreeStageState.Invalid; } + if (!TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents)) + { + return TalentTreeStageState.Locked; + } TalentOption targetTalentOption = subTree.TalentOptionStages[index]; - if (targetTalentOption.TalentIdentifiers.Any(t => character.HasTalent(t))) + if (targetTalentOption.HasEnoughTalents(character.Info)) { return TalentTreeStageState.Unlocked; } - if (targetTalentOption.TalentIdentifiers.Any(t => selectedTalents.Contains(t))) + if (targetTalentOption.HasSelectedTalent(selectedTalents)) { return TalentTreeStageState.Highlighted; } @@ -91,8 +105,8 @@ namespace Barotrauma if (lastindex >= 0) { TalentOption lastLatentOption = subTree.TalentOptionStages[lastindex]; - hasTalentInLastTier = lastLatentOption.TalentIdentifiers.Any(HasTalent); - isLastTalentPurchased = lastLatentOption.TalentIdentifiers.Any(t => character.HasTalent(t)); + hasTalentInLastTier = lastLatentOption.HasEnoughTalents(selectedTalents); + isLastTalentPurchased = lastLatentOption.HasEnoughTalents(character.Info); } if (!hasTalentInLastTier) @@ -108,38 +122,29 @@ namespace Barotrauma } return TalentTreeStageState.Locked; - - bool HasTalent(Identifier talentId) - { - return selectedTalents.Contains(talentId); - } } - public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, ICollection selectedTalents) + public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection selectedTalents) { if (character?.Info?.Job.Prefab == null) { return false; } - if (character.Info.GetTotalTalentPoints() - selectedTalents.Count() <= 0) { return false; } + + if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } - foreach (var subTree in talentTree.TalentSubTrees) + foreach (var subTree in talentTree!.TalentSubTrees) { - if (subTree.ForceUnlock && subTree.TalentOptionStages.Any(option => option.TalentIdentifiers.Contains(talentIdentifier))) { return true; } - foreach (var talentOptionStage in subTree.TalentOptionStages) { - bool hasTalentInThisTier = talentOptionStage.TalentIdentifiers.Any(t => selectedTalents.Contains(t)); + bool hasTalentInThisTier = talentOptionStage.HasEnoughTalents(selectedTalents); if (!hasTalentInThisTier) { if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier)) { - return true; - } - else - { - break; + return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); } + break; } } } @@ -164,60 +169,119 @@ namespace Barotrauma } } } + return viableTalents; } public override void Dispose() { } } - class TalentSubTree + internal enum TalentTreeType + { + Specialization, + Primary + } + + internal sealed class TalentSubTree { public Identifier Identifier { get; } public LocalizedString DisplayName { get; } - public bool ForceUnlock; - public readonly ImmutableArray TalentOptionStages; public readonly ImmutableHashSet AllTalentIdentifiers; + public readonly TalentTreeType Type; + public readonly ImmutableHashSet RequiredTrees; + public readonly ImmutableHashSet BlockedTrees; + + public bool IsCompleted(IReadOnlyCollection talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents)); + public bool HasAnyTalent(IReadOnlyCollection talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents)); + public TalentSubTree(ContentXElement subTreeElement) { Identifier = subTreeElement.GetAttributeIdentifier("identifier", ""); - DisplayName = TextManager.Get("talenttree." + Identifier).Fallback(Identifier.Value); + string nameIdentifier = subTreeElement.GetAttributeString("nameidentifier", string.Empty); + if (string.IsNullOrWhiteSpace(nameIdentifier)) + { + nameIdentifier = $"talenttree.{Identifier}"; + } + DisplayName = TextManager.Get($"talenttree.{nameIdentifier}").Fallback(Identifier.Value); + Type = subTreeElement.GetAttributeEnum("type", TalentTreeType.Specialization); + RequiredTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("requires", ImmutableHashSet.Empty); + BlockedTrees = subTreeElement.GetAttributeIdentifierImmutableHashSet("blocks", ImmutableHashSet.Empty); List talentOptionStages = new List(); foreach (var talentOptionsElement in subTreeElement.GetChildElements("talentoptions")) { talentOptionStages.Add(new TalentOption(talentOptionsElement, Identifier)); } + TalentOptionStages = talentOptionStages.ToImmutableArray(); AllTalentIdentifiers = TalentOptionStages.SelectMany(t => t.TalentIdentifiers).ToImmutableHashSet(); } - } - class TalentOption + internal readonly struct TalentOption { private readonly ImmutableHashSet talentIdentifiers; public IEnumerable TalentIdentifiers => talentIdentifiers; - public bool HasTalent(Identifier talentIdentifier) + public readonly int MaxChosenTalents; + + /// + /// When specified the talent option will show talent with this identifier + /// and clicking on it will expand the talent option to show the talents + /// + public readonly Option ShowcaseTalent; + + public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= MaxChosenTalents; + public bool HasEnoughTalents(IReadOnlyCollection selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents; + + // No LINQ + public bool HasSelectedTalent(IReadOnlyCollection selectedTalents) { - return talentIdentifiers.Contains(talentIdentifier); + foreach (Identifier talent in selectedTalents) + { + if (talentIdentifiers.Contains(talent)) + { + return true; + } + } + return false; + } + + public int CountMatchingTalents(IReadOnlyCollection talents) + { + int i = 0; + foreach (Identifier talent in talents) + { + if (talentIdentifiers.Contains(talent)) + { + i++; + } + } + return i; } public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier) { + MaxChosenTalents = talentOptionsElement.GetAttributeInt("maxchosentalents", 1); + + Identifier showcaseTalent = talentOptionsElement.GetAttributeIdentifier("showcasetalent", Identifier.Empty); + ShowcaseTalent = !showcaseTalent.IsEmpty + ? Option.Some(showcaseTalent) + : Option.None(); + var talentIdentifiers = new HashSet(); foreach (var talentOptionElement in talentOptionsElement.GetChildElements("talentoption")) { Identifier identifier = talentOptionElement.GetAttributeIdentifier("identifier", Identifier.Empty); talentIdentifiers.Add(identifier); } + this.talentIdentifiers = talentIdentifiers.ToImmutableHashSet(); } } - -} +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs index 4e0a4328b..18da93f92 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPath.cs @@ -57,7 +57,7 @@ namespace Barotrauma { Option ugcId = ContentPackageId.Parse(otherModName.Value); ContentPackage? otherMod = - allPackages.FirstOrDefault(p => ugcId == p.UgcId) + allPackages.FirstOrDefault(p => ugcId.IsSome() && ugcId == p.UgcId) ?? allPackages.FirstOrDefault(p => p.Name == otherModName) ?? allPackages.FirstOrDefault(p => p.NameMatches(otherModName)) ?? throw new MissingContentPackageException(ContentPackage, otherModName.Value); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs index b184da625..f2b9dbae6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentXElement.cs @@ -1,6 +1,8 @@ #nullable enable using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection.Metadata.Ecma335; using System.Xml.Linq; @@ -63,6 +65,8 @@ namespace Barotrauma public Identifier GetAttributeIdentifier(string key, string def) => Element.GetAttributeIdentifier(key, def); public Identifier GetAttributeIdentifier(string key, Identifier def) => Element.GetAttributeIdentifier(key, def); public Identifier[]? GetAttributeIdentifierArray(string key, Identifier[] def, bool trim = true) => Element.GetAttributeIdentifierArray(key, def, trim); + [return:NotNullIfNotNull("def")] + public ImmutableHashSet? GetAttributeIdentifierImmutableHashSet(string key, ImmutableHashSet? def, bool trim = true) => Element.GetAttributeIdentifierImmutableHashSet(key, def, trim); public string? GetAttributeString(string key, string? def) => Element.GetAttributeString(key, def); public string GetAttributeStringUnrestricted(string key, string def) => Element.GetAttributeStringUnrestricted(key, def); public string[]? GetAttributeStringArray(string key, string[]? def, bool convertToLowerInvariant = false) => Element.GetAttributeStringArray(key, def, convertToLowerInvariant); diff --git a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs index b19d0f034..824924101 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/ContentManagement/Identifier.cs @@ -121,6 +121,10 @@ namespace Barotrauma public static bool operator !=(string str, in Identifier? identifier) => !(identifier == str); + + internal int IndexOf(char c) => Value.IndexOf(c); + + internal Identifier this[Range range] => Value[range].ToIdentifier(); } public static class IdentifierExtensions diff --git a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs index 9a7710898..050230110 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/DebugConsole.cs @@ -1334,7 +1334,7 @@ namespace Barotrauma if (!prefab.UpgradeCategories.Contains(category)) { continue; } if (!string.IsNullOrWhiteSpace(prefabIdentifier) && prefab.Identifier != prefabIdentifier) { continue; } - int targetLevel = prefab.MaxLevel - upgradeManager.GetRealUpgradeLevel(prefab, category); + int targetLevel = prefab.GetMaxLevelForCurrentSub() - upgradeManager.GetRealUpgradeLevel(prefab, category); for (int i = 0; i < targetLevel; i++) { upgradeManager.PurchaseUpgrade(prefab, category, force: true); @@ -1750,6 +1750,17 @@ namespace Barotrauma NewMessage("Set minimum loading time to " + time + " seconds.", Color.White); })); + + commands.Add(new Command("resetcharacternetstate", "resetcharacternetstate [character name]: A debug-only command that resets a character's network state, intended for diagnosing character syncing issues.", null, + () => + { + if (GameMain.NetworkMember == null) { return null; } + return new string[][] + { + Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray() + }; + })); + commands.Add(new Command("storeinfo", "", (string[] args) => { if (GameMain.GameSession?.Map?.CurrentLocation is Location location) @@ -1803,6 +1814,7 @@ namespace Barotrauma commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true)); commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true)); commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true)); + commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false)); commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false)); commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null)); commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs index bf8d1addd..f23d4a02c 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Enums.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Enums.cs @@ -43,6 +43,7 @@ namespace Barotrauma OnRepairComplete, OnItemFabricationSkillGain, OnItemFabricatedAmount, + OnItemFabricatedIngredients, OnAllyItemFabricatedAmount, OnOpenItemContainer, OnUseRangedWeapon, @@ -51,6 +52,7 @@ namespace Barotrauma OnSelfRagdoll, OnRagdoll, OnRoundEnd, + OnLootCharacter, OnAnyMissionCompleted, OnAllMissionsCompleted, OnGiveOrder, @@ -80,6 +82,11 @@ namespace Barotrauma // Skills ElectricalSkillBonus, HelmSkillBonus, + HelmSkillOverride, + MedicalSkillOverride, + WeaponsSkillOverride, + ElectricalSkillOverride, + MechanicalSkillOverride, MechanicalSkillBonus, MedicalSkillBonus, WeaponsSkillBonus, @@ -105,6 +112,7 @@ namespace Barotrauma RangedSpreadReduction, // Utility RepairSpeed, + MechanicalRepairSpeed, DeconstructorSpeedMultiplier, RepairToolStructureRepairMultiplier, RepairToolStructureDamageMultiplier, @@ -115,20 +123,53 @@ namespace Barotrauma GeneticMaterialRefineBonus, GeneticMaterialTaintedProbabilityReductionOnCombine, SkillGainSpeed, + ExtraLevelGain, + HelmSkillGainSpeed, + WeaponsSkillGainSpeed, + MedicalSkillGainSpeed, + ElectricalSkillGainSpeed, + MechanicalSkillGainSpeed, MedicalItemApplyingMultiplier, + MedicalItemDurationMultiplier, + PoisonMultiplier, // Tinker TinkeringDuration, TinkeringStrength, TinkeringDamage, // Misc ReputationGainMultiplier, + ReputationLossMultiplier, MissionMoneyGainMultiplier, ExperienceGainMultiplier, MissionExperienceGainMultiplier, ExtraMissionCount, ExtraSpecialSalesCount, - ApplyTreatmentsOnSelfFraction, + StoreSellMultiplier, + StoreBuyMultiplierAffiliated, + StoreBuyMultiplier, MaxAttachableCount, + ExplosionRadiusMultiplier, + ExplosionDamageMultiplier, + FabricateMedicineSpeedMultiplier, + BallastFloraDamageMultiplier, + HoldBreathMultiplier, + Apprenticeship, + CPRBoost + } + + internal enum ItemTalentStats + { + None, + DetoriationSpeed, + BatteryCapacity, + EngineSpeed, + EngineMaxSpeed, + PumpSpeed, + PumpMaxFlow, + ReactorMaxOutput, + ReactorFuelEfficiency, + DeconstructorSpeed, + FabricationSpeed } [Flags] @@ -145,8 +186,8 @@ namespace Barotrauma GainSkillPastMaximum = 0x80, RetainExperienceForNewCharacter = 0x100, AllowSecondOrderedTarget = 0x200, - PowerfulCPR = 0x400, - AlwaysStayConscious = 0x800, + AlwaysStayConscious = 0x400, + CanNotDieToAfflictions = 0x800, } [Flags] diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs index 143563584..1db3cdd56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; @@ -21,7 +22,14 @@ namespace Barotrauma private bool isFinished = false; - public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } + public NPCChangeTeamAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) + { + var enums = Enum.GetValues(typeof(CharacterTeamType)).Cast(); + if (!enums.Contains(TeamTag)) + { + DebugConsole.ThrowError($"Error in {nameof(NPCChangeTeamAction)} in the event {ParentEvent.Prefab.Identifier}. \"{TeamTag}\" is not a valid Team ID. Valid values are {string.Join(',', Enum.GetNames(typeof(CharacterTeamType)))}."); + } + } private List affectedNpcs = null; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs index 657c6af21..77661bdbc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/SpawnAction.cs @@ -139,7 +139,7 @@ namespace Barotrauma humanPrefab.GiveItems(newCharacter, newCharacter.Submarine); if (LootingIsStealing) { - foreach (Item item in newCharacter.Inventory.AllItems) + foreach (Item item in newCharacter.Inventory.FindAllItems(recursive: true)) { item.SpawnedInCurrentOutpost = true; item.AllowStealing = false; @@ -257,21 +257,11 @@ namespace Barotrauma { if (!SpawnPointTag.IsEmpty) { - List potentialItems = SpawnLocation switch - { - SpawnLocationType.MainSub => Item.ItemList.FindAll(it => it.Submarine == Submarine.MainSub), - SpawnLocationType.MainPath => Item.ItemList.FindAll(it => it.Submarine == null), - SpawnLocationType.Outpost => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsRuin), - SpawnLocationType.BeaconStation => Item.ItemList.FindAll(it => it.Submarine?.Info != null && it.Submarine.Info.IsBeacon), - _ => throw new NotImplementedException() - }; - + List potentialItems = Item.ItemList.FindAll(it => IsValidSubmarineType(SpawnLocation, it.Submarine)); var item = potentialItems.Where(it => it.HasTag(SpawnPointTag)).GetRandomUnsynced(); if (item != null) { return item; } - var target = ParentEvent.GetTargets(SpawnPointTag).GetRandomUnsynced(); + var target = ParentEvent.GetTargets(SpawnPointTag).Where(t => IsValidSubmarineType(SpawnLocation, t.Submarine)).GetRandomUnsynced(); if (target != null) { return target; } } @@ -281,19 +271,25 @@ namespace Barotrauma return GetSpawnPos(SpawnLocation, spawnPointType, targetModuleTags, SpawnPointTag.ToEnumerable(), requireTaggedSpawnPoint: RequireSpawnPointTag); } + private static bool IsValidSubmarineType(SpawnLocationType spawnLocation, Submarine submarine) + { + return spawnLocation switch + { + SpawnLocationType.MainSub => submarine == Submarine.MainSub, + SpawnLocationType.MainPath => submarine == null, + SpawnLocationType.Outpost => submarine is { Info: { IsOutpost: true } }, + SpawnLocationType.Wreck => submarine is { Info: { IsWreck: true } }, + SpawnLocationType.Ruin => submarine is { Info: { IsRuin: true } }, + SpawnLocationType.BeaconStation => submarine?.Info?.BeaconStationInfo != null, + _ => throw new NotImplementedException(), + }; + } + public static WayPoint GetSpawnPos(SpawnLocationType spawnLocation, SpawnType? spawnPointType, IEnumerable moduleFlags = null, IEnumerable spawnpointTags = null, bool asFarAsPossibleFromAirlock = false, bool requireTaggedSpawnPoint = false) { - List potentialSpawnPoints = spawnLocation switch - { - SpawnLocationType.MainSub => WayPoint.WayPointList.FindAll(wp => wp.Submarine == Submarine.MainSub && wp.CurrentHull != null), - SpawnLocationType.MainPath => WayPoint.WayPointList.FindAll(wp => wp.Submarine == null), - SpawnLocationType.Outpost => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.CurrentHull != null && wp.Submarine.Info.IsOutpost), - SpawnLocationType.Wreck => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsWreck), - SpawnLocationType.Ruin => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsRuin), - SpawnLocationType.BeaconStation => WayPoint.WayPointList.FindAll(wp => wp.Submarine?.Info != null && wp.Submarine.Info.IsBeacon), - _ => throw new NotImplementedException() - }; - + bool requireHull = spawnLocation == SpawnLocationType.MainSub || spawnLocation == SpawnLocationType.Outpost; + List potentialSpawnPoints = WayPoint.WayPointList.FindAll(wp => IsValidSubmarineType(spawnLocation, wp.Submarine) && (wp.CurrentHull != null || !requireHull)); + potentialSpawnPoints = potentialSpawnPoints.FindAll(wp => wp.ConnectedDoor == null && wp.Ladders == null && !wp.isObstructed); if (moduleFlags != null && moduleFlags.Any()) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs index 9b397d4be..cd6a3cae6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Events/EventSet.cs @@ -273,13 +273,13 @@ namespace Barotrauma IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); ResetTime = element.GetAttributeFloat("resettime", 0); - DefaultCommonness = 1.0f; + DefaultCommonness = element.GetAttributeFloat("commonness", 1.0f); foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "commonness": - DefaultCommonness = subElement.GetAttributeFloat("commonness", 0.0f); + DefaultCommonness = subElement.GetAttributeFloat("commonness", DefaultCommonness); foreach (XElement overrideElement in subElement.Elements()) { if (overrideElement.NameAsIdentifier() == "override") diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs index 4d06bc9a9..4347b8687 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/AutoItemPlacer.cs @@ -163,6 +163,7 @@ namespace Barotrauma { if (!subs.Contains(item.Submarine)) { continue; } if (item.GetRootInventoryOwner() is Character) { continue; } + if (item.NonInteractable) { continue; } containers.AddRange(item.GetComponents()); } containers.Shuffle(Rand.RandSync.ServerAndClient); diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs index 6fa21d9ae..edcf1532a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs @@ -158,6 +158,16 @@ namespace Barotrauma this.campaign = campaign; } + public static bool HasUnlockedStoreItem(ItemPrefab prefab) + { + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + if (character.HasStoreAccessForItem(prefab)) { return true; } + } + + return false; + } + private List GetItems(Identifier identifier, Dictionary> items, bool create = false) { if (items.TryGetValue(identifier, out var storeSpecificItems) && storeSpecificItems != null) diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs index 581566f00..76ba0fc8b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Factions.cs @@ -1,6 +1,7 @@ #nullable enable using Microsoft.Xna.Framework; using System; +using System.Linq; namespace Barotrauma { @@ -14,6 +15,13 @@ namespace Barotrauma Prefab = prefab; Reputation = new Reputation(metadata, this, prefab.MinReputation, prefab.MaxReputation, prefab.InitialReputation); } + + public bool IsAffiliated() + { + if (GameMain.GameSession?.Campaign?.Factions.MaxBy(static f => f.Reputation.Value) is not { } highestFaction) { return false; } + + return highestFaction.Reputation.Value < 0 || Prefab.Identifier == highestFaction.Prefab.Identifier; + } } internal class FactionPrefab : Prefab diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs index fe45446d3..ed89cf0c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs @@ -70,6 +70,15 @@ namespace Barotrauma } reputationChange *= reputationGainMultiplier; } + else if (reputationChange < 0f) + { + float reputationLossMultiplier = 1f; + foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) + { + reputationLossMultiplier += character.GetStatValue(StatTypes.ReputationLossMultiplier); + } + reputationChange *= reputationLossMultiplier; + } Value += reputationChange; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs index b6868a586..4409e74f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs @@ -139,6 +139,15 @@ namespace Barotrauma public virtual bool PurchasedLostShuttles { get; set; } public virtual bool PurchasedItemRepairs { get; set; } + private static bool AnyOneAllowedToManageCampaign(ClientPermissions permissions) + { + if (GameMain.NetworkMember == null) { return true; } + //allow managing if no-one with permissions is alive + return + GameMain.NetworkMember.ConnectedClients.Count == 1 || + GameMain.NetworkMember.ConnectedClients.None(c => c.InGame && c.Character is { IsIncapacitated: false, IsDead: false } && (IsOwner(c) || c.HasPermission(permissions))); + } + protected CampaignMode(GameModePreset preset, CampaignSettings settings) : base(preset) { @@ -156,7 +165,7 @@ namespace Barotrauma { if (!(e.ChangedData.BalanceChanged is Some { Value: var changed })) { return; } - if (changed != 0) { return; } + if (changed == 0) { return; } bool isGain = changed > 0; Color clr = isGain ? GUIStyle.Yellow : GUIStyle.Red; @@ -211,7 +220,7 @@ namespace Barotrauma return Level.Loaded?.StartLocation ?? Map.CurrentLocation; } - public List GetSubsToLeaveBehind(Submarine leavingSub) + public static List GetSubsToLeaveBehind(Submarine leavingSub) { //leave subs behind if they're not docked to the leaving sub and not at the same exit return Submarine.Loaded.FindAll(sub => @@ -266,7 +275,7 @@ namespace Barotrauma wasDocked = Level.Loaded.StartOutpost != null && connectedSubs.Contains(Level.Loaded.StartOutpost); } - public int GetHullRepairCost() + public static int GetHullRepairCost() { float totalDamage = 0; foreach (Structure wall in Structure.WallList) @@ -283,7 +292,7 @@ namespace Barotrauma return (int)Math.Min(totalDamage * HullRepairCostPerDamage, MaxHullRepairCost); } - public int GetItemRepairCost() + public static int GetItemRepairCost() { float totalRepairDuration = 0.0f; foreach (Item item in Item.ItemList) @@ -551,7 +560,7 @@ namespace Barotrauma /// /// Which submarine is at a position where it can leave the level and enter another one (if any). /// - private Submarine GetLeavingSub() + private static Submarine GetLeavingSub() { if (Level.IsLoadedOutpost) { @@ -1025,7 +1034,7 @@ namespace Barotrauma } } - protected void LeaveUnconnectedSubs(Submarine leavingSub) + protected static void LeaveUnconnectedSubs(Submarine leavingSub) { if (leavingSub != Submarine.MainSub && !leavingSub.DockedTo.Contains(Submarine.MainSub)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs index 1d96fa2fa..15c864bd7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignSettings.cs @@ -2,6 +2,7 @@ using Microsoft.Xna.Framework; using System.Collections.Generic; +using System.Linq; using System.Xml.Linq; namespace Barotrauma @@ -103,12 +104,7 @@ namespace Barotrauma private static int GetAddedMissionCount() { - int count = 0; - foreach (Character character in GameSession.GetSessionCrewCharacters(CharacterType.Both)) - { - count += (int)character.GetStatValue(StatTypes.ExtraMissionCount); - } - return count; + return GameSession.GetSessionCrewCharacters(CharacterType.Both).Max(static character => (int)character.GetStatValue(StatTypes.ExtraMissionCount)); } } } \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs index 1b56b6f5c..26bb6fc58 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs @@ -756,7 +756,7 @@ namespace Barotrauma /// public static ImmutableHashSet GetSessionCrewCharacters(CharacterType type) { - if (!(GameMain.GameSession.CrewManager is { } crewManager)) { return ImmutableHashSet.Empty; } + if (GameMain.GameSession.CrewManager is not { } crewManager) { return ImmutableHashSet.Empty; } IEnumerable players; IEnumerable bots; @@ -766,8 +766,8 @@ namespace Barotrauma players = GameMain.Server.ConnectedClients.Select(c => c.Character).Where(c => c?.Info != null && !c.IsDead); bots = crewManager.GetCharacters().Where(c => !c.IsRemotePlayer); #elif CLIENT - players = crewManager.GetCharacters().Where(c => c.IsPlayer); - bots = crewManager.GetCharacters().Where(c => c.IsBot); + players = crewManager.GetCharacters().Where(static c => c.IsPlayer); + bots = crewManager.GetCharacters().Where(static c => c.IsBot); #endif if (type.HasFlag(CharacterType.Bot)) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs index a35b86654..e4c16b822 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs @@ -177,12 +177,13 @@ namespace Barotrauma return; } - int price = prefab.Price.GetBuyprice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); + int price = prefab.Price.GetBuyPrice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation); int currentLevel = GetUpgradeLevel(prefab, category); - if (currentLevel + 1 > prefab.MaxLevel) + int maxLevel = prefab.GetMaxLevelForCurrentSub(); + if (currentLevel + 1 > maxLevel) { - DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {prefab.MaxLevel}). The transaction has been cancelled."); + DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {maxLevel}). The transaction has been cancelled."); return; } @@ -206,7 +207,7 @@ namespace Barotrauma price = 0; } - if (Campaign.TryPurchase(client, price)) + if (force || Campaign.TryPurchase(client, price)) { if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { @@ -472,7 +473,7 @@ namespace Barotrauma { int newLevel = BuyUpgrade(prefab, category, Submarine.MainSub, level); DebugConsole.Log($" - {category.Identifier}.{prefab.Identifier} lvl. {level}, new: ({newLevel})"); - SetUpgradeLevel(prefab, category, Math.Clamp(GetRealUpgradeLevel(prefab, category) + level, 0, prefab.MaxLevel)); + SetUpgradeLevel(prefab, category, GetRealUpgradeLevel(prefab, category) + level); } PendingUpgrades.Clear(); @@ -652,16 +653,13 @@ namespace Barotrauma /// /// Gets the progress that is shown on the store interface. - /// Includes values stored in the metadata and + /// Includes values stored in the metadata and , and takes submarine tier and class restrictions into account /// - /// - /// - /// public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category) { if (!Metadata.HasKey(FormatIdentifier(prefab, category))) { return GetPendingLevel(); } - return GetRealUpgradeLevel(prefab, category) + GetPendingLevel(); + return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), prefab.GetMaxLevelForCurrentSub()); int GetPendingLevel() { @@ -671,11 +669,8 @@ namespace Barotrauma } /// - /// Gets the level of the upgrade that is stored in the metadata. + /// Gets the level of the upgrade that is stored in the metadata. May be higher than the apparent level on the current sub if the player has switched to a lower-tier sub /// - /// - /// - /// public int GetRealUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category) { return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0); @@ -684,9 +679,6 @@ namespace Barotrauma /// /// Stores the target upgrade level in the campaign metadata. /// - /// - /// - /// private void SetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, int level) { Metadata.SetValue(FormatIdentifier(prefab, category), level); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs index 8ef6c05ad..40585fad2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/DockingPort.cs @@ -1164,11 +1164,14 @@ namespace Barotrauma.Items.Components public override void ReceiveSignal(Signal signal, Connection connection) { #if CLIENT - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && - !(GameMain.GameSession?.Campaign?.AllowedToManageCampaign(ClientPermissions.ManageMap) ?? false)) + if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (GameMain.GameSession?.Campaign != null && !CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap)) + { + return; + } #endif if (dockingCooldown > 0.0f) { return; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs index 8188d52c9..c6b403f9e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ElectricalDischarger.cs @@ -1,4 +1,5 @@ using Barotrauma.Networking; +using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; @@ -63,6 +64,9 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0.0f, IsPropertySaveable.No)] + public float RaycastRange { get; set; } + [Serialize(0.25f, IsPropertySaveable.Yes, description: "The duration of an individual discharge (in seconds)."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] public float Duration { @@ -70,6 +74,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(0.25f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.0f, MaxValueFloat = 60.0f, ValueStep = 0.1f, DecimalCount = 2)] + public float Reload + { + get; + set; + } + [Serialize(false, IsPropertySaveable.Yes, "If set to true, the discharge cannot travel inside the submarine nor shock anyone inside."), Editable] public bool OutdoorsOnly { @@ -77,6 +88,13 @@ namespace Barotrauma.Items.Components set; } + [Serialize(false, IsPropertySaveable.Yes)] + public bool IgnoreUser + { + get; + set; + } + private readonly List nodes = new List(); public IEnumerable Nodes { @@ -91,6 +109,10 @@ namespace Barotrauma.Items.Components private readonly Attack attack; + private Character user; + + private float reloadTimer; + public ElectricalDischarger(Item item, ContentXElement element) : base(item, element) { @@ -125,6 +147,7 @@ namespace Barotrauma.Items.Components charging = true; timer = Duration; IsActive = true; + user = character; #if SERVER if (GameMain.Server != null) { item.CreateServerEvent(this); } #endif @@ -144,6 +167,11 @@ namespace Barotrauma.Items.Components if (timer <= 0.0f) { + if (reloadTimer > 0.0f) + { + reloadTimer -= deltaTime; + return; + } IsActive = false; return; } @@ -196,6 +224,7 @@ namespace Barotrauma.Items.Components private void Discharge() { + reloadTimer = Reload; ApplyStatusEffects(ActionType.OnUse, 1.0f); FindNodes(item.WorldPosition, Range); if (attack != null) @@ -203,7 +232,7 @@ namespace Barotrauma.Items.Components foreach ((Character character, Node node) in charactersInRange) { if (character == null || character.Removed) { continue; } - character.ApplyAttack(null, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor)); + character.ApplyAttack(user, node.WorldPosition, attack, MathHelper.Clamp(Voltage, 1.0f, MaxOverVoltageFactor)); } } DischargeProjSpecific(); @@ -214,6 +243,18 @@ namespace Barotrauma.Items.Components public void FindNodes(Vector2 worldPosition, float range) { + if (RaycastRange > 0.0f) + { + float angle = 0.0f; + float dir = 1; + if (item.body != null) + { + angle += item.body.Rotation; + dir = item.body.Dir; + } + worldPosition += new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)) * RaycastRange * dir; + } + //see which submarines are within range so we can skip structures that are in far-away subs List submarinesInRange = new List(); foreach (Submarine sub in Submarine.Loaded) @@ -222,7 +263,7 @@ namespace Barotrauma.Items.Components { submarinesInRange.Add(sub); } - else + else if (sub != null) { Rectangle subBorders = new Rectangle( sub.Borders.X - (int)range, sub.Borders.Y + (int)range, @@ -263,26 +304,41 @@ namespace Barotrauma.Items.Components entitiesInRange.Add(structure); } + + nodes.Clear(); + if (RaycastRange > 0.0f) + { + nodes.Add(new Node(item.WorldPosition, -1)); + int parentNodeIndex = 0; + AddNodesBetweenPoints(item.WorldPosition, worldPosition, 0.5f, ref parentNodeIndex); + } + else + { + nodes.Add(new Node(worldPosition, -1)); + } + + float totalRange = RaycastRange + range; foreach (Character character in Character.CharacterList) { - if (!character.Enabled) continue; - if (OutdoorsOnly && character.Submarine != null) continue; - if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) continue; + if (!character.Enabled) { continue; } + if (IgnoreUser && character == user) { continue; } + if (OutdoorsOnly && character.Submarine != null) { continue; } + if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) { continue; } - if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < range * range * RangeMultiplierInWalls) + if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls || + (RaycastRange > 0.0f && MathUtils.LineToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition) < range * range * RangeMultiplierInWalls)) { entitiesInRange.Add(character); + charactersInRange.Add((character, nodes[0])); } } - nodes.Clear(); - nodes.Add(new Node(worldPosition, -1)); - FindNodes(entitiesInRange, worldPosition, 0, range); + FindNodes(entitiesInRange, worldPosition, nodes.Count - 1, range); //construct final nodes (w/ lengths and angles so they don't have to be recalculated when rendering the discharge) for (int i = 0; i < nodes.Count; i++) { - if (nodes[i].ParentIndex < 0) continue; + if (nodes[i].ParentIndex < 0) { continue; } Node parentNode = nodes[nodes[i].ParentIndex]; float length = Vector2.Distance(nodes[i].WorldPosition, parentNode.WorldPosition) * Rand.Range(1.0f, 1.25f); float angle = MathUtils.VectorToAngle(parentNode.WorldPosition - nodes[i].WorldPosition); @@ -292,7 +348,7 @@ namespace Barotrauma.Items.Components private void FindNodes(List entitiesInRange, Vector2 currPos, int parentNodeIndex, float currentRange) { - if (currentRange <= 0.0f || nodes.Count >= MaxNodes) return; + if (currentRange <= 0.0f || nodes.Count >= MaxNodes) { return; } //find the closest structure int closestIndex = -1; @@ -434,20 +490,21 @@ namespace Barotrauma.Items.Components for (int j = 0; j < entitiesInRange.Count; j++) { var otherEntity = entitiesInRange[j]; - if (!(otherEntity is Character character)) continue; - if (OutdoorsOnly && character.Submarine != null) continue; + if (otherEntity is not Character character) { continue; } + if (IgnoreUser && character == user) { continue; } + if (OutdoorsOnly && character.Submarine != null) { continue; } if (targetStructure.IsHorizontal) { - if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) continue; - if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) continue; - if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) continue; + if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) { continue; } + if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) { continue; } + if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) { continue; } } else { - if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) continue; - if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) continue; - if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) continue; + if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; } + if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) { continue; } + if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) { continue; } } float closestNodeDistSqr = float.MaxValue; int closestNodeIndex = -1; @@ -473,7 +530,10 @@ namespace Barotrauma.Items.Components AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex); nodes.Add(new Node(targetPos, parentNodeIndex)); entitiesInRange.RemoveAt(closestIndex); - charactersInRange.Add((character, nodes[parentNodeIndex])); + if (!charactersInRange.Any(c => c.character == character)) + { + charactersInRange.Add((character, nodes[parentNodeIndex])); + } FindNodes(entitiesInRange, targetPos, nodes.Count - 1, currentRange); } } @@ -483,7 +543,7 @@ namespace Barotrauma.Items.Components Vector2 diff = targetPos - currPos; float dist = diff.Length(); Vector2 normal = new Vector2(-diff.Y, diff.X) / dist; - for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.5f)) + for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.0f)) { //0 at the edges, 1 at the center float normalOffset = (0.5f - Math.Abs(x / dist - 0.5f)) * 2.0f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs index 5641b078f..aa5dc7d28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/Holdable.cs @@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components } } - const float MaxAttachDistance = 150.0f; + private const float MaxAttachDistance = ItemPrefab.DefaultInteractDistance * 0.95f; //the position(s) in the item that the Character grabs protected Vector2[] handlePos; @@ -127,7 +127,7 @@ namespace Barotrauma.Items.Components set { attachedByDefault = value; } } - [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ + [Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+ " For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")] public Vector2 HoldPos { @@ -143,7 +143,11 @@ namespace Barotrauma.Items.Components set { aimPos = ConvertUnits.ToSimUnits(value); } } +#if DEBUG [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "The rotation at which the character holds the item (in degrees, relative to the rotation of the character's hand).")] +#else + [Serialize(0.0f, IsPropertySaveable.No)] +#endif public float HoldAngle { get { return MathHelper.ToDegrees(holdAngle); } @@ -151,23 +155,50 @@ namespace Barotrauma.Items.Components } private Vector2 swingAmount; +#if DEBUG [Editable, Serialize("0.0,0.0", IsPropertySaveable.No, description: "How much the item swings around when aiming/holding it (in pixels, as an offset from AimPos/HoldPos).")] +#else + [Serialize("0.0,0.0", IsPropertySaveable.No)] +#endif public Vector2 SwingAmount { get { return ConvertUnits.ToDisplayUnits(swingAmount); } set { swingAmount = ConvertUnits.ToSimUnits(value); } } - +#if DEBUG [Editable, Serialize(0.0f, IsPropertySaveable.No, description: "How fast the item swings around when aiming/holding it (only valid if SwingAmount is set).")] +#else + [Serialize(0.0f, IsPropertySaveable.No)] +#endif + public float SwingSpeed { get; set; } +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being held.")] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool SwingWhenHolding { get; set; } + +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being aimed.")] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool SwingWhenAiming { get; set; } + +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No, description: "Should the item swing around when it's being used (for example, when firing a weapon or a welding tool).")] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool SwingWhenUsing { get; set; } + +#if DEBUG [Editable, Serialize(false, IsPropertySaveable.No)] +#else + [Serialize(false, IsPropertySaveable.No)] +#endif public bool DisableHeadRotation { get; set; } [ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")] @@ -731,10 +762,24 @@ namespace Barotrauma.Items.Components mouseDiff = mouseDiff.ClampLength(MaxAttachDistance); Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position; - Vector2 attachPos = userPos + mouseDiff; - if (user.Submarine == null && Level.Loaded != null) + if (user.Submarine != null) + { + if (Submarine.PickBody( + ConvertUnits.ToSimUnits(user.Position), + ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null) + { + attachPos = userPos + mouseDiff * Submarine.LastPickedFraction; + + //round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall + return + new Vector2( + mouseDiff.X > 0 ? (float)Math.Floor(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X, + mouseDiff.Y > 0 ? (float)Math.Floor(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.Y); + } + } + else if (Level.Loaded != null) { bool edgeFound = false; foreach (var cell in Level.Loaded.GetCells(attachPos)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs index da4e9b663..f0b500c43 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/MeleeWeapon.cs @@ -112,7 +112,7 @@ namespace Barotrauma.Items.Components reloadTimer = reload; reloadTimer /= 1f + character.GetStatValue(StatTypes.MeleeAttackSpeed); reloadTimer /= 1f + item.GetQualityModifier(Quality.StatType.StrikingSpeedMultiplier); - character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer; + character.AnimController.LockFlippingUntil = (float)Timing.TotalTime + reloadTimer * 0.9f; item.body.FarseerBody.CollisionCategories = Physics.CollisionProjectile; item.body.FarseerBody.CollidesWith = Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionItemBlocking; @@ -421,7 +421,7 @@ namespace Barotrauma.Items.Components if (targetItem.Removed) { return; } var attackResult = Attack.DoDamage(User, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) { Character.Controlled?.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs index 9986d7130..bf808c48a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RepairTool.cs @@ -514,7 +514,7 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) { - if (Rand.Range(0.0f, 1.0f) < FireProbability * deltaTime) + if (Rand.Range(0.0f, 1.0f) < FireProbability * deltaTime && item.CurrentHull != null) { Vector2 displayPos = ConvertUnits.ToDisplayUnits(rayStart + (rayEnd - rayStart) * lastPickedFraction * 0.9f); if (item.CurrentHull.Submarine != null) { displayPos += item.CurrentHull.Submarine.Position; } @@ -636,11 +636,14 @@ namespace Barotrauma.Items.Components float addedDetachTime = deltaTime * (1f + user.GetStatValue(StatTypes.RepairToolDeattachTimeMultiplier)) * (1f + item.GetQualityModifier(Quality.StatType.RepairToolDeattachTimeMultiplier)); levelResource.DeattachTimer += addedDetachTime; #if CLIENT - Character.Controlled?.UpdateHUDProgressBar( - this, - targetItem.WorldPosition, - levelResource.DeattachTimer / levelResource.DeattachDuration, - GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching"); + if (targetItem.Prefab.ShowHealthBar) + { + Character.Controlled?.UpdateHUDProgressBar( + this, + targetItem.WorldPosition, + levelResource.DeattachTimer / levelResource.DeattachDuration, + GUIStyle.Red, GUIStyle.Green, "progressbar.deattaching"); + } #endif FixItemProjSpecific(user, deltaTime, targetItem, showProgressBar: false); return true; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs index af53b043f..ca421f499 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemComponent.cs @@ -111,6 +111,13 @@ namespace Barotrauma.Items.Components private bool drawable = true; + [Serialize(PropertyConditional.Comparison.And, IsPropertySaveable.No)] + public PropertyConditional.Comparison IsActiveConditionalComparison + { + get; + set; + } + public List IsActiveConditionals; public bool Drawable @@ -241,6 +248,18 @@ namespace Barotrauma.Items.Components [Serialize(0, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] public int ManuallySelectedSound { get; private set; } + + /// + /// Can be used by status effects or conditionals to the speed of the item + /// + public float Speed + { + get + { + return item.Speed; + } + } + public ItemComponent(Item item, ContentXElement element) { this.item = item; @@ -814,7 +833,7 @@ namespace Barotrauma.Items.Components } } - public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f, float applyOnUserFraction = 0.0f) + public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f) { if (statusEffectLists == null) { return; } @@ -828,11 +847,6 @@ namespace Barotrauma.Items.Components if (user != null) { effect.SetUser(user); } effect.AfflictionMultiplier = afflictionMultiplier; item.ApplyStatusEffect(effect, type, deltaTime, character, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition); - if (user != null && applyOnUserFraction > 0.0f && effect.HasTargetType(StatusEffect.TargetType.Character)) - { - effect.AfflictionMultiplier = applyOnUserFraction; - item.ApplyStatusEffect(effect, type, deltaTime, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), useTarget, false, false, worldPosition); - } effect.AfflictionMultiplier = 1.0f; reducesCondition |= effect.ReducesItemCondition(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs index 1de5e3c80..21179fc74 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Deconstructor.cs @@ -104,12 +104,14 @@ namespace Barotrauma.Items.Components // doesn't quite work properly, remaining time changes if tinkering stops float deconstructionSpeedModifier = userDeconstructorSpeedMultiplier * (1f + tinkeringStrength * TinkeringSpeedIncrease); + float deconstructionSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DeconstructorSpeed, DeconstructionSpeed); + if (DeconstructItemsSimultaneously) { float deconstructTime = 0.0f; foreach (Item targetItem in inputContainer.Inventory.AllItems) { - deconstructTime += targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier); + deconstructTime += targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier); } progressState = Math.Min(progressTimer / deconstructTime, 1.0f); @@ -139,7 +141,7 @@ namespace Barotrauma.Items.Components if (targetItem == null) { return; } var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList(); - float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (DeconstructionSpeed * deconstructionSpeedModifier) : 1.0f; + float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f; progressState = Math.Min(progressTimer / deconstructTime, 1.0f); if (progressTimer > deconstructTime) @@ -218,7 +220,7 @@ namespace Barotrauma.Items.Components if (percentageHealth < deconstructProduct.MinCondition || percentageHealth > deconstructProduct.MaxCondition) { return; } - if (!(MapEntityPrefab.Find(null, deconstructProduct.ItemIdentifier) is ItemPrefab itemPrefab)) + if (MapEntityPrefab.FindByIdentifier(deconstructProduct.ItemIdentifier) is not ItemPrefab itemPrefab) { DebugConsole.ThrowError("Tried to deconstruct item \"" + targetItem.Name + "\" but couldn't find item prefab \"" + deconstructProduct.ItemIdentifier + "\"!"); return; @@ -284,9 +286,10 @@ namespace Barotrauma.Items.Components { Entity.Spawner.AddItemToSpawnQueue(itemPrefab, outputContainer.Inventory, condition, onSpawned: (Item spawnedItem) => { - spawnedItem.SpawnedInCurrentOutpost = item.SpawnedInCurrentOutpost; spawnedItem.StolenDuringRound = targetItem.StolenDuringRound; spawnedItem.AllowStealing = targetItem.AllowStealing; + spawnedItem.OriginalOutpost = targetItem.OriginalOutpost; + spawnedItem.SpawnedInCurrentOutpost = targetItem.SpawnedInCurrentOutpost; for (int i = 0; i < outputContainer.Capacity; i++) { var containedItem = outputContainer.Inventory.GetItemAt(i); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs index 13f858f37..d6ae48ec5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Engine.cs @@ -30,11 +30,8 @@ namespace Barotrauma.Items.Components Serialize(500.0f, IsPropertySaveable.Yes, description: "The amount of force exerted on the submarine when the engine is operating at full power.")] public float MaxForce { - get { return maxForce; } - set - { - maxForce = Math.Max(0.0f, value); - } + get => maxForce; + set => maxForce = Math.Max(0.0f, value); } [Editable, Serialize("0.0,0.0", IsPropertySaveable.Yes, @@ -94,7 +91,7 @@ namespace Barotrauma.Items.Components } partial void InitProjSpecific(ContentXElement element); - + public override void Update(float deltaTime, Camera cam) { UpdateOnActiveEffects(deltaTime); @@ -129,12 +126,14 @@ namespace Barotrauma.Items.Components { forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100)); } - currForce *= maxForce * forceMultiplier; - if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) + currForce *= item.StatManager.GetAdjustedValue(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier; + if (item.GetComponent() is { IsTinkering: true } repairable) { currForce *= 1f + repairable.TinkeringStrength * TinkeringForceIncrease; } + currForce = item.StatManager.GetAdjustedValue(ItemTalentStats.EngineSpeed, currForce); + //less effective when in a bad condition currForce *= MathHelper.Lerp(0.5f, 2.0f, condition); if (item.Submarine.FlippedX) { currForce *= -1; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs index 9047d2f5e..537366213 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Fabricator.cs @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components { DebugConsole.ThrowError("Error in item " + item.Name + "! Fabrication recipes should be defined in the craftable item's xml, not in the fabricator."); break; - } + } } var fabricationRecipes = new Dictionary(); @@ -104,6 +104,18 @@ namespace Barotrauma.Items.Components continue; } } + + bool recipeInvalid = false; + foreach (var requiredItem in recipe.RequiredItems) + { + if (requiredItem.ItemPrefabs.None()) + { + DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Could not find the ingredient \"{requiredItem}\"."); + recipeInvalid = true; + } + } + if (recipeInvalid) { continue; } + fabricationRecipes.Add(recipe.RecipeHash, recipe); if (recipe.FabricationLimitMax >= 0) { @@ -356,9 +368,10 @@ namespace Barotrauma.Items.Components bool ingredientsStolen = false; bool ingredientsAllowStealing = true; - if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer) + if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer) { - fabricatedItem.RequiredItems.ForEach(requiredItem => + List foundAvailableItems = new List(); + foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems) { for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++) { @@ -367,10 +380,7 @@ namespace Barotrauma.Items.Components if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; } var availableItems = availableIngredients[requiredPrefab.Identifier]; - var availableItem = availableItems.FirstOrDefault(potentialPrefab => - { - return requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage); - }); + var availableItem = availableItems.FirstOrDefault(potentialPrefab => requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage)); if (availableItem == null) { continue; } @@ -401,13 +411,21 @@ namespace Barotrauma.Items.Components } } + foundAvailableItems.Add(availableItem); availableItems.Remove(availableItem); - Entity.Spawner.AddItemToRemoveQueue(availableItem); - inputContainer.Inventory.RemoveItem(availableItem); break; } } - }); + } + + var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems); + user.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients); + + foreach (Item availableItem in fabricationIngredients.Items) + { + Entity.Spawner.AddItemToRemoveQueue(availableItem); + inputContainer.Inventory.RemoveItem(availableItem); + } int amountFittingContainer = outputContainer.Inventory.HowManyCanBePut(fabricatedItem.TargetItem, fabricatedItem.OutCondition * fabricatedItem.TargetItem.Health); @@ -535,12 +553,13 @@ namespace Barotrauma.Items.Components return currPowerConsumption; } - private int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) + private static int GetFabricatedItemQuality(FabricationRecipe fabricatedItem, Character user) { - if (user == null) { return 0; } + if (user?.Info == null) { return 0; } if (fabricatedItem.TargetItem.ConfigElement.GetChildElement("Quality") == null) { return 0; } int quality = 0; float floatQuality = 0.0f; + floatQuality += user.GetStatValue(StatTypes.IncreaseFabricationQuality); foreach (var tag in fabricatedItem.TargetItem.Tags) { floatQuality += user.Info.GetSavedStatValue(StatTypes.IncreaseFabricationQuality, tag); @@ -637,9 +656,14 @@ namespace Barotrauma.Items.Components //fabricating takes 100 times longer if degree of success is close to 0 //characters with a higher skill than required can fabricate up to 100% faster - return fabricableItem.RequiredTime / FabricationSpeed / MathHelper.Clamp(t, 0.01f, 2.0f); + float time = fabricableItem.RequiredTime / item.StatManager.GetAdjustedValue(ItemTalentStats.FabricationSpeed, FabricationSpeed) / MathHelper.Clamp(t, 0.01f, 2.0f); + if (user is not null && fabricableItem.TargetItem is { } it && it.Tags.Contains("medical")) + { + time *= 1f + user.GetStatValue(StatTypes.FabricateMedicineSpeedMultiplier); + } + return time; } - + public float FabricationDegreeOfSuccess(Character character, ImmutableArray skills) { if (skills.Length == 0) { return 1.0f; } @@ -713,7 +737,14 @@ namespace Barotrauma.Items.Components { availableIngredients[itemIdentifier] = new List(itemList.Count); } - availableIngredients[itemIdentifier].Add(item); + //order by condition (prefer using worst-condition items) + int index = 0; + while (index < availableIngredients[itemIdentifier].Count && + availableIngredients[itemIdentifier][index].Condition < item.Condition) + { + index++; + } + availableIngredients[itemIdentifier].Insert(index, item); } } @@ -827,5 +858,15 @@ namespace Barotrauma.Items.Components public float Value { get; set; } public ItemPrefab ItemPrefab { get; set; } } + + internal sealed class AbilityFabricationItemIngredients : AbilityObject + { + public List Items { get; set; } + + public AbilityFabricationItemIngredients(List items) + { + Items = items; + } + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs index db2eae084..923623a3e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Pump.cs @@ -57,8 +57,8 @@ namespace Barotrauma.Items.Components [Editable, Serialize(80.0f, IsPropertySaveable.No, description: "How fast the item pumps water in/out when operating at 100%.", alwaysUseInstanceValues: true)] public float MaxFlow { - get { return maxFlow; } - set { maxFlow = value; } + get => maxFlow; + set => maxFlow = value; } [Editable, Serialize(true, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)] @@ -92,13 +92,16 @@ namespace Barotrauma.Items.Components } partial void InitProjSpecific(ContentXElement element); - + public override void Update(float deltaTime, Camera cam) { pumpSpeedLockTimer -= deltaTime; isActiveLockTimer -= deltaTime; - if (!IsActive) { return; } + if (!IsActive) + { + return; + } currFlow = 0.0f; @@ -122,7 +125,10 @@ namespace Barotrauma.Items.Components FlowPercentage = ((float)TargetLevel - hullPercentage) * 10.0f; } - if (!HasPower) { return; } + if (!HasPower) + { + return; + } UpdateProjSpecific(deltaTime); @@ -132,13 +138,15 @@ namespace Barotrauma.Items.Components float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor); - currFlow = flowPercentage / 100.0f * maxFlow * powerFactor; + currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValue(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor; - if (item.GetComponent() is Repairable repairable && repairable.IsTinkering) + if (item.GetComponent() is { IsTinkering: true } repairable) { currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease; } + currFlow = item.StatManager.GetAdjustedValue(ItemTalentStats.PumpSpeed, currFlow); + //less effective when in a bad condition currFlow *= MathHelper.Lerp(0.5f, 1.0f, item.Condition / item.MaxCondition); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs index 4630a0bf2..8a70da4c7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Reactor.cs @@ -83,19 +83,24 @@ namespace Barotrauma.Items.Components { if (lastUser == value) { return; } lastUser = value; - degreeOfSuccess = lastUser == null ? 0.0f : Math.Min(DegreeOfSuccess(lastUser), 1.0f); - LastUserWasPlayer = lastUser.IsPlayer; + if (lastUser == null) + { + degreeOfSuccess = 0.0f; + LastUserWasPlayer = false; + } + else + { + degreeOfSuccess = Math.Min(DegreeOfSuccess(lastUser), 1.0f); + LastUserWasPlayer = lastUser.IsPlayer; + } } } - + [Editable(0.0f, float.MaxValue), Serialize(10000.0f, IsPropertySaveable.Yes, description: "How much power (kW) the reactor generates when operating at full capacity.", alwaysUseInstanceValues: true)] public float MaxPowerOutput { - get { return maxPowerOutput; } - set - { - maxPowerOutput = Math.Max(0.0f, value); - } + get => maxPowerOutput; + set => maxPowerOutput = Math.Max(0.0f, value); } [Editable(0.0f, float.MaxValue), Serialize(120.0f, IsPropertySaveable.Yes, description: "How long the temperature has to stay critical until a meltdown occurs.")] @@ -144,11 +149,11 @@ namespace Barotrauma.Items.Components turbineOutput = MathHelper.Clamp(value, 0.0f, 100.0f); } } - + [Serialize(0.2f, IsPropertySaveable.Yes, description: "How fast the condition of the contained fuel rods deteriorates per second."), Editable(0.0f, 1000.0f, decimals: 3)] public float FuelConsumptionRate { - get { return fuelConsumptionRate; } + get => fuelConsumptionRate; set { if (!MathUtils.IsValid(value)) return; @@ -248,6 +253,8 @@ namespace Barotrauma.Items.Components } #endif + float maxPowerOut = GetMaxOutput(); + if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1) { TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f); @@ -281,9 +288,9 @@ namespace Barotrauma.Items.Components //use a smoothed "correct output" instead of the actual correct output based on the load //so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily - if (!MathUtils.NearlyEqual(MaxPowerOutput, 0.0f)) + if (!MathUtils.NearlyEqual(maxPowerOut, 0.0f)) { - CorrectTurbineOutput += MathHelper.Clamp((Load / MaxPowerOutput * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime; + CorrectTurbineOutput += MathHelper.Clamp((Load / maxPowerOut * 100.0f) - CorrectTurbineOutput, -20.0f, 20.0f) * deltaTime; } //calculate tolerances of the meters based on the skills of the user @@ -342,7 +349,7 @@ namespace Barotrauma.Items.Components if (!isConnectedToFriendlyOutpost) { - item.Condition -= fissionRate / 100.0f * fuelConsumptionRate * deltaTime; + item.Condition -= fissionRate / 100.0f * GetFuelConsumption() * deltaTime; } } fuelLeft += item.ConditionPercentage; @@ -351,10 +358,10 @@ namespace Barotrauma.Items.Components if (fissionRate > 0.0f) { - if (item.AiTarget != null && MaxPowerOutput > 0) + if (item.AiTarget != null && maxPowerOut > 0) { var aiTarget = item.AiTarget; - float range = Math.Abs(currPowerConsumption) / MaxPowerOutput; + float range = Math.Abs(currPowerConsumption) / maxPowerOut; aiTarget.SoundRange = MathHelper.Lerp(aiTarget.MinSoundRange, aiTarget.MaxSoundRange, range); if (item.CurrentHull != null) { @@ -425,15 +432,17 @@ namespace Barotrauma.Items.Components tolerance = 3f; } + float maxPowerOut = GetMaxOutput(); + float temperatureFactor = Math.Min(temperature / 50.0f, 1.0f); - float minOutput = MaxPowerOutput * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1); - float maxOutput = MaxPowerOutput * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor); + float minOutput = maxPowerOut * Math.Clamp(Math.Min((turbineOutput - tolerance) / 100.0f, temperatureFactor), 0, 1); + float maxOutput = maxPowerOut * Math.Min((turbineOutput + tolerance) / 100.0f, temperatureFactor); minUpdatePowerOut = minOutput; maxUpdatePowerOut = maxOutput; - float reactorMax = PowerOn ? MaxPowerOutput : maxUpdatePowerOut; - + float reactorMax = PowerOn ? maxPowerOut : maxUpdatePowerOut; + return new PowerRange(minOutput, maxOutput, reactorMax); } @@ -456,11 +465,13 @@ namespace Barotrauma.Items.Components float output = MathHelper.Clamp(ratio * (maxUpdatePowerOut - minUpdatePowerOut) + minUpdatePowerOut, minUpdatePowerOut, maxUpdatePowerOut); float newLoad = loadLeft; + float maxOutput = GetMaxOutput(); + //Adjust behaviour for multi reactor setup - if (MaxPowerOutput != minMaxPower.ReactorMaxOutput) + if (maxOutput != minMaxPower.ReactorMaxOutput) { - float idealLoad = MaxPowerOutput / minMaxPower.ReactorMaxOutput * loadLeft; - float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * MaxPowerOutput), -MaxPowerOutput / 100, MaxPowerOutput / 100); + float idealLoad = maxOutput / minMaxPower.ReactorMaxOutput * loadLeft; + float loadAdjust = MathHelper.Clamp((ratio - 0.5f) * 25 + idealLoad - (turbineOutput / 100 * maxOutput), -maxOutput / 100, maxOutput / 100); newLoad = MathHelper.Clamp(loadLeft - (expectedPower - output) + loadAdjust, 0, loadLeft); } @@ -501,7 +512,7 @@ namespace Barotrauma.Items.Components //calculate the maximum output if the fission rate is cranked as high as it goes and turbine output is at max float theoreticalMaxHeat = GetGeneratedHeat(fissionRate: maxFissionRate); float temperatureFactor = Math.Min(theoreticalMaxHeat / 50.0f, 1.0f); - float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * MaxPowerOutput; + float theoreticalMaxOutput = Math.Min(maxTurbineOutput / 100.0f, temperatureFactor) * GetMaxOutput(); //maximum output not enough, we need more fuel return theoreticalMaxOutput < Load * minimumOutputRatio; @@ -685,7 +696,7 @@ namespace Barotrauma.Items.Components aiUpdateTimer = AIUpdateInterval; // load more fuel if the current maximum output is only 50% of the current load // or if the fuel rod is (almost) deplenished - float minCondition = fuelConsumptionRate * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2); + float minCondition = GetFuelConsumption() * MathUtils.Pow2((degreeOfSuccess - refuelLimit) * 2); if (NeedMoreFuel(minimumOutputRatio: 0.5f, minCondition: minCondition)) { bool outOfFuel = false; @@ -863,5 +874,8 @@ namespace Barotrauma.Items.Components if (GameMain.NetworkMember is { IsServer: true }) { unsentChanges = true; } } } + + private float GetMaxOutput() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorMaxOutput, MaxPowerOutput); + private float GetFuelConsumption() => item.StatManager.GetAdjustedValue(ItemTalentStats.ReactorFuelEfficiency, fuelConsumptionRate); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs index 077ae053f..6e727c30d 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Sonar.cs @@ -153,13 +153,6 @@ namespace Barotrauma.Items.Components bool changed = currentMode != value; currentMode = value; - if (value == Mode.Passive) - { - if (item.AiTarget != null) - { - item.AiTarget.SectorDegrees = 360.0f; - } - } #if CLIENT if (changed) { prevPassivePingRadius = float.MaxValue; } UpdateGUIElements(); @@ -204,15 +197,13 @@ namespace Barotrauma.Items.Components if (currentPingIndex != -1) { var activePing = activePings[currentPingIndex]; + if (item.AiTarget != null) + { + float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom); + item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range); + } if (activePing.State > 1.0f) { - if (item.AiTarget != null) - { - float range = MathUtils.InverseLerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, Range * activePing.State / zoom); - item.AiTarget.SoundRange = MathHelper.Lerp(item.AiTarget.MinSoundRange, item.AiTarget.MaxSoundRange, range); - item.AiTarget.SectorDegrees = activePing.IsDirectional ? DirectionalPingSector : 360.0f; - item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); - } aiPingCheckPending = true; currentPingIndex = -1; } @@ -228,15 +219,16 @@ namespace Barotrauma.Items.Components activePings[currentPingIndex].Direction = pingDirection; activePings[currentPingIndex].State = 0.0f; activePings[currentPingIndex].PrevPingRadius = 0.0f; + if (item.AiTarget != null) + { + item.AiTarget.SectorDegrees = useDirectionalPing ? DirectionalPingSector : 360.0f; + item.AiTarget.SectorDir = new Vector2(pingDirection.X, -pingDirection.Y); + } item.Use(deltaTime); } } else { - if (item.AiTarget != null) - { - item.AiTarget.SectorDegrees = 360.0f; - } aiPingCheckPending = false; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs index 67c2f4a94..b1ce29df7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Power/PowerContainer.cs @@ -65,7 +65,7 @@ namespace Barotrauma.Items.Components [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "The maximum capacity of the device (kW * min). For example, a value of 1000 means the device can output 100 kilowatts of power for 10 minutes, or 1000 kilowatts for 1 minute.")] public float Capacity { - get { return capacity; } + get => capacity; set { capacity = Math.Max(value, 1.0f); } } @@ -89,7 +89,7 @@ namespace Barotrauma.Items.Components } } - public float ChargePercentage => MathUtils.Percentage(Charge, Capacity); + public float ChargePercentage => MathUtils.Percentage(Charge, GetCapacity()); [Editable, Serialize(10.0f, IsPropertySaveable.Yes, description: "How fast the device can be recharged. For example, a recharge speed of 100 kW and a capacity of 1000 kW*min would mean it takes 10 minutes to fully charge the device.")] public float MaxRechargeSpeed @@ -125,10 +125,19 @@ namespace Barotrauma.Items.Components set { efficiency = MathHelper.Clamp(value, 0.0f, 1.0f); } } + private bool flipIndicator; + [Editable, Serialize(false, IsPropertySaveable.Yes, description: "Should the progress bar indicating the charge be flipped to fill from the other side.")] + public bool FlipIndicator + { + get { return flipIndicator; } + set { flipIndicator = value; } + } + public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed; public const float aiRechargeTargetRatio = 0.5f; private bool isRunning; + public bool HasBeenTuned { get; private set; } public PowerContainer(Item item, ContentXElement element) @@ -146,7 +155,7 @@ namespace Barotrauma.Items.Components return picker != null; } - public override void Update(float deltaTime, Camera cam) + public override void Update(float deltaTime, Camera cam) { if (item.Connections == null) { @@ -283,7 +292,7 @@ namespace Barotrauma.Items.Components else { //Decrease charge based on how much power is leaving the device - Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, Capacity); + Charge = Math.Clamp(Charge - CurrPowerOutput / 60 * UpdateInterval, 0, GetCapacity()); prevCharge = Charge; } } @@ -370,5 +379,7 @@ namespace Barotrauma.Items.Components } } } + + public float GetCapacity() => item.StatManager.GetAdjustedValue(ItemTalentStats.BatteryCapacity, Capacity); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs index b2d26005a..a56e5afde 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs @@ -736,6 +736,7 @@ namespace Barotrauma.Items.Components { return false; } + if (target.IsSensor) { return false; } if (hits.Contains(target.Body)) { return false; } if (target.Body.UserData is Submarine) { @@ -881,7 +882,7 @@ namespace Barotrauma.Items.Components { attackResult = Attack.DoDamage(User ?? Attacker, targetItem, item.WorldPosition, 1.0f); #if CLIENT - if (attackResult.Damage > 0.0f) + if (attackResult.Damage > 0.0f && targetItem.Prefab.ShowHealthBar) { Character.Controlled?.UpdateHUDProgressBar(targetItem, targetItem.WorldPosition, diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs index e486300c6..e5604f449 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Repairable.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; +using Barotrauma.Abilities; namespace Barotrauma.Items.Components { @@ -420,7 +421,8 @@ namespace Barotrauma.Items.Components if (item.ConditionPercentage > MinDeteriorationCondition) { - item.Condition -= DeteriorationSpeed * deltaTime; + float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed); + item.Condition -= deteriorationSpeed * deltaTime; } } return; @@ -467,8 +469,14 @@ namespace Barotrauma.Items.Components wasGoodCondition = true; } + float talentMultiplier = CurrentFixer.GetStatValue(StatTypes.RepairSpeed); + if (requiredSkills.Any(static skill => skill.Identifier == "mechanical")) + { + talentMultiplier += CurrentFixer.GetStatValue(StatTypes.MechanicalRepairSpeed); + } + float fixDuration = MathHelper.Lerp(FixDurationLowSkill, FixDurationHighSkill, successFactor); - fixDuration /= 1 + CurrentFixer.GetStatValue(StatTypes.RepairSpeed) + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; + fixDuration /= 1 + talentMultiplier + currentRepairItem?.Prefab.AddedRepairSpeedMultiplier ?? 0f; fixDuration /= 1 + item.GetQualityModifier(Quality.StatType.RepairSpeed); item.MaxRepairConditionMultiplier = GetMaxRepairConditionMultiplier(CurrentFixer); @@ -500,7 +508,7 @@ namespace Barotrauma.Items.Components SkillSettings.Current.SkillIncreasePerRepair / Math.Max(characterSkillLevel, 1.0f)); } SteamAchievementManager.OnItemRepaired(item, CurrentFixer); - CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete); + CurrentFixer.CheckTalents(AbilityEffectType.OnRepairComplete, new AbilityRepairable(item)); } if (CurrentFixer?.SelectedItem == item) { CurrentFixer.SelectedItem = null; } deteriorationTimer = Rand.Range(MinDeteriorationDelay, MaxDeteriorationDelay); @@ -687,4 +695,14 @@ namespace Barotrauma.Items.Components //where set_active/set_state signals can disable the component } } + + internal sealed class AbilityRepairable : AbilityObject, IAbilityItem + { + public Item Item { get; set; } + + public AbilityRepairable(Item item) + { + Item = item; + } + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs index 57b4dc685..67084c123 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Wearable.cs @@ -270,6 +270,9 @@ namespace Barotrauma.Items.Components public bool AutoEquipWhenFull { get; private set; } public bool DisplayContainedStatus { get; private set; } + [Serialize(false, IsPropertySaveable.No, description: "Can the item be used (assuming it has components that are usable in some way) when worn."), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f)] + public bool AllowUseWhenWorn { get; set; } + public readonly int Variants; private int variant; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs index afb94878c..2a9d2ff17 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Inventory.cs @@ -226,6 +226,14 @@ namespace Barotrauma { foreach (var item in slots[i].Items) { + if (item == null) + { +#if DEBUG + DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!"); +#endif + continue; + } + bool duplicateFound = false; for (int j = 0; j < i; j++) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 8181756f4..8902fd99e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -424,6 +424,39 @@ namespace Barotrauma public Color? HighlightColor; + /// + /// Can be used by status effects or conditionals to check whether the item is contained inside something + /// + public bool IsContained + { + get + { + return parentInventory != null; + } + } + + /// + /// Can be used by status effects or conditionals to the speed of the item + /// + public float Speed + { + get + { + if (body != null && body.PhysEnabled) + { + return body.LinearVelocity.Length(); + } + else if (ParentInventory?.Owner is Character character) + { + return character.AnimController.MainLimb.LinearVelocity.Length(); + } + else if (container != null) + { + return container.Speed; + } + return 0.0f; + } + } [Serialize("", IsPropertySaveable.Yes)] @@ -821,6 +854,16 @@ namespace Barotrauma public bool IsSecondaryItem { get; } + private ItemStatManager statManager; + public ItemStatManager StatManager + { + get + { + statManager ??= new ItemStatManager(this); + return statManager; + } + } + public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true) : this(new Rectangle( (int)(position.X - itemPrefab.Sprite.size.X / 2 * itemPrefab.Scale), @@ -1837,16 +1880,32 @@ namespace Barotrauma if (ic.IsActiveConditionals != null) { - bool shouldBeActive = true; - foreach (var conditional in ic.IsActiveConditionals) + if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And) { - if (!ConditionalMatches(conditional)) + bool shouldBeActive = true; + foreach (var conditional in ic.IsActiveConditionals) { - shouldBeActive = false; - break; + if (!ConditionalMatches(conditional)) + { + shouldBeActive = false; + break; + } } + ic.IsActive = shouldBeActive; + } + else + { + bool shouldBeActive = false; + foreach (var conditional in ic.IsActiveConditionals) + { + if (ConditionalMatches(conditional)) + { + shouldBeActive = true; + break; + } + } + ic.IsActive = shouldBeActive; } - ic.IsActive = shouldBeActive; } #if CLIENT if (ic.HasSounds) @@ -2072,7 +2131,7 @@ namespace Barotrauma } //no need to apply buoyancy if the item is still and not light enough to float - if (moving || body.Density < 10.0f) + if (moving || body.Density <= 10.0f) { Vector2 buoyancy = -GameMain.World.Gravity * forceFactor * volume * Physics.NeutralDensity; body.ApplyForce(buoyancy); @@ -2699,8 +2758,6 @@ namespace Barotrauma } #endif - float applyOnSelfFraction = user?.GetStatValue(StatTypes.ApplyTreatmentsOnSelfFraction) ?? 0.0f; - bool remove = false; foreach (ItemComponent ic in components) { @@ -2713,19 +2770,7 @@ namespace Barotrauma ic.PlaySound(actionType, user); #endif ic.WasUsed = true; - ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user, applyOnUserFraction: applyOnSelfFraction); - - if (applyOnSelfFraction > 0.0f) - { - //hacky af - ic.statusEffectLists.TryGetValue(actionType, out var effectList); - if (effectList != null) - { - effectList.ForEach(e => e.AfflictionMultiplier = applyOnSelfFraction); - ic.ApplyStatusEffects(actionType, 1.0f, user, targetLimb == null ? null : user.AnimController.GetLimb(targetLimb.type), user: user); - effectList.ForEach(e => e.AfflictionMultiplier = 1.0f); - } - } + ic.ApplyStatusEffects(actionType, 1.0f, character, targetLimb, user: user); if (GameMain.NetworkMember is { IsServer: true }) { @@ -2866,15 +2911,20 @@ namespace Barotrauma //to ensure client/server doesn't get any properties mixed up if there's some conditions that can vary between the server and the clients var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties(); SerializableProperty property = extraData.SerializableProperty; + ISerializableEntity entity = extraData.Entity; if (property != null) { - var propertyOwner = allProperties.Find(p => p.property == property); if (allProperties.Count > 1) { - msg.WriteByte((byte)allProperties.FindIndex(p => p.property == property)); + int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity); + if (propertyIndex < -1) + { + throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\""); + } + msg.WriteVariableUInt32((uint)propertyIndex); } - object value = property.GetValue(propertyOwner.obj); + object value = property.GetValue(entity); if (value is string stringVal) { msg.WriteString(stringVal); @@ -2979,7 +3029,7 @@ namespace Barotrauma int propertyIndex = 0; if (allProperties.Count > 1) { - propertyIndex = msg.ReadByte(); + propertyIndex = (int)msg.ReadVariableUInt32(); } bool allowEditing = true; @@ -3119,14 +3169,14 @@ namespace Barotrauma } logPropertyChangeCoroutine = CoroutineManager.Invoke(() => { - GameServer.Log($"{sender.Character.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction); + GameServer.Log($"{sender.Character?.Name ?? sender.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction); }, delay: 1.0f); } #endif - if (GameMain.NetworkMember is { IsServer: true }) + if (GameMain.NetworkMember is { IsServer: true } && parentObject is ISerializableEntity entity) { - GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property)); + GameMain.NetworkMember.CreateEntityEvent(this, new ChangePropertyEventData(property, entity)); } } @@ -3230,7 +3280,7 @@ namespace Barotrauma { if (!(property.GetValue(item)?.Equals(prevValue) ?? true)) { - GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property)); + GameMain.NetworkMember.CreateEntityEvent(item, new ChangePropertyEventData(property, item)); } } } @@ -3349,8 +3399,24 @@ namespace Barotrauma item.PurchasedNewSwap = false; } - item.condition = element.GetAttributeFloat("condition", item.condition); - item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); + if (element.GetAttribute("conditionpercentage") != null) + { + item.condition = element.GetAttributeFloat("conditionpercentage", 100.0f) / 100.0f * item.MaxCondition; + } + else + { + //backwards compatibility + item.condition = element.GetAttributeFloat("condition", item.condition); + //if the item was in full condition considering the unmodified health + //(not taking possible HealthMultipliers added by mods into account), + //make sure it stays in full condition + bool wasFullCondition = item.condition >= item.Prefab.Health; + if (wasFullCondition) + { + item.condition = item.MaxCondition; + } + item.condition = MathHelper.Clamp(item.condition, 0, item.MaxCondition); + } item.lastSentCondition = item.condition; item.RecalculateConditionValues(); item.SetActiveSprite(); @@ -3370,6 +3436,7 @@ namespace Barotrauma foreach (ItemComponent component in item.components) { + if (component.Parent != null) { component.IsActive = component.Parent.IsActive; } component.OnItemLoaded(); } @@ -3401,11 +3468,6 @@ namespace Barotrauma element.Add(new XAttribute("availableswaps", string.Join(',', AvailableSwaps.Select(s => s.Identifier)))); } - if (condition < MaxCondition) - { - element.Add(new XAttribute("condition", condition.ToString("G", CultureInfo.InvariantCulture))); - } - if (!MathUtils.NearlyEqual(healthMultiplier, 1.0f)) { element.Add(new XAttribute("healthmultiplier", HealthMultiplier.ToString("G", CultureInfo.InvariantCulture))); @@ -3442,6 +3504,16 @@ namespace Barotrauma upgrade.Save(element); } + if (condition < MaxCondition) + { + element.Add(new XAttribute("conditionpercentage", ConditionPercentage.ToString("G", CultureInfo.InvariantCulture))); + } + else + { + var conditionAttribute = element.GetAttribute("condition"); + if (conditionAttribute != null) { conditionAttribute.Remove(); } + } + parentElement.Add(element); return element; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs index e0c65a747..77b03826f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemEventData.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Barotrauma.Items.Components; using Barotrauma.Networking; using Microsoft.Xna.Framework; @@ -18,9 +19,10 @@ namespace Barotrauma AssignCampaignInteraction = 6, ApplyStatusEffect = 7, Upgrade = 8, + ItemStat = 9, MinValue = 0, - MaxValue = 6 + MaxValue = 9 } public interface IEventData : NetEntityEvent.IData @@ -56,10 +58,24 @@ namespace Barotrauma { public EventType EventType => EventType.ChangeProperty; public readonly SerializableProperty SerializableProperty; + public readonly ISerializableEntity Entity; - public ChangePropertyEventData(SerializableProperty serializableProperty) + public ChangePropertyEventData(SerializableProperty serializableProperty, ISerializableEntity entity) { SerializableProperty = serializableProperty; + Entity = entity; + } + } + + public readonly struct SetItemStatEventData : IEventData + { + public EventType EventType => EventType.ItemStat; + + public readonly Dictionary Stats; + + public SetItemStatEventData(Dictionary stats) + { + Stats = stats; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs index 59ee01824..478b231fa 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs @@ -47,8 +47,8 @@ namespace Barotrauma CopyCondition = element.GetAttributeBool("copycondition", false); Commonness = element.GetAttributeFloat("commonness", 1.0f); RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor", - element.Parent?.GetAttributeStringArray("requireddeconstructor", new string[0]) ?? new string[0]); - RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", new string[0]); + element.Parent?.GetAttributeStringArray("requireddeconstructor", Array.Empty()) ?? Array.Empty()); + RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", Array.Empty()); ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty); InfoText = element.GetAttributeString("infotext", string.Empty); InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty); @@ -102,12 +102,13 @@ namespace Barotrauma { public readonly Identifier ItemPrefabIdentifier; - public ItemPrefab ItemPrefab => ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab - : MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab ?? throw new Exception($"No ItemPrefab with identifier or name \"{ItemPrefabIdentifier}\""); + public ItemPrefab ItemPrefab => + ItemPrefab.Prefabs.TryGet(ItemPrefabIdentifier, out var prefab) ? prefab + : MapEntityPrefab.FindByName(ItemPrefabIdentifier.Value) as ItemPrefab; public override UInt32 UintIdentifier { get; } - public override IEnumerable ItemPrefabs => ItemPrefab.ToEnumerable(); + public override IEnumerable ItemPrefabs => ItemPrefab == null ? Enumerable.Empty() : ItemPrefab.ToEnumerable(); public override ItemPrefab FirstMatchingPrefab => ItemPrefab; @@ -122,6 +123,11 @@ namespace Barotrauma using MD5 md5 = MD5.Create(); UintIdentifier = ToolBox.IdentifierToUint32Hash(itemPrefab, md5); } + + public override string ToString() + { + return $"{base.ToString()} ({ItemPrefabIdentifier})"; + } } public class RequiredItemByTag : RequiredItem @@ -146,6 +152,11 @@ namespace Barotrauma using MD5 md5 = MD5.Create(); UintIdentifier = ToolBox.IdentifierToUint32Hash(tag, md5); } + + public override string ToString() + { + return $"{base.ToString()} ({Tag})"; + } } public readonly Identifier TargetItemPrefabIdentifier; @@ -390,6 +401,8 @@ namespace Barotrauma { public static readonly PrefabCollection Prefabs = new PrefabCollection(); + public const float DefaultInteractDistance = 120.0f; + //default size public Vector2 Size { get; private set; } @@ -410,7 +423,6 @@ namespace Barotrauma public ImmutableArray Triggers { get; private set; } private ImmutableDictionary treatmentSuitability; - private readonly List fabricationRecipeElements = new List(); /// /// Is this prefab overriding a prefab in another content package @@ -590,7 +602,7 @@ namespace Barotrauma public override ImmutableHashSet Aliases => aliases; //how close the Character has to be to the item to pick it up - [Serialize(120.0f, IsPropertySaveable.No)] + [Serialize(DefaultInteractDistance, IsPropertySaveable.No)] public float InteractDistance { get; private set; } // this can be used to allow items which are behind other items tp @@ -752,6 +764,9 @@ namespace Barotrauma [Serialize(false, IsPropertySaveable.No)] public bool DontTransferBetweenSubs { get; private set; } + [Serialize(true, IsPropertySaveable.No)] + public bool ShowHealthBar { get; private set; } + protected override Identifier DetermineIdentifier(XElement element) { Identifier identifier = base.DetermineIdentifier(element); @@ -1143,7 +1158,7 @@ namespace Barotrauma public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo) { priceInfo = GetPriceInfo(store); - return priceInfo != null && priceInfo.CanBeBought && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; + return priceInfo is { CanBeBought: true } && (store.Location?.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty; } public bool CanBeBoughtFrom(Location location) @@ -1240,13 +1255,12 @@ namespace Barotrauma throw new ArgumentException("Both name and identifier cannot be null."); } - ItemPrefab prefab; if (identifier.IsEmpty) { //legacy support identifier = GenerateLegacyIdentifier(name); } - Prefabs.TryGet(identifier, out prefab); + Prefabs.TryGet(identifier, out ItemPrefab prefab); //not found, see if we can find a prefab with a matching alias if (prefab == null && !string.IsNullOrEmpty(name)) @@ -1294,8 +1308,8 @@ namespace Barotrauma return PreferredContainers.Any(pc => IsItemConditionAcceptable(item, pc) && IsContainerPreferred(pc.Secondary, identifiersOrTags)); } - private bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; - private bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) => + private static bool IsItemConditionAcceptable(Item item, PreferredContainer pc) => item.ConditionPercentage >= pc.MinCondition && item.ConditionPercentage <= pc.MaxCondition; + private static bool CanBeTransferred(Identifier item, PreferredContainer pc, ItemContainer targetContainer) => pc.AllowTransfersHere && (!pc.TransferOnlyOnePerContainer || targetContainer.Inventory.AllItems.None(i => i.Prefab.Identifier == item)); public static bool IsContainerPreferred(IEnumerable preferences, ItemContainer c) => preferences.Any(id => c.Item.Prefab.Identifier == id || c.Item.HasTag(id)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs new file mode 100644 index 000000000..4ad18a238 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/ItemStatManager.cs @@ -0,0 +1,64 @@ +#nullable enable + +using System; +using System.Collections.Generic; + +namespace Barotrauma +{ + internal sealed class ItemStatManager + { + private Item item; + + public ItemStatManager(Item item) + { + this.item = item; + } + + [NetworkSerialize] + public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct + { + public override int GetHashCode() => HashCode.Combine(TalentIdentifier, CharacterID, Stat); + } + + private readonly Dictionary talentStats = new(); + + public void ApplyStat(ItemTalentStats stat, float value, CharacterTalent talent) + { + if (talent.Character?.ID is not { } characterId || + talent.Prefab?.Identifier is not { } talentIdentifier) + { + return; + } + + TalentStatIdentifier identifier = new TalentStatIdentifier(stat, talentIdentifier, characterId); + talentStats[identifier] = value; + +#if SERVER + if (GameMain.NetworkMember is { IsServer: true } server) + { + server.CreateEntityEvent(item, new Item.SetItemStatEventData(talentStats)); + } +#endif + } + + // Used for getting the value value from network packet + public void ApplyStat(TalentStatIdentifier identifier, float value) + { + talentStats[identifier] = value; + } + + public float GetAdjustedValue(ItemTalentStats stat, float originalValue) + { + float total = originalValue; + foreach (var (key, value) in talentStats) + { + if (key.Stat == stat) + { + total *= value; + } + } + + return total; + } + } +} \ No newline at end of file diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs index 04351e385..26b7d6e00 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/RelatedItem.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; +using Barotrauma.Extensions; namespace Barotrauma { @@ -21,6 +22,8 @@ namespace Barotrauma public bool MatchOnEmpty { get; set; } + public bool RequireEmpty { get; set; } + public bool IgnoreInEditor { get; set; } private ImmutableHashSet excludedIdentifiers; @@ -133,40 +136,52 @@ namespace Barotrauma if (parentItem == null) { return false; } return CheckContained(parentItem); case RelationType.Container: - if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty; } - return (!ExcludeBroken || parentItem.Container.Condition > 0.0f) && (!ExcludeFullCondition || !parentItem.Container.IsFullCondition) && MatchesItem(parentItem.Container); + if (parentItem == null || parentItem.Container == null) { return MatchOnEmpty || RequireEmpty; } + return CheckItem(parentItem.Container, this); case RelationType.Equipped: if (character == null) { return false; } - if (MatchOnEmpty && !character.HeldItems.Any()) { return true; } - foreach (Item equippedItem in character.HeldItems) + var heldItems = character.HeldItems; + if ((RequireEmpty || MatchOnEmpty) && heldItems.None()) { return true; } + foreach (Item equippedItem in heldItems) { if (equippedItem == null) { continue; } - if ((!ExcludeBroken || equippedItem.Condition > 0.0f) && (!ExcludeFullCondition || !equippedItem.IsFullCondition) && MatchesItem(equippedItem)) { return true; } + if (CheckItem(equippedItem, this)) + { + if (RequireEmpty && equippedItem.Condition > 0) { return false; } + return true; + } } break; case RelationType.Picked: - if (character == null || character.Inventory == null) { return false; } - foreach (Item pickedItem in character.Inventory.AllItems) + if (character == null) { return false; } + if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; } + var allItems = character.Inventory.AllItems; + if ((RequireEmpty || MatchOnEmpty) && allItems.None()) { return true; } + foreach (Item pickedItem in allItems) { - if (MatchesItem(pickedItem)) { return true; } + if (pickedItem == null) { continue; } + if (CheckItem(pickedItem, this)) + { + if (RequireEmpty && pickedItem.Condition > 0) { return false; } + return true; + } } break; default: return true; } + static bool CheckItem(Item i, RelatedItem ri) => (!ri.ExcludeBroken || ri.RequireEmpty || i.Condition > 0.0f) && (!ri.ExcludeFullCondition || !i.IsFullCondition) && ri.MatchesItem(i); + return false; } private bool CheckContained(Item parentItem) { if (parentItem.OwnInventory == null) { return false; } - - if (MatchOnEmpty && parentItem.OwnInventory.IsEmpty()) - { - return true; - } - + bool isEmpty = parentItem.OwnInventory.IsEmpty(); + if (RequireEmpty && !isEmpty) { return false; } + if (MatchOnEmpty && isEmpty) { return true; } foreach (Item contained in parentItem.ContainedItems) { if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; } @@ -184,6 +199,7 @@ namespace Barotrauma new XAttribute("optional", IsOptional), new XAttribute("ignoreineditor", IgnoreInEditor), new XAttribute("excludebroken", ExcludeBroken), + new XAttribute("requireempty", RequireEmpty), new XAttribute("excludefullcondition", ExcludeFullCondition), new XAttribute("targetslot", TargetSlot), new XAttribute("allowvariants", AllowVariants)); @@ -249,6 +265,7 @@ namespace Barotrauma RelatedItem ri = new RelatedItem(identifiers, excludedIdentifiers) { ExcludeBroken = element.GetAttributeBool("excludebroken", true), + RequireEmpty = element.GetAttributeBool("requireempty", false), ExcludeFullCondition = element.GetAttributeBool("excludefullcondition", false), AllowVariants = element.GetAttributeBool("allowvariants", true) }; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs index 8eabe4b03..0770ba69f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Creatures/BallastFloraBehavior.cs @@ -1026,7 +1026,7 @@ namespace Barotrauma.MapCreatures.Behavior branch.DamageVisualizationTimer = 1.0f; } - if (branch.IsRootGrowth && root != null && root.Health > 0.0f) { return; } + if (branch.IsRootGrowth && root is { Health: > 0.0f }) { return; } if (type != AttackType.Other && type != AttackType.CutFromRoot) { @@ -1035,7 +1035,7 @@ namespace Barotrauma.MapCreatures.Behavior } if (GameMain.NetworkMember != null) - { + { // damage is handled server side if (GameMain.NetworkMember.IsClient) { @@ -1059,6 +1059,11 @@ namespace Barotrauma.MapCreatures.Behavior if (type == AttackType.Fire) { + if (attacker is not null) + { + damage *= 1f + attacker.GetStatValue(StatTypes.BallastFloraDamageMultiplier); + } + if (IsInWater(branch)) { damage *= 1f - SubmergedWaterResistance; @@ -1066,7 +1071,7 @@ namespace Barotrauma.MapCreatures.Behavior if (defenseCooldown <= 0) { - if (!(StateMachine.State is DefendWithPumpState)) + if (StateMachine.State is not DefendWithPumpState) { StateMachine.EnterState(new DefendWithPumpState(branch, ClaimedTargets, attacker)); defenseCooldown = 180f; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs index 329102f51..58f800b98 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Explosion.cs @@ -1,13 +1,12 @@ -using Barotrauma.Items.Components; +using Barotrauma.Extensions; +using Barotrauma.Items.Components; +using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; -using Barotrauma.Extensions; -using Barotrauma.MapCreatures.Behavior; namespace Barotrauma { @@ -131,17 +130,23 @@ namespace Barotrauma if (damageSource is Item sourceItem) { var launcher = sourceItem.GetComponent()?.Launcher; - displayRange *= - 1.0f - + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius) + displayRange *= + 1.0f + + sourceItem.GetQualityModifier(Quality.StatType.ExplosionRadius) + (launcher?.GetQualityModifier(Quality.StatType.ExplosionRadius) ?? 0); - Attack.DamageMultiplier *= - 1.0f + Attack.DamageMultiplier *= + 1.0f + sourceItem.GetQualityModifier(Quality.StatType.ExplosionDamage) + (launcher?.GetQualityModifier(Quality.StatType.ExplosionDamage) ?? 0); Attack.SourceItem ??= sourceItem; } + if (attacker is not null) + { + displayRange *= 1f + attacker.GetStatValue(StatTypes.ExplosionRadiusMultiplier); + Attack.DamageMultiplier *= 1f + attacker.GetStatValue(StatTypes.ExplosionDamageMultiplier); + } + Vector2 cameraPos = GameMain.GameScreen.Cam.Position; float cameraDist = Vector2.Distance(cameraPos, worldPosition) / 2.0f; GameMain.GameScreen.Cam.Shake = cameraShake * Math.Max((cameraShakeRange - cameraDist) / cameraShakeRange, 0.0f); @@ -187,7 +192,7 @@ namespace Barotrauma var powerContainer = item.GetComponent(); if (powerContainer != null) { - powerContainer.Charge -= powerContainer.Capacity * EmpStrength * distFactor; + powerContainer.Charge -= powerContainer.GetCapacity() * EmpStrength * distFactor; } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs index 508032303..433b711a2 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs @@ -3015,6 +3015,7 @@ namespace Barotrauma var selectedLocation = allValidLocations.FirstOrDefault(l => Vector2.Distance(l.Edge.Point1, l.Edge.Point2) is float edgeLength && !l.Edge.OutsideLevel && + ((l.Edge.Cell1?.IsDestructible ?? false) || (l.Edge.Cell2?.IsDestructible ?? false)) && requiredAmount <= (int)Math.Floor(edgeLength / ((1.0f - maxResourceOverlap) * prefab.Size.X))); @@ -3905,7 +3906,7 @@ namespace Barotrauma private bool HasEndOutpost() { - if (preSelectedStartOutpost != null) { return true; } + if (preSelectedEndOutpost != null) { return true; } //don't create an end outpost for locations if (LevelData.Type == LevelData.LevelType.Outpost) { return false; } if (EndLocation != null && !EndLocation.Type.HasOutpost) { return false; } @@ -4323,6 +4324,10 @@ namespace Barotrauma sp = corpsePoints.FirstOrDefault(sp => sp.AssignedJob == null) ?? pathPoints.FirstOrDefault(sp => sp.AssignedJob == null); // Deduce the job from the selected prefab selectedPrefab = GetCorpsePrefab(usedJobs); + if (selectedPrefab != null) + { + job = selectedPrefab.GetJobPrefab(); + } } } if (selectedPrefab == null) { continue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs index 8e30125de..5b9d587fc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Levels/LevelObjects/LevelObjectManager.cs @@ -542,38 +542,41 @@ namespace Barotrauma GlobalForceDecreaseTimer = 0.0f; } - foreach (LevelObject obj in updateableObjects) + if (updateableObjects is not null) { - if (GameMain.NetworkMember is { IsServer: true }) + foreach (LevelObject obj in updateableObjects) { - obj.NetworkUpdateTimer -= deltaTime; - if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f) + if (GameMain.NetworkMember is { IsServer: true }) { - GameMain.NetworkMember.CreateEntityEvent(this, new EventData(obj)); - obj.NeedsNetworkSyncing = false; - obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; - } - } - if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } - - if (obj.Triggers != null) - { - obj.ActivePrefab = obj.Prefab; - for (int i = 0; i < obj.Triggers.Count; i++) - { - obj.Triggers[i].Update(deltaTime); - if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null) + obj.NetworkUpdateTimer -= deltaTime; + if (obj.NeedsNetworkSyncing && obj.NetworkUpdateTimer <= 0.0f) { - obj.ActivePrefab = obj.Prefab.OverrideProperties[i]; + GameMain.NetworkMember.CreateEntityEvent(this, new EventData(obj)); + obj.NeedsNetworkSyncing = false; + obj.NetworkUpdateTimer = NetConfig.LevelObjectUpdateInterval; } } - } + if (obj.Prefab.HideWhenBroken && obj.Health <= 0.0f) { continue; } - if (obj.PhysicsBody != null) - { - if (obj.Prefab.PhysicsBodyTriggerIndex > -1) { obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered; } - /*obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z); - obj.Rotation = -obj.PhysicsBody.Rotation;*/ + if (obj.Triggers != null) + { + obj.ActivePrefab = obj.Prefab; + for (int i = 0; i < obj.Triggers.Count; i++) + { + obj.Triggers[i].Update(deltaTime); + if (obj.Triggers[i].IsTriggered && obj.Prefab.OverrideProperties[i] != null) + { + obj.ActivePrefab = obj.Prefab.OverrideProperties[i]; + } + } + } + + if (obj.PhysicsBody != null) + { + if (obj.Prefab.PhysicsBodyTriggerIndex > -1) { obj.PhysicsBody.Enabled = obj.Triggers[obj.Prefab.PhysicsBodyTriggerIndex].IsTriggered; } + /*obj.Position = new Vector3(obj.PhysicsBody.Position, obj.Position.Z); + obj.Rotation = -obj.PhysicsBody.Rotation;*/ + } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs index cb1861ec7..80b11198b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs @@ -135,7 +135,7 @@ namespace Barotrauma foreach (var stockElement in storeElement.GetChildElements("stock")) { var identifier = stockElement.GetAttributeIdentifier("id", Identifier.Empty); - if (identifier.IsEmpty || !(ItemPrefab.FindByIdentifier(identifier) is ItemPrefab prefab)) { continue; } + if (identifier.IsEmpty || ItemPrefab.FindByIdentifier(identifier) is not ItemPrefab prefab) { continue; } int qty = stockElement.GetAttributeInt("qty", 0); if (qty < 1) { continue; } Stock.Add(new PurchasedItem(prefab, qty, buyer: null)); @@ -157,7 +157,7 @@ namespace Barotrauma foreach (var childElement in element.GetChildElements("item")) { var id = childElement.GetAttributeIdentifier("id", Identifier.Empty); - if (id.IsEmpty || !(ItemPrefab.FindByIdentifier(id) is ItemPrefab prefab)) { continue; } + if (id.IsEmpty || ItemPrefab.FindByIdentifier(id) is not ItemPrefab prefab) { continue; } specials.Add(prefab); } return specials; @@ -240,7 +240,7 @@ namespace Barotrauma availableStock.Add(stockItem.ItemPrefab, weight); } DailySpecials.Clear(); - int extraSpecialSalesCount = Location.GetExtraSpecialSalesCount(); + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); for (int i = 0; i < Location.DailySpecialsCount + extraSpecialSalesCount; i++) { if (availableStock.None()) { break; } @@ -283,6 +283,17 @@ namespace Barotrauma } // Adjust by current location reputation price *= Location.GetStoreReputationModifier(true); + + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (characters.Any()) + { + if (Location.Reputation?.Faction is { } faction && faction.IsAffiliated()) + { + price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated)); + } + price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier)); + price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag))); + } // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -303,6 +314,14 @@ namespace Barotrauma } // Adjust by current location reputation price *= Location.GetStoreReputationModifier(false); + + var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); + if (characters.Any()) + { + price *= 1f + characters.Max(static c => c.GetStatValue(StatTypes.StoreSellMultiplier)); + price *= 1f + characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreSellMultiplier, tag))); + } + // Price should never go below 1 mk return Math.Max((int)price, 1); } @@ -906,11 +925,17 @@ namespace Barotrauma public LocationType GetLocationType() { - if (IsCriticallyRadiated() && LocationType.Prefabs[Type.ReplaceInRadiation] is { } newLocationType) + if (IsCriticallyRadiated() && !Type.ReplaceInRadiation.IsEmpty) { - return newLocationType; + if (LocationType.Prefabs.TryGet(Type.ReplaceInRadiation, out LocationType newLocationType)) + { + return newLocationType; + } + else + { + DebugConsole.ThrowError($"Error when trying to get a new location type for an irradiated location - location type \"{newLocationType}\" not found."); + } } - return Type; } @@ -1119,7 +1144,7 @@ namespace Barotrauma public void UpdateStores() { // In multiplayer, stores should be updated by the server and loaded from save data by clients - if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; } + if (GameMain.NetworkMember is { IsClient: true }) { return; } if (Stores == null) { CreateStores(); @@ -1161,13 +1186,10 @@ namespace Barotrauma stockToRemove.ForEach(i => stock.Remove(i)); store.Stock.Clear(); store.Stock.AddRange(stock); - int extraSpecialSalesCount = GetExtraSpecialSalesCount(); - if (++StepsSinceSpecialsUpdated >= SpecialsUpdateInterval || store.DailySpecials.Count() != DailySpecialsCount + extraSpecialSalesCount) - { - store.GenerateSpecials(); - } store.GeneratePriceModifier(); } + + StepsSinceSpecialsUpdated++; foreach (var identifier in storesToRemove) { Stores.Remove(identifier); @@ -1178,6 +1200,20 @@ namespace Barotrauma } } + public void UpdateSpecials() + { + if (GameMain.NetworkMember is { IsClient: true } || Stores is null) { return; } + + int extraSpecialSalesCount = GetExtraSpecialSalesCount(); + + foreach (StoreInfo store in Stores.Values) + { + if (StepsSinceSpecialsUpdated < SpecialsUpdateInterval && store.DailySpecials.Count == DailySpecialsCount + extraSpecialSalesCount) { continue; } + + store.GenerateSpecials(); + } + } + private void UpdateStoreIdentifiers() { StoreIdentifiers.Clear(); @@ -1249,7 +1285,7 @@ namespace Barotrauma } } - public int GetExtraSpecialSalesCount() + public static int GetExtraSpecialSalesCount() { var characters = GameSession.GetSessionCrewCharacters(CharacterType.Both); if (!characters.Any()) { return 0; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs index e402b8073..97651462a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/LocationType.cs @@ -65,7 +65,7 @@ namespace Barotrauma private set; } - public string ReplaceInRadiation { get; } + public Identifier ReplaceInRadiation { get; } public Sprite Sprite { get; private set; } public Sprite RadiationSprite { get; } @@ -108,7 +108,7 @@ namespace Barotrauma HideEntitySubcategories = element.GetAttributeStringArray("hideentitysubcategories", Array.Empty()).ToList(); - ReplaceInRadiation = element.GetAttributeString(nameof(ReplaceInRadiation).ToLower(), ""); + ReplaceInRadiation = element.GetAttributeIdentifier(nameof(ReplaceInRadiation), Identifier.Empty); string teamStr = element.GetAttributeString("outpostteam", "FriendlyNPC"); Enum.TryParse(teamStr, out OutpostTeam); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs index ff196b2d6..3da878131 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs @@ -560,6 +560,42 @@ namespace Barotrauma } } + //make sure the location at the right side of the gate between biomes isn't a dead-end + //those may sometimes generate if all the connections of the right-side location lead to the previous biome + //(i.e. a situation where the adjacent locations happen to be at the left side of the border of the biomes, see see Regalis11/Barotrauma#10047) + for (int i = 0; i < Connections.Count; i++) + { + var connection = Connections[i]; + if (!connection.Locked) { continue; } + var rightMostLocation = + connection.Locations[0].MapPosition.X > connection.Locations[1].MapPosition.X ? + connection.Locations[0] : + connection.Locations[1]; + + //if there's only one connection (= the connection between biomes), create a new connection to the closest location to the right + if (rightMostLocation.Connections.Count == 1) + { + Location closestLocation = null; + float closestDist = float.PositiveInfinity; + foreach (Location otherLocation in Locations) + { + if (otherLocation == rightMostLocation || otherLocation.MapPosition.X < rightMostLocation.MapPosition.X) { continue; } + float dist = Vector2.DistanceSquared(rightMostLocation.MapPosition, otherLocation.MapPosition); + if (dist < closestDist || closestLocation == null) + { + closestLocation = otherLocation; + closestDist = dist; + } + } + + var newConnection = new LocationConnection(rightMostLocation, closestLocation); + rightMostLocation.Connections.Add(newConnection); + closestLocation.Connections.Add(newConnection); + Connections.Add(newConnection); + GenerateLocationConnectionVisuals(newConnection); + } + } + //remove orphans Locations.RemoveAll(l => !Connections.Any(c => c.Locations.Contains(l))); @@ -606,7 +642,9 @@ namespace Barotrauma } } - partial void GenerateLocationConnectionVisuals(); + partial void GenerateAllLocationConnectionVisuals(); + + partial void GenerateLocationConnectionVisuals(LocationConnection connection); private int GetZoneIndex(float xPos) { @@ -944,6 +982,16 @@ namespace Barotrauma ProgressWorld(); } + // always update specials every step + for (int i = 0; i < Math.Max(1, steps); i++) + { + foreach (Location location in Locations) + { + if (!location.Discovered) { continue; } + location.UpdateSpecials(); + } + } + Radiation?.OnStep(steps); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs index 96a8e9028..38cf694b1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Map/PriceInfo.cs @@ -27,6 +27,8 @@ namespace Barotrauma public bool DisplayNonEmpty { get; } = false; public Identifier StoreIdentifier { get; } + public bool RequiresUnlock { get; } + /// /// Used when both and are set to 0. /// @@ -48,6 +50,7 @@ namespace Barotrauma int maxAmount = GetMaxAmount(element); maxAmount = Math.Min(maxAmount, CargoManager.MaxQuantity); MaxAvailableAmount = Math.Max(maxAmount, MinAvailableAmount); + RequiresUnlock = element.GetAttributeBool("requiresunlock", false); } public PriceInfo(int price, bool canBeBought, diff --git a/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs index 4885dd27a..f28300c70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/NetStructBitField.cs @@ -8,23 +8,7 @@ using Lidgren.Network; namespace Barotrauma { - interface IWritableBitField - { - public void WriteBoolean(bool b); - public void WriteInteger(int value, int min, int max); - public void WriteFloat(float value, float min, float max, int numberOfBits); - - public void WriteToMessage(IWriteMessage msg); - } - - interface IReadableBitField - { - public bool ReadBoolean(); - public int ReadInteger(int min, int max); - public float ReadFloat(float min, float max, int numberOfBits); - } - - sealed class WriteOnlyBitField : IWritableBitField, IDisposable + sealed class WriteOnlyBitField : IDisposable { private const int AmountOfBoolsInByte = 7; // Reserve last bit for end marker private readonly List Buffer = new List(); @@ -100,7 +84,7 @@ namespace Barotrauma } } - sealed class ReadOnlyBitField : IReadableBitField + sealed class ReadOnlyBitField { private const int AmountOfBoolsInByte = 7; // Reserve last bit for end marker private readonly ImmutableArray buffer; @@ -110,17 +94,14 @@ namespace Barotrauma { List bytes = new List(); byte currentByte; - int reads = 0; do { + if (inc.BitPosition >= inc.LengthBits) + { + throw new Exception("Failed to find the end of the bit field: end of the message reached."); + } currentByte = inc.ReadByte(); bytes.Add(currentByte); - - reads++; - if (reads > 100) - { - throw new Exception($"Failed to find the end of the bit field after 100 reads. Terminating to prevent the game from freezing."); - } } while (!IsBitSet(currentByte, AmountOfBoolsInByte)); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs index 8cb93bbb2..cd1b6ad8a 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/ChildServerRelay.cs @@ -113,6 +113,11 @@ namespace Barotrauma.Networking break; } } + catch (AggregateException aggregateException) + { + if (aggregateException.InnerException is OperationCanceledException) { return -1; } + throw; + } catch (OperationCanceledException) { return -1; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs index b8e4460b7..e3f946325 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/INetSerializableStruct.cs @@ -61,9 +61,9 @@ namespace Barotrauma { public interface IReadWriteBehavior { - public delegate object? ReadDelegate(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField); + public delegate object? ReadDelegate(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField); - public delegate void WriteDelegate(object? obj, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField); + public delegate void WriteDelegate(object? obj, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField); public ReadDelegate ReadAction { get; } public WriteDelegate WriteAction { get; } @@ -71,9 +71,9 @@ namespace Barotrauma public readonly struct ReadWriteBehavior : IReadWriteBehavior { - public delegate T ReadDelegate(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField); + public delegate T ReadDelegate(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField); - public delegate void WriteDelegate(T obj, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField); + public delegate void WriteDelegate(T obj, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField); public IReadWriteBehavior.ReadDelegate ReadAction { get; } public IReadWriteBehavior.WriteDelegate WriteAction { get; } @@ -256,18 +256,18 @@ namespace Barotrauma ReadImmutableArray, WriteImmutableArray); - private static ImmutableArray ReadImmutableArray(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : notnull + private static ImmutableArray ReadImmutableArray(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : notnull { return ReadArray(inc, attribute, bitField).ToImmutableArray(); } - private static void WriteImmutableArray(ImmutableArray array, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull + private static void WriteImmutableArray(ImmutableArray array, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : notnull { ToolBox.ThrowIfNull(array); WriteIReadOnlyCollection(array, attribute, msg, bitField); } - private static T[] ReadArray(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : notnull + private static T[] ReadArray(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : notnull { int length = bitField.ReadInteger(0, attribute.ArrayMaxSize); @@ -286,13 +286,13 @@ namespace Barotrauma return array; } - private static void WriteArray(T[] array, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull + private static void WriteArray(T[] array, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : notnull { ToolBox.ThrowIfNull(array); WriteIReadOnlyCollection(array, attribute, msg, bitField); } - private static void WriteIReadOnlyCollection(IReadOnlyCollection array, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull + private static void WriteIReadOnlyCollection(IReadOnlyCollection array, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : notnull { bitField.WriteInteger(array.Count, 0, attribute.ArrayMaxSize); @@ -307,18 +307,18 @@ namespace Barotrauma } } - private static T ReadINetSerializableStruct(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : INetSerializableStruct + private static T ReadINetSerializableStruct(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : INetSerializableStruct { return INetSerializableStruct.ReadInternal(inc, bitField); } - private static void WriteINetSerializableStruct(T serializableStruct, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : INetSerializableStruct + private static void WriteINetSerializableStruct(T serializableStruct, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : INetSerializableStruct { ToolBox.ThrowIfNull(serializableStruct); serializableStruct.WriteInternal(msg, bitField); } - private static T ReadEnum(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : Enum + private static T ReadEnum(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : Enum { var type = typeof(T); @@ -338,7 +338,7 @@ namespace Barotrauma throw new InvalidOperationException($"An enum {type} with value {enumIndex} could not be found in {nameof(ReadEnum)}"); } - private static void WriteEnum(T value, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : Enum + private static void WriteEnum(T value, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : Enum { ToolBox.ThrowIfNull(value); @@ -346,7 +346,7 @@ namespace Barotrauma bitField.WriteInteger((int)Convert.ChangeType(value, value.GetTypeCode()), range.Start, range.End); } - private static T? ReadNullable(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : struct => + private static T? ReadNullable(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : struct => ReadOption(inc, attribute, bitField) switch { Some { Value: var value } => value, @@ -354,10 +354,10 @@ namespace Barotrauma _ => throw new ArgumentOutOfRangeException() }; - private static void WriteNullable(T? value, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : struct => + private static void WriteNullable(T? value, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : struct => WriteOption(value.HasValue ? Option.Some(value.Value) : Option.None(), attribute, msg, bitField); - private static Option ReadOption(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) where T : notnull + private static Option ReadOption(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) where T : notnull { bool hasValue = bitField.ReadBoolean(); if (!hasValue) @@ -373,7 +373,7 @@ namespace Barotrauma throw new InvalidOperationException($"Could not find suitable behavior for type {typeof(T)} in {nameof(ReadOption)}"); } - private static void WriteOption(Option option, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) where T : notnull + private static void WriteOption(Option option, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) where T : notnull { ToolBox.ThrowIfNull(option); @@ -391,22 +391,22 @@ namespace Barotrauma } } - private static bool ReadBoolean(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => bitField.ReadBoolean(); - private static void WriteBoolean(bool b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { bitField.WriteBoolean(b); } + private static bool ReadBoolean(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => bitField.ReadBoolean(); + private static void WriteBoolean(bool b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { bitField.WriteBoolean(b); } - private static byte ReadByte(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadByte(); - private static void WriteByte(byte b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteByte(b); } + private static byte ReadByte(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadByte(); + private static void WriteByte(byte b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteByte(b); } - private static ushort ReadUInt16(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadUInt16(); - private static void WriteUInt16(ushort b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteUInt16(b); } + private static ushort ReadUInt16(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadUInt16(); + private static void WriteUInt16(ushort b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteUInt16(b); } - private static short ReadInt16(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadInt16(); - private static void WriteInt16(short b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteInt16(b); } + private static short ReadInt16(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadInt16(); + private static void WriteInt16(short b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteInt16(b); } - private static uint ReadUInt32(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadUInt32(); - private static void WriteUInt32(uint b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteUInt32(b); } + private static uint ReadUInt32(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadUInt32(); + private static void WriteUInt32(uint b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteUInt32(b); } - private static int ReadInt32(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) + private static int ReadInt32(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) { if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt)) { @@ -416,7 +416,7 @@ namespace Barotrauma return inc.ReadInt32(); } - private static void WriteInt32(int i, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) + private static void WriteInt32(int i, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { ToolBox.ThrowIfNull(i); @@ -429,13 +429,13 @@ namespace Barotrauma msg.WriteInt32(i); } - private static ulong ReadUInt64(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadUInt64(); - private static void WriteUInt64(ulong b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteUInt64(b); } + private static ulong ReadUInt64(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadUInt64(); + private static void WriteUInt64(ulong b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteUInt64(b); } - private static long ReadInt64(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadInt64(); - private static void WriteInt64(long b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteInt64(b); } + private static long ReadInt64(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadInt64(); + private static void WriteInt64(long b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteInt64(b); } - private static float ReadSingle(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) + private static float ReadSingle(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) { if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat)) { @@ -445,7 +445,7 @@ namespace Barotrauma return inc.ReadSingle(); } - private static void WriteSingle(float f, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) + private static void WriteSingle(float f, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { ToolBox.ThrowIfNull(f); @@ -458,16 +458,16 @@ namespace Barotrauma msg.WriteSingle(f); } - private static double ReadDouble(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadDouble(); - private static void WriteDouble(double b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteDouble(b); } + private static double ReadDouble(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadDouble(); + private static void WriteDouble(double b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteDouble(b); } - private static string ReadString(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadString(); - private static void WriteString(string b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteString(b); } + private static string ReadString(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadString(); + private static void WriteString(string b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteString(b); } - private static Identifier ReadIdentifier(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => inc.ReadIdentifier(); - private static void WriteIdentifier(Identifier b, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) { msg.WriteIdentifier(b); } + private static Identifier ReadIdentifier(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => inc.ReadIdentifier(); + private static void WriteIdentifier(Identifier b, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteIdentifier(b); } - private static AccountId ReadAccountId(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) + private static AccountId ReadAccountId(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) { string str = inc.ReadString(); return AccountId.Parse(str).TryUnwrap(out var accountId) @@ -475,14 +475,14 @@ namespace Barotrauma : throw new InvalidCastException($"Could not parse \"{str}\" as an {nameof(AccountId)}"); } - private static void WriteAccountId(AccountId accountId, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) + private static void WriteAccountId(AccountId accountId, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { msg.WriteString(accountId.StringRepresentation); } - private static Color ReadColor(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); + private static Color ReadColor(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8(); - private static void WriteColor(Color color, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) + private static void WriteColor(Color color, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { ToolBox.ThrowIfNull(color); @@ -495,7 +495,7 @@ namespace Barotrauma msg.WriteColorR8G8B8(color); } - private static Vector2 ReadVector2(IReadMessage inc, NetworkSerialize attribute, IReadableBitField bitField) + private static Vector2 ReadVector2(IReadMessage inc, NetworkSerialize attribute, ReadOnlyBitField bitField) { float x = ReadSingle(inc, attribute, bitField); float y = ReadSingle(inc, attribute, bitField); @@ -503,7 +503,7 @@ namespace Barotrauma return new Vector2(x, y); } - private static void WriteVector2(Vector2 vector2, NetworkSerialize attribute, IWriteMessage msg, IWritableBitField bitField) + private static void WriteVector2(Vector2 vector2, NetworkSerialize attribute, IWriteMessage msg, WriteOnlyBitField bitField) { ToolBox.ThrowIfNull(vector2); @@ -690,11 +690,11 @@ namespace Barotrauma /// A new struct of type T with fields and properties deserialized public static T Read(IReadMessage inc) where T : INetSerializableStruct { - IReadableBitField bitField = new ReadOnlyBitField(inc); + ReadOnlyBitField bitField = new ReadOnlyBitField(inc); return ReadInternal(inc, bitField); } - public static T ReadInternal(IReadMessage inc, IReadableBitField bitField) where T : INetSerializableStruct + public static T ReadInternal(IReadMessage inc, ReadOnlyBitField bitField) where T : INetSerializableStruct { object? newObject = Activator.CreateInstance(typeof(T)); if (newObject is null) { return default!; } @@ -744,14 +744,14 @@ namespace Barotrauma /// Outgoing network message public void Write(IWriteMessage msg) { - IWritableBitField bitField = new WriteOnlyBitField(); + WriteOnlyBitField bitField = new WriteOnlyBitField(); IWriteMessage structWriteMsg = new WriteOnlyMessage(); WriteInternal(structWriteMsg, bitField); bitField.WriteToMessage(msg); msg.WriteBytes(structWriteMsg.Buffer, 0, structWriteMsg.LengthBytes); } - public void WriteInternal(IWriteMessage msg, IWritableBitField bitField) + public void WriteInternal(IWriteMessage msg, WriteOnlyBitField bitField) { var properties = NetSerializableProperties.GetPropertiesAndFields(GetType()); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs index 5fadd7644..eaea9b556 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Address/LidgrenAddress.cs @@ -40,6 +40,25 @@ namespace Barotrauma.Networking return Option.None(); } + public static Option ParseHostName(string endpointStr) + { + try + { + var resolvedAddresses = Dns.GetHostAddresses(endpointStr); + return resolvedAddresses.Any() + ? Option.Some(new LidgrenAddress(resolvedAddresses.First())) + : Option.None(); + } + catch (SocketException) + { + return Option.None(); + } + catch (ArgumentOutOfRangeException) + { + return Option.None(); + } + } + public override bool Equals(object? obj) => obj switch { diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs index 44d20264a..8e11264c6 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Endpoint/LidgrenEndpoint.cs @@ -23,6 +23,11 @@ namespace Barotrauma.Networking } public new static Option Parse(string endpointStr) + { + return ParseFromWithHostNameCheck(endpointStr, tryParseHostName: false); + } + + public static Option ParseFromWithHostNameCheck(string endpointStr, bool tryParseHostName) { string hostName = endpointStr; int port = NetConfig.DefaultPort; @@ -33,7 +38,8 @@ namespace Barotrauma.Networking port = int.TryParse(split[1], out var tmpPort) ? tmpPort : port; } - if (LidgrenAddress.Parse(hostName).TryUnwrap(out var adr)) + if (LidgrenAddress.Parse(hostName).TryUnwrap(out var adr) || + (tryParseHostName && LidgrenAddress.ParseHostName(hostName).TryUnwrap(out adr))) { return Option.Some(new LidgrenEndpoint(adr.NetAddress, port)); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs index d4330e11e..87e0463a9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/Message/Message.cs @@ -37,7 +37,12 @@ namespace Barotrauma.Networking internal static class MsgWriter { - internal static void Write(ref byte[] buf, ref int bitPos, bool val) + internal static void UpdateBitLength(ref int bitLength, int bitPos) + { + bitLength = Math.Max(bitLength, bitPos); + } + + internal static void WriteBoolean(ref byte[] buf, ref int bitPos, ref int bitLength, bool val) { #if DEBUG int resetPos = bitPos; @@ -52,7 +57,7 @@ namespace Barotrauma.Networking buf[bytePos] &= bitMask; if (val) buf[bytePos] |= bitFlag; bitPos++; - + UpdateBitLength(ref bitLength, bitPos); #if DEBUG bool testVal = MsgReader.ReadBoolean(buf, ref resetPos); if (testVal != val || resetPos != bitPos) @@ -62,63 +67,71 @@ namespace Barotrauma.Networking #endif } - internal static void WritePadBits(ref byte[] buf, ref int bitPos) + internal static void WritePadBits(ref byte[] buf, ref int bitPos, ref int bitLength) { int bitOffset = bitPos % 8; bitPos += ((8 - bitOffset) % 8); + UpdateBitLength(ref bitLength, bitPos); EnsureBufferSize(ref buf, bitPos); } - internal static void Write(ref byte[] buf, ref int bitPos, byte val) + internal static void WriteByte(ref byte[] buf, ref int bitPos, ref int bitLength, byte val) { EnsureBufferSize(ref buf, bitPos + 8); NetBitWriter.WriteByte(val, 8, buf, bitPos); bitPos += 8; + UpdateBitLength(ref bitLength, bitPos); } - internal static void Write(ref byte[] buf, ref int bitPos, UInt16 val) + internal static void WriteUInt16(ref byte[] buf, ref int bitPos, ref int bitLength, UInt16 val) { EnsureBufferSize(ref buf, bitPos + 16); NetBitWriter.WriteUInt16(val, 16, buf, bitPos); bitPos += 16; + UpdateBitLength(ref bitLength, bitPos); } - internal static void Write(ref byte[] buf, ref int bitPos, Int16 val) + internal static void WriteInt16(ref byte[] buf, ref int bitPos, ref int bitLength, Int16 val) { EnsureBufferSize(ref buf, bitPos + 16); NetBitWriter.WriteUInt16((UInt16)val, 16, buf, bitPos); bitPos += 16; + UpdateBitLength(ref bitLength, bitPos); } - internal static void Write(ref byte[] buf, ref int bitPos, UInt32 val) + internal static void WriteUInt32(ref byte[] buf, ref int bitPos, ref int bitLength, UInt32 val) { EnsureBufferSize(ref buf, bitPos + 32); NetBitWriter.WriteUInt32(val, 32, buf, bitPos); bitPos += 32; + UpdateBitLength(ref bitLength, bitPos); } - internal static void Write(ref byte[] buf, ref int bitPos, Int32 val) + internal static void WriteInt32(ref byte[] buf, ref int bitPos, ref int bitLength, Int32 val) { EnsureBufferSize(ref buf, bitPos + 32); NetBitWriter.WriteUInt32((UInt32)val, 32, buf, bitPos); bitPos += 32; + UpdateBitLength(ref bitLength, bitPos); } - internal static void Write(ref byte[] buf, ref int bitPos, UInt64 val) + internal static void WriteUInt64(ref byte[] buf, ref int bitPos, ref int bitLength, UInt64 val) { EnsureBufferSize(ref buf, bitPos + 64); NetBitWriter.WriteUInt64(val, 64, buf, bitPos); bitPos += 64; + UpdateBitLength(ref bitLength, bitPos); } - internal static void Write(ref byte[] buf, ref int bitPos, Int64 val) + internal static void WriteInt64(ref byte[] buf, ref int bitPos, ref int bitLength, Int64 val) { EnsureBufferSize(ref buf, bitPos + 64); NetBitWriter.WriteUInt64((UInt64)val, 64, buf, bitPos); bitPos += 64; + UpdateBitLength(ref bitLength, bitPos); } - internal static void Write(ref byte[] buf, ref int bitPos, Single val) + internal static void WriteSingle(ref byte[] buf, ref int bitPos, ref int bitLength, Single val) { // Use union to avoid BitConverter.GetBytes() which allocates memory on the heap SingleUIntUnion su; @@ -129,61 +142,62 @@ namespace Barotrauma.Networking NetBitWriter.WriteUInt32(su.UIntValue, 32, buf, bitPos); bitPos += 32; + UpdateBitLength(ref bitLength, bitPos); } - internal static void Write(ref byte[] buf, ref int bitPos, Double val) + internal static void WriteDouble(ref byte[] buf, ref int bitPos, ref int bitLength, Double val) { EnsureBufferSize(ref buf, bitPos + 64); byte[] bytes = BitConverter.GetBytes(val); - WriteBytes(ref buf, ref bitPos, bytes, 0, 8); + WriteBytes(ref buf, ref bitPos, ref bitLength, bytes, 0, 8); } - internal static void WriteColorR8G8B8(ref byte[] buf, ref int bitPos, Color val) + internal static void WriteColorR8G8B8(ref byte[] buf, ref int bitPos, ref int bitLength, Color val) { EnsureBufferSize(ref buf, bitPos + 24); - Write(ref buf, ref bitPos, val.R); - Write(ref buf, ref bitPos, val.G); - Write(ref buf, ref bitPos, val.B); + WriteByte(ref buf, ref bitPos, ref bitLength, val.R); + WriteByte(ref buf, ref bitPos, ref bitLength, val.G); + WriteByte(ref buf, ref bitPos, ref bitLength, val.B); } - internal static void WriteColorR8G8B8A8(ref byte[] buf, ref int bitPos, Color val) + internal static void WriteColorR8G8B8A8(ref byte[] buf, ref int bitPos, ref int bitLength, Color val) { EnsureBufferSize(ref buf, bitPos + 32); - Write(ref buf, ref bitPos, val.R); - Write(ref buf, ref bitPos, val.G); - Write(ref buf, ref bitPos, val.B); - Write(ref buf, ref bitPos, val.A); + WriteByte(ref buf, ref bitPos, ref bitLength, val.R); + WriteByte(ref buf, ref bitPos, ref bitLength, val.G); + WriteByte(ref buf, ref bitPos, ref bitLength, val.B); + WriteByte(ref buf, ref bitPos, ref bitLength, val.A); } - internal static void Write(ref byte[] buf, ref int bitPos, string val) + internal static void WriteString(ref byte[] buf, ref int bitPos, ref int bitLength, string val) { if (string.IsNullOrEmpty(val)) { - WriteVariableUInt32(ref buf, ref bitPos, 0u); + WriteVariableUInt32(ref buf, ref bitPos, ref bitLength, 0u); return; } byte[] bytes = Encoding.UTF8.GetBytes(val); - WriteVariableUInt32(ref buf, ref bitPos, (uint)bytes.Length); - WriteBytes(ref buf, ref bitPos, bytes, 0, bytes.Length); + WriteVariableUInt32(ref buf, ref bitPos, ref bitLength, (uint)bytes.Length); + WriteBytes(ref buf, ref bitPos, ref bitLength, bytes, 0, bytes.Length); } - internal static void WriteVariableUInt32(ref byte[] buf, ref int bitPos, uint value) + internal static void WriteVariableUInt32(ref byte[] buf, ref int bitPos, ref int bitLength, uint value) { uint remainingValue = value; while (remainingValue >= 0x80) { - Write(ref buf, ref bitPos, (byte)(remainingValue | 0x80)); + WriteByte(ref buf, ref bitPos, ref bitLength, (byte)(remainingValue | 0x80)); remainingValue >>= 7; } - Write(ref buf, ref bitPos, (byte)remainingValue); + WriteByte(ref buf, ref bitPos, ref bitLength, (byte)remainingValue); } - internal static void WriteRangedInteger(ref byte[] buf, ref int bitPos, int val, int min, int max) + internal static void WriteRangedInteger(ref byte[] buf, ref int bitPos, ref int bitLength, int val, int min, int max) { uint range = (uint)(max - min); int numberOfBits = NetUtility.BitsToHoldUInt(range); @@ -193,9 +207,10 @@ namespace Barotrauma.Networking uint rvalue = (uint)(val - min); NetBitWriter.WriteUInt32(rvalue, numberOfBits, buf, bitPos); bitPos += numberOfBits; + UpdateBitLength(ref bitLength, bitPos); } - internal static void WriteRangedSingle(ref byte[] buf, ref int bitPos, Single val, Single min, Single max, int numberOfBits) + internal static void WriteRangedSingle(ref byte[] buf, ref int bitPos, ref int bitLength, Single val, Single min, Single max, int numberOfBits) { float range = max - min; float unit = ((val - min) / range); @@ -205,13 +220,15 @@ namespace Barotrauma.Networking NetBitWriter.WriteUInt32((UInt32)(maxVal * unit), numberOfBits, buf, bitPos); bitPos += numberOfBits; + UpdateBitLength(ref bitLength, bitPos); } - internal static void WriteBytes(ref byte[] buf, ref int bitPos, byte[] val, int pos, int length) + internal static void WriteBytes(ref byte[] buf, ref int bitPos, ref int bitLength, byte[] val, int pos, int length) { EnsureBufferSize(ref buf, bitPos + length * 8); NetBitWriter.WriteBytes(val, pos, length, buf, bitPos); bitPos += length * 8; + UpdateBitLength(ref bitLength, bitPos); } internal static void EnsureBufferSize(ref byte[] buf, int numberOfBits) @@ -447,77 +464,77 @@ namespace Barotrauma.Networking public void WriteBoolean(bool val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteBoolean(ref buf, ref seekPos, ref lengthBits, val); } public void WritePadBits() { - MsgWriter.WritePadBits(ref buf, ref seekPos); + MsgWriter.WritePadBits(ref buf, ref seekPos, ref lengthBits); } public void WriteByte(byte val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteByte(ref buf, ref seekPos, ref lengthBits, val); } public void WriteUInt16(UInt16 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteUInt16(ref buf, ref seekPos, ref lengthBits, val); } public void WriteInt16(Int16 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteInt16(ref buf, ref seekPos, ref lengthBits, val); } public void WriteUInt32(UInt32 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteUInt32(ref buf, ref seekPos, ref lengthBits, val); } public void WriteInt32(Int32 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteInt32(ref buf, ref seekPos, ref lengthBits, val); } public void WriteUInt64(UInt64 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteUInt64(ref buf, ref seekPos, ref lengthBits, val); } public void WriteInt64(Int64 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteInt64(ref buf, ref seekPos, ref lengthBits, val); } public void WriteSingle(Single val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteSingle(ref buf, ref seekPos, ref lengthBits, val); } public void WriteDouble(Double val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteDouble(ref buf, ref seekPos, ref lengthBits, val); } public void WriteColorR8G8B8(Color val) { - MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, val); + MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, ref lengthBits, val); } public void WriteColorR8G8B8A8(Color val) { - MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, val); + MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, ref lengthBits, val); } public void WriteVariableUInt32(UInt32 val) { - MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); + MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, ref lengthBits, val); } public void WriteString(String val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteString(ref buf, ref seekPos, ref lengthBits, val); } public void WriteIdentifier(Identifier val) @@ -527,17 +544,17 @@ namespace Barotrauma.Networking public void WriteRangedInteger(int val, int min, int max) { - MsgWriter.WriteRangedInteger(ref buf, ref seekPos, val, min, max); + MsgWriter.WriteRangedInteger(ref buf, ref seekPos, ref lengthBits, val, min, max); } public void WriteRangedSingle(Single val, Single min, Single max, int bitCount) { - MsgWriter.WriteRangedSingle(ref buf, ref seekPos, val, min, max, bitCount); + MsgWriter.WriteRangedSingle(ref buf, ref seekPos, ref lengthBits, val, min, max, bitCount); } public void WriteBytes(byte[] val, int startPos, int length) { - MsgWriter.WriteBytes(ref buf, ref seekPos, val, startPos, length); + MsgWriter.WriteBytes(ref buf, ref seekPos, ref lengthBits, val, startPos, length); } public byte[] PrepareForSending(bool compressPastThreshold, out bool isCompressed, out int length) @@ -814,77 +831,77 @@ namespace Barotrauma.Networking public void WriteBoolean(bool val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteBoolean(ref buf, ref seekPos, ref lengthBits, val); } public void WritePadBits() { - MsgWriter.WritePadBits(ref buf, ref seekPos); + MsgWriter.WritePadBits(ref buf, ref seekPos, ref lengthBits); } public void WriteByte(byte val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteByte(ref buf, ref seekPos, ref lengthBits, val); } public void WriteUInt16(UInt16 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteUInt16(ref buf, ref seekPos, ref lengthBits, val); } public void WriteInt16(Int16 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteInt16(ref buf, ref seekPos, ref lengthBits, val); } public void WriteUInt32(UInt32 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteUInt32(ref buf, ref seekPos, ref lengthBits, val); } public void WriteInt32(Int32 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteInt32(ref buf, ref seekPos, ref lengthBits, val); } public void WriteUInt64(UInt64 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteUInt64(ref buf, ref seekPos, ref lengthBits, val); } public void WriteInt64(Int64 val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteInt64(ref buf, ref seekPos, ref lengthBits, val); } public void WriteSingle(Single val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteSingle(ref buf, ref seekPos, ref lengthBits, val); } public void WriteDouble(Double val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteDouble(ref buf, ref seekPos, ref lengthBits, val); } public void WriteColorR8G8B8(Color val) { - MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, val); + MsgWriter.WriteColorR8G8B8(ref buf, ref seekPos, ref lengthBits, val); } public void WriteColorR8G8B8A8(Color val) { - MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, val); + MsgWriter.WriteColorR8G8B8A8(ref buf, ref seekPos, ref lengthBits, val); } public void WriteVariableUInt32(UInt32 val) { - MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, val); + MsgWriter.WriteVariableUInt32(ref buf, ref seekPos, ref lengthBits, val); } public void WriteString(String val) { - MsgWriter.Write(ref buf, ref seekPos, val); + MsgWriter.WriteString(ref buf, ref seekPos, ref lengthBits, val); } public void WriteIdentifier(Identifier val) @@ -894,17 +911,17 @@ namespace Barotrauma.Networking public void WriteRangedInteger(int val, int min, int max) { - MsgWriter.WriteRangedInteger(ref buf, ref seekPos, val, min, max); + MsgWriter.WriteRangedInteger(ref buf, ref seekPos, ref lengthBits, val, min, max); } public void WriteRangedSingle(Single val, Single min, Single max, int bitCount) { - MsgWriter.WriteRangedSingle(ref buf, ref seekPos, val, min, max, bitCount); + MsgWriter.WriteRangedSingle(ref buf, ref seekPos, ref lengthBits, val, min, max, bitCount); } public void WriteBytes(byte[] val, int startPos, int length) { - MsgWriter.WriteBytes(ref buf, ref seekPos, val, startPos, length); + MsgWriter.WriteBytes(ref buf, ref seekPos, ref lengthBits, val, startPos, length); } public bool ReadBoolean() diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs index 0a042c27f..2ef9e10ec 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/Primitives/NetworkPeerStructs.cs @@ -286,8 +286,13 @@ namespace Barotrauma.Networking } public DateTime InstallTime => cachedDateTime ??= DateTime.UtcNow + TimeSpan.FromSeconds(InstallTimeDiffInSeconds); - public RegularPackage? RegularPackage => ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Hash.Equals(Hash)); - public CorePackage? CorePackage => ContentPackageManager.CorePackages.FirstOrDefault(p => p.Hash.Equals(Hash)); + public RegularPackage? RegularPackage => + ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name.Equals(Name) && p.Hash.Equals(Hash)) ?? + ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Hash.Equals(Hash)); + + public CorePackage? CorePackage => + ContentPackageManager.CorePackages.FirstOrDefault(p => p.Name.Equals(Name) && p.Hash.Equals(Hash)) ?? + ContentPackageManager.CorePackages.FirstOrDefault(p => p.Hash.Equals(Hash)); public ContentPackage? ContentPackage => (ContentPackage?)RegularPackage ?? CorePackage; public ServerContentPackage() { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs index 6f68b3730..15a041953 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Networking/RespawnManager.cs @@ -258,7 +258,7 @@ namespace Barotrauma.Networking var powerContainer = item.GetComponent(); if (powerContainer != null) { - powerContainer.Charge = powerContainer.Capacity; + powerContainer.Charge = powerContainer.GetCapacity(); } var door = item.GetComponent(); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs index ee9831e4d..8286eee68 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Physics/PhysicsBody.cs @@ -97,7 +97,6 @@ namespace Barotrauma get { return list; } } - protected Vector2 prevPosition; protected float prevRotation; @@ -344,6 +343,22 @@ namespace Barotrauma } } + /// + /// Ignore rotation calls for the rest of this and the next update. Automatically disabled after that. Used for temporarily suppressing the SmoothRotate calls to prevent conflicting or unitentionally amplified rotations. + /// + public bool SuppressSmoothRotationCalls + { + get => _suppressSmoothRotationCalls; + set + { + _suppressSmoothRotationCalls = value; + smoothRotationSuppressionCounter = 0; + } + } + + private bool _suppressSmoothRotationCalls; + private int smoothRotationSuppressionCounter; + public PhysicsBody(XElement element, float scale = 1.0f, bool findNewContacts = true) : this(element, Vector2.Zero, scale, findNewContacts: findNewContacts) { } public PhysicsBody(ColliderParams cParams, bool findNewContacts = true) : this(cParams, Vector2.Zero, findNewContacts) { } public PhysicsBody(LimbParams lParams, bool findNewContacts = true) : this(lParams, Vector2.Zero, findNewContacts) { } @@ -831,6 +846,17 @@ namespace Barotrauma } drawOffset = NetConfig.InterpolateSimPositionError(drawOffset, PositionSmoothingFactor); rotationOffset = NetConfig.InterpolateRotationError(rotationOffset); + if (SuppressSmoothRotationCalls) + { + if (smoothRotationSuppressionCounter > 0) + { + SuppressSmoothRotationCalls = false; + } + else + { + smoothRotationSuppressionCounter++; + } + } } public void UpdateDrawPosition() @@ -873,6 +899,7 @@ namespace Barotrauma /// Should the angles be wrapped. Set to false if it makes a difference whether the angle of the body is 0.0f or 360.0f. public void SmoothRotate(float targetRotation, float force = 10.0f, bool wrapAngle = true) { + if (SuppressSmoothRotationCalls) { return; } float nextAngle = FarseerBody.Rotation + FarseerBody.AngularVelocity * (float)Timing.Step; float angle = wrapAngle ? MathUtils.GetShortestAngle(nextAngle, targetRotation) : @@ -881,7 +908,7 @@ namespace Barotrauma if (FarseerBody.BodyType == BodyType.Kinematic) { - if (!IsValidValue(torque, "torque")) return; + if (!IsValidValue(torque, "torque")) { return; } FarseerBody.AngularVelocity = torque; } else diff --git a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs index 93fa4d6a7..4b3b7edc9 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Prefabs/PrefabCollection.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography; @@ -256,7 +257,7 @@ namespace Barotrauma /// Prefab identifier /// The matching prefab (if one is found) /// Whether a prefab with the identifier exists or not - public bool TryGet(Identifier identifier, out T? result) + public bool TryGet(Identifier identifier, [NotNullWhen(true)] out T? result) { Prefab.DisallowCallFromConstructor(); if (prefabs.TryGetValue(identifier, out PrefabSelector? selector)) diff --git a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs index 2b8e658f9..226e1c3c1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Screens/NetLobbyScreen.cs @@ -65,7 +65,7 @@ namespace Barotrauma } #endif #if CLIENT - (botCountText as GUITextBlock).Text = botCount.ToString(); + botCountText.Text = botCount.ToString(); #endif } @@ -79,7 +79,13 @@ namespace Barotrauma } #endif #if CLIENT - (botSpawnModeText as GUITextBlock).Text = TextManager.Get(botSpawnMode.ToString()); + + botSpawnModeText.Text = TextManager.Get(botSpawnMode.ToString()); + botSpawnModeText.ToolTip = TextManager.Get($"botspawnmode.{botSpawnMode}.tooltip") + "\n\n" + TextManager.Get("botspawn.campaignnote"); + foreach (var btn in botSpawnModeButtons) + { + btn.ToolTip = botSpawnModeText.ToolTip; + } #endif } @@ -89,7 +95,7 @@ namespace Barotrauma if (GameMain.Server != null) GameMain.Server.ServerSettings.TraitorsEnabled = enabled; #endif #if CLIENT - (traitorProbabilityText as GUITextBlock).Text = TextManager.Get(enabled.ToString()); + traitorProbabilityText.Text = TextManager.Get(enabled.ToString()); #endif } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs index 16e5022f4..09e78f943 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Serialization/XMLExtensions.cs @@ -214,6 +214,7 @@ namespace Barotrauma return splitValue; } + public static Identifier[] GetAttributeIdentifierArray(this XElement element, string name, Identifier[] defaultValue, bool trim = true) { return element.GetAttributeStringArray(name, null, trim: trim, convertToLowerInvariant: false) @@ -221,6 +222,12 @@ namespace Barotrauma ?? defaultValue; } + public static ImmutableHashSet GetAttributeIdentifierImmutableHashSet(this XElement element, string key, ImmutableHashSet defaultValue, bool trim = true) + { + return element.GetAttributeIdentifierArray(key, null, trim)?.ToImmutableHashSet() + ?? defaultValue; + } + public static float GetAttributeFloat(this XElement element, float defaultValue, params string[] matchingAttributeName) { if (element == null) { return defaultValue; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs index 4b625e9e8..3173558d7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs @@ -1,6 +1,7 @@ using Barotrauma.Abilities; using Barotrauma.Extensions; using Barotrauma.Items.Components; +using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using System; @@ -278,6 +279,13 @@ namespace Barotrauma [Serialize(1, IsPropertySaveable.No)] public int Count { get; private set; } + /// + /// The maximum amount of creatures of the same species in the same team that are allowed to be spawned via this status effect. + /// Also the creatures spawned by other means are counted in the check. + /// + [Serialize(0, IsPropertySaveable.No)] + public int TotalMaxCount { get; private set; } + [Serialize(0, IsPropertySaveable.No)] public int Stun { get; private set; } @@ -362,7 +370,7 @@ namespace Barotrauma private readonly int useItemCount; - private readonly bool removeItem, dropContainedItems, removeCharacter, breakLimb, hideLimb; + private readonly bool removeItem, dropContainedItems, dropItem, removeCharacter, breakLimb, hideLimb; private readonly float hideLimbTimer; public readonly ActionType type = ActionType.OnActive; @@ -656,6 +664,9 @@ namespace Barotrauma case "dropcontaineditems": dropContainedItems = true; break; + case "dropitem": + dropItem = true; + break; case "removecharacter": removeCharacter = true; break; @@ -1282,6 +1293,16 @@ namespace Barotrauma } } + if (dropItem) + { + for (int i = 0; i < targets.Count; i++) + { + if (targets[i] is Item item) + { + item.Drop(dropper: null); + } + } + } if (dropContainedItems) { for (int i = 0; i < targets.Count; i++) @@ -1333,17 +1354,20 @@ namespace Barotrauma if (limb.body == sourceBody) { targetLimb = limb; - if (breakLimb) - { - character.TrySeverLimbJoints(limb, severLimbsProbability: 100, damage: 100, allowBeheading: true, attacker: user); - } break; } } } - if (hideLimb) + if (targetLimb != null) { - targetLimb?.HideAndDisable(hideLimbTimer); + if (breakLimb) + { + targetLimb.character.TrySeverLimbJoints(targetLimb, severLimbsProbability: 1, damage: -1, allowBeheading: true, attacker: user); + } + if (hideLimb) + { + targetLimb.HideAndDisable(hideLimbTimer); + } } } } @@ -1475,6 +1499,9 @@ namespace Barotrauma targetCharacter = targetLimb.character; } } + + Character entityCharacter = entity as Character; + targetCharacter ??= entityCharacter; if (targetCharacter != null && !targetCharacter.Removed && !targetCharacter.IsPlayer) { if (targetCharacter.AIController is EnemyAIController enemyAI) @@ -1482,7 +1509,13 @@ namespace Barotrauma foreach (AITrigger trigger in aiTriggers) { if (Rand.Value(Rand.RandSync.Unsynced) > trigger.Probability) { continue; } - if (target is Limb targetLimb && targetCharacter.LastDamage.HitLimb != targetLimb) { continue; } + if (entityCharacter != targetCharacter) + { + if (target is Limb targetLimb && targetCharacter.LastDamage.HitLimb is Limb hitLimb) + { + if (hitLimb != targetLimb) { continue; } + } + } if (targetCharacter.LastDamage.Damage < trigger.MinDamage) { continue; } enemyAI.LaunchTrigger(trigger); break; @@ -1606,6 +1639,14 @@ namespace Barotrauma Entity.Spawner.AddCharacterToSpawnQueue(characterSpawnInfo.SpeciesName, position + Rand.Vector(characterSpawnInfo.Spread, Rand.RandSync.Unsynced) + characterSpawnInfo.Offset, onSpawn: newCharacter => { + if (characterSpawnInfo.TotalMaxCount > 0) + { + if (Character.CharacterList.Count(c => c.SpeciesName == characterSpawnInfo.SpeciesName && c.TeamID == newCharacter.TeamID) > characterSpawnInfo.TotalMaxCount) + { + Entity.Spawner?.AddEntityToRemoveQueue(newCharacter); + return; + } + } if (newCharacter.AIController is EnemyAIController enemyAi && enemyAi.PetBehavior != null && entity is Item item && @@ -1666,11 +1707,11 @@ namespace Barotrauma Character.Controlled = newCharacter; } #elif SERVER - /*foreach (Client c in GameMain.Server.ConnectedClients) + foreach (Client c in GameMain.Server.ConnectedClients) { if (c.Character != target) { continue; } GameMain.Server.SetClientCharacter(c, newCharacter); - }*/ + } #endif } if (characterSpawnInfo.RemovePreviousCharacter) { Entity.Spawner?.AddEntityToRemoveQueue(character); } @@ -1787,10 +1828,16 @@ namespace Barotrauma rotation += spread; if (projectile != null) { - projectile.Shoot(user, - ConvertUnits.ToSimUnits(worldPos), - ConvertUnits.ToSimUnits(worldPos), - rotation, + Vector2 spawnPos; + if (projectile.Hitscan) + { + spawnPos = sourceBody != null ? sourceBody.SimPosition : entity.SimPosition; + } + else + { + spawnPos = ConvertUnits.ToSimUnits(worldPos); + } + projectile.Shoot(user, spawnPos, spawnPos, rotation, ignoredBodies: user?.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: true); } else if (newItem.body != null) @@ -2070,12 +2117,13 @@ namespace Barotrauma if (entity is Item sourceItem && sourceItem.HasTag("medical")) { multiplier *= 1 + targetCharacter.GetStatValue(StatTypes.MedicalItemEffectivenessMultiplier); - - if (user != null) + + if (user is not null) { multiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemApplyingMultiplier); } } + return multiplier * AfflictionMultiplier; } @@ -2087,6 +2135,18 @@ namespace Barotrauma afflictionMultiplier *= targetCharacter.MaxVitality / 100f; } + if (user is not null) + { + if (affliction.Prefab.IsBuff) + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.MedicalItemDurationMultiplier); + } + else if (affliction.Prefab.AfflictionType == "poison") + { + afflictionMultiplier *= 1 + user.GetStatValue(StatTypes.PoisonMultiplier); + } + } + if (!MathUtils.NearlyEqual(afflictionMultiplier, 1.0f)) { return affliction.CreateMultiplied(afflictionMultiplier, affliction.Probability); diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs index 7f27612ea..6ddc13c18 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/LocalizedString.cs @@ -13,7 +13,7 @@ namespace Barotrauma Yes } - protected LanguageIdentifier language { get; private set; } = LanguageIdentifier.None; + public LanguageIdentifier Language { get; private set; } = LanguageIdentifier.None; private int languageVersion = 0; protected string cachedValue = ""; @@ -32,13 +32,13 @@ namespace Barotrauma protected void UpdateLanguage() { - language = GameSettings.CurrentConfig.Language; + Language = GameSettings.CurrentConfig.Language; languageVersion = TextManager.LanguageVersion; } protected virtual bool MustRetrieveValue() //this can't be called on other LocalizedStrings by derived classes { - return language != GameSettings.CurrentConfig.Language || languageVersion != TextManager.LanguageVersion; + return Language != GameSettings.CurrentConfig.Language || languageVersion != TextManager.LanguageVersion; } protected static bool MustRetrieveValue(LocalizedString str) //this can be called by derived classes diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs index 0056fc9da..7c8ede811 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/LocalizedString/TagLString.cs @@ -54,10 +54,10 @@ namespace Barotrauma return (loaded ? candidates.GetRandomUnsynced() : "", loaded); } - var (value, loaded) = tryLoad(language); + var (value, loaded) = tryLoad(Language); loadedSuccessfully = loaded ? LoadedSuccessfully.Yes : LoadedSuccessfully.No; cachedValue = value; - if (!loaded && language != TextManager.DefaultLanguage) + if (!loaded && Language != TextManager.DefaultLanguage) { (value, _) = tryLoad(TextManager.DefaultLanguage); cachedValue = value; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs index 78cc321f7..f4bd75f05 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Text/TextManager.cs @@ -20,6 +20,8 @@ namespace Barotrauma public static class TextManager { + public static bool DebugDraw; + public readonly static LanguageIdentifier DefaultLanguage = "English".ToLanguageIdentifier(); public readonly static ConcurrentDictionary> TextPacks = new ConcurrentDictionary>(); public static IEnumerable AvailableLanguages => TextPacks.Keys; diff --git a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs index f66cdcfe5..1aa43ae28 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Upgrades/UpgradePrefab.cs @@ -43,9 +43,9 @@ namespace Barotrauma } } - public int GetBuyprice(int level, Location? location = null) + public int GetBuyPrice(int level, Location? location = null) { - int maxLevel = Prefab.MaxLevel; + int maxLevel = Prefab.GetMaxLevelForCurrentSub(); if (level > maxLevel) { maxLevel = level; } @@ -209,9 +209,17 @@ namespace Barotrauma { if (type is MaxLevelModType.Invalid) { return false; } + int subTier = sub.Tier; + if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) + { + int modifier = metadata.GetInt(new Identifier("tiermodifieroverride"), 0); + + subTier = Math.Max(modifier, subTier); + } + if (tierOrClass.TryGet(out int tier)) { - return sub.Tier == tier; + return subTier == tier; } if (tierOrClass.TryGet(out SubmarineClass subClass)) @@ -292,16 +300,10 @@ namespace Barotrauma onRemoveOverrideFile: null ); - private readonly int maxLevel; - - public int MaxLevel - { - get - { - Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub; - return sub is { Info: var info } ? GetMaxLevel(info) : maxLevel; - } - } + /// + /// Maximum upgrade level without taking submarine tier or class restrictions into account + /// + public readonly int MaxLevel; public LocalizedString Name { get; } @@ -343,7 +345,7 @@ namespace Barotrauma { Name = element.GetAttributeString("name", string.Empty)!; Description = element.GetAttributeString("description", string.Empty)!; - maxLevel = element.GetAttributeInt("maxlevel", 1); + MaxLevel = element.GetAttributeInt("maxlevel", 1); SuppressWarnings = element.GetAttributeBool("supresswarnings", false); HideInMenus = element.GetAttributeBool("hideinmenus", false); SourceElement = element; @@ -428,15 +430,33 @@ namespace Barotrauma .ToImmutableHashSet() ?? ImmutableHashSet.Empty; } + /// + /// Returns the maximum upgrade level for the current sub, taking tier and class restrictions into account + /// + public int GetMaxLevelForCurrentSub() + { + Submarine? sub = GameMain.GameSession?.Submarine ?? Submarine.MainSub; + return sub is { Info: var info } ? GetMaxLevel(info) : MaxLevel; + } + + /// + /// Returns the maximum upgrade level for the specified sub, taking tier and class restrictions into account + /// public int GetMaxLevel(SubmarineInfo info) { - int level = maxLevel; + int level = MaxLevel; foreach (UpgradeMaxLevelMod mod in MaxLevelsMods) { if (mod.AppliesTo(info)) { level = mod.GetLevelAfter(level); } } + if (GameMain.GameSession?.Campaign?.CampaignMetadata is { } metadata) + { + int modifier = metadata.GetInt(new Identifier($"tiermodifiers.{Identifier}"), 0); + level += modifier; + } + return level; } diff --git a/Barotrauma/BarotraumaShared/changelog.txt b/Barotrauma/BarotraumaShared/changelog.txt index 8e9e8e7ac..d8999df6b 100644 --- a/Barotrauma/BarotraumaShared/changelog.txt +++ b/Barotrauma/BarotraumaShared/changelog.txt @@ -1,3 +1,111 @@ +--------------------------------------------------------------------------------------------------------- +v0.20.0.0 +--------------------------------------------------------------------------------------------------------- + +Talent overhaul: +- Redesigned and rebalanced talents: lots of new talents, changes and balancing to existing ones and replacing some of the more broken ones with new ones. +- Redesigned talent tree structure: there's now a selection of "generic" talents, and after you've unlocked 4 of them, you can choose one specialization talent tree (meaning that you can no longer unlock all trees, but have to specialize in one). +- There's still a lot to test, balance and fix in the new talents, so all feedback and issue reports regarding the talents are much appreciated: + - Are there some talents that don't work as expected? + - Are the XP bonuses too large or too small? + - In general, how balanced are the talents, are there any that feel OP or too weak? + - Any inaccuracies or things to improve in the talent descriptions? + +Changes and additions: +- Added a button for opening the Steam Workshop to all tabs of the workshop menu. +- Added tooltips that explain how the bot spawn modes work to the server lobby. +- Made saline significantly less effective as a treatment for bloodloss to make blood packs more useful. +- Nerfed flak cannon's explosive ammo. +- Allow putting medium items (e.g. storage container) in medical and toxic cabinets. + +Multiplayer: +- Fixed "Input contains duplicate packages" error still occuring if you try to join a server that has empty content packages when you don't have those packages yourself. +- Fixed networking errors when the connection to the server is momentarily lost and then re-established. +- Added a cooldown to client name changes to prevent using it for spamming. +- Fixed bans issued with the "banaddress" command using a client's Steam ID not working. + +Bugfixes: +- Fixed Esc not closing the campaign interfaces (map, store, shipyard, etc) but opening the pause menu instead. +- Fixed bots sometimes firing in a random direction when they equip a weapon. Happened because the aim and shoot inputs could already be active when the bot switches from another item to the weapon (e.g. from underwater scooter to some gun). +- Fixed pirates sometimes being unable to operate multiple turrets at the same time (even if there's enough crew to operate multiple), and attempting to operate hardpoints. +- Fabricator chooses the available ingredient that's in the worst condition when there's multiple suitable ingredients available. +- Fixed characters falling off ladders when using aimable tools. +- Fixed currently selected mission being included in the mission count displayed on the campaign map (i.e. showing "1/2" when you're choosing a new destination at an empty location). +- Fixed inability to sit in cafeteria chairs. +- Fixed projectile impacts getting triggered by gravity spheres and other TriggerComponents (meaning you couldn't hit monsters near a gravity sphere). +- Fixed blood pack fabrication recipe outputting only one item. +- Fixed tutorial not progressing when inserting a welding fuel tank inside the welding tool straight away, rather than inventory first. +- Fixed PUCS not beeping when you're underwater without a tank if you're inside a hull that has oxygen in it. +- Fixed some issues in sonar AITargets which made monsters hear the sonar when they shouldn't: switching to passive would immediately make the current directional ping cover 360 degrees, and whether the ping was directional or not would actually depend on whether the previous ping was directional, not what the mode is now. +- Fixed items getting autofilled into non-interactable containers in wrecks and outposts. +- Fixes to ID card tag issues in wrecks (prevented accessing the secure containers with the ID cards looted from the corpses). +- Fixed verifying file integrity on Steam resetting the server settings file. +- Fixed crashing if you try to open an access-restricted directory in the file selection dialog. +- Fixed a typo in physicorium shell's damage config, causing it to not do bleeding damage. +- Fixed money gain/lose popups no longer showing in the campaign. +- Fixed bloodloss and drunkenness never fully healing, just dropping below the threshold at which the icon appears. Caused e.g. drunkenness and bloodloss to never fully go away, causing issues with some talent effects. +- Fixed bots always opening the door/hatch they're trying to repair. +- Fixed power indicator not rotating with batteries. +- Fixed lights on welding tools and plasma cutters emitting light the next round if the round ends while using them. +- Fixed Camel's airlock not draining fully. +- Fixed Berilia's bottom EDC not being wired to a supercapacitor and a loose wire between the flak cannon and the right supercapacitor. + +Modding: +- Fixed increasing an item's HealthMultiplier making the items appear damaged in existing subs/saves (e.g. if you doubled an item's maximum condition, the items would remain in the old maximum condition and appear 50% damaged). +- Fixed crashing if a talent is triggered when the character receives some affliction, and that talent applies the same affliction on the character. +- Fixed crashing if the ingredient of a fabrication recipe can't be found. +- Fixed inability to sync properties of ItemComponents that the item has multiple of (meaning that it was only possible to e.g. edit the light color of the item's first LightComponent if it has multiple). + +--------------------------------------------------------------------------------------------------------- +v0.19.14.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed submarine upgrades getting clamped to the maximum of that upgrade between rounds, disregarding class/tier bonuses. + +--------------------------------------------------------------------------------------------------------- +v0.19.13.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed "failed to find the end of the bit field after 100 reads" error when trying to join a server that has a large number of mods enabled. +- Fixed "Tandem Fire" talent causing a crash due to the changes in the previous version. + +--------------------------------------------------------------------------------------------------------- +v0.19.12.0 +--------------------------------------------------------------------------------------------------------- + +- Fixed submarine upgrades getting lost if you switch to a lower-tier sub that can't have as many levels of upgrades as the current sub, and then back again. +- Fixed some monster events not being as common/uncommon as intended. In more technical terms (which may be of interest to modders): the commonness defined as an attribute of an EventSet did nothing, making the event default to a commonness of 1. The commonnesses defined for specific level types worked correctly. +- Fixed clients getting stuck in a non-functional lobby if they happen to disconnect or get kicked back to the lobby at a specific point when loading a new round. +- Fixed large turret hardpoint origin being off, causing turrets installed on a large hardpoint to be misaligned. +- Attempt to fix crashing when disconnecting from the server you're hosting. +- Fixed Ctrl+Shift+S shortcut (quicksave) not working in the sub editor. +- Fixed toolbelts and storage containers in old subs going inside toolbelts. +- Fixed submarine tier resetting to default when reopening the sub editor's save dialog. +- Fixed sub editor not taking filename case into account when saving an existing sub: if you'd try to save the file with a different filename case, it'd ask about overwriting the existing sub, but save it as a new file even if you opt to overwrite. +- Fixes to Herja room names (use Engineering, Gunnery compartment, etc. labels), add camera to the front, with a periscope for the captain. +- Fixed non-purchaseable talent items not being available as extra cargo. +- Sorted extra cargo alphabetically + added a filter box. +- Fixed taking items that spawned inside another item (e.g. tanks in a diving mask) from NPCs spawned by an event not counting as stealing. +- Fixed characters falling off ladders when using aimable tools. +- Fixed money gain/lose popups no longer showing in the campaign. +- Fixed inability to manage the campaign if there's no-one with permissions alive. Previously we allowed anyone to manage the campaign if there's no-one with permissions present in the server, but that's not enough, because the players with permissions can't end the round if they're dead. Now if there's no-one with permissions alive, anyone is allowed to manage the campaign. +- If Select and Deselect have been bound to the same key, the deselect input is ignored when interacting with another item than the selected one. Prevents e.g. falling off ladders when trying to open a hatch when both Select and Deselect have been bound to E. +- Made it possible to enter a hostname (e.g. someserver.com) in the direct join prompt. +- Adjusted the size of the submarine list elements in the server lobby to reduce the amount of empty space on large resolutions. +- Fixed event texts for the "scan ruin" mission being in an incorrect language. +- Attachable items cannot be attached inside walls. +- Fixed distance at which you can attach items being slightly longer than the interact distance, making it possible to attach items out of reach. +- Fixed inability to turn when you're dual wielding melee weapons and attacking continuously. +- Fixed inability to cancel deconstruction if there's non-deconstructible items in the queue. +- Fixed local copy of a mod you're publishing not using the version number you've entered in the publish menu. +- Fixed crashing when trying to open the tab menu's character tab with a character who has no personality trait (may happen e.g. if you use a mod that adds custom personality traits and try to play that save without the mod). +- Fixed sourcerect issue in alien generator + decorative sprite not disappearing when the fuel rod is taken out. +- Fixed corrupted mods causing a nullref exception when autodetecting required mods in the sub editor. +- Fixed minerals not disappearing from mineral scanner if they get detached by something else than a character picking them up (e.g. by the destructible ice wall they're on breaking). +- Fixed event-specific metal crate deconstructing to steel. +- Fixed inability to join servers that have enabled multiple mods with identical content. +- Fixed tandem fire not working if there's a character between you and the other character on a periscope. + --------------------------------------------------------------------------------------------------------- v0.19.11.0 --------------------------------------------------------------------------------------------------------- diff --git a/Barotrauma/BarotraumaShared/serversettings.xml b/Barotrauma/BarotraumaShared/serversettings.xml deleted file mode 100644 index 482899fc2..000000000 --- a/Barotrauma/BarotraumaShared/serversettings.xml +++ /dev/null @@ -1,57 +0,0 @@ - - \ No newline at end of file diff --git a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs index e319baede..fdd87cc18 100644 --- a/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs +++ b/Barotrauma/BarotraumaTest/INetSerializableStructTests.cs @@ -29,7 +29,17 @@ namespace TestProject [Fact] public void TestBitField() { + // 0-length bitfield test + SerializeDeserializeBitField(Array.Empty()); + + // Normal bitfield test Prop.ForAll(SerializeDeserializeBitField).VerboseCheckThrowOnFailure(); + + // Large bitfield test + Prop.ForAll( + Arb.Generate().Resize(1000).Where(arr => arr.Length >= 800) + .ToArbitrary(), + SerializeDeserializeBitField).VerboseCheckThrowOnFailure(); } [Fact] @@ -278,7 +288,7 @@ namespace TestProject private static void SerializeDeserializeBitField(bool[] arg) { ReadWriteMessage msg = new ReadWriteMessage(); - IWritableBitField bitFieldWrite = new WriteOnlyBitField(); + WriteOnlyBitField bitFieldWrite = new WriteOnlyBitField(); foreach (bool b in arg) { @@ -288,7 +298,7 @@ namespace TestProject bitFieldWrite.WriteToMessage(msg); msg.BitPosition = 0; - IReadableBitField bitFieldRead = new ReadOnlyBitField(msg); + ReadOnlyBitField bitFieldRead = new ReadOnlyBitField(msg); foreach (bool b in arg) {